React/Library

리액트 엑셀 - 4. 여러 탭 활용하기 (xlsx, file-saver)

또롱또 2023. 6. 29. 03:49
728x90

4편 한 파일내에서 여러탭을 활용하기 입니다.

프로젝트 매니저가 또 찾아오더니 갑자기 자신은 지금처럼 여러개의 버튼으로 각 데이터를 추출하는 것도 필요하지만,

 

버튼 한개로 해당 페이지내의 모든 데이터들을 여러개의 탭으로 정리해서 한개의 파일로 받아보고 싶다고 합니다.

 

즉 우리가 하나의 웹 브라우저에서 탭 여러개 키듯이,

 

🆘 하나의 엑셀파일에서 여러개의 탭에 데이터를 여러개 정리하고 싶다고 합니다. 

 

🔅 프로젝트 매니저가 원하는 것 중에 거절하며 돌려보내기엔 데이터 관리페이지에 실제로 엄청 필요한 기능입니다.

 

그래서 원래 만들었던 글로벌 엑셀 컴포넌트에 props로 멀티 데이터를 받아서 엑셀로 추출하는걸 해보려 합니다.


먼저 사용할 데이터는 원래 데이터 그대로 사용하겠습니다.

더보기
export const data = [
  {
    name: "Jack",
    region: "USA",
    age: 30,
    occupation: "Lawyer",
  },
  {
    region: "Canada",
    name: "Emily",
    age: 25,
    occupation: "Engineer",
  },
  {
    age: 42,
    region: "UK",
    name: "Sophia",
    occupation: "Doctor",
  },
  {
    name: "Michael",
    age: 35,
    region: "Australia",
    occupation: "Teacher",
  },
  {
    occupation: "Designer",
    name: "Emma",
    region: "Germany",
    age: 28,
  },
];

export const data2 = [
  {
    name: "Jack",
    gender: "남자",
    eye: "blue",
  },
  {
    name: "Emily",
    gender: "여자",
    eye: "brown",
  },
  {
    name: "Sophia",
    gender: "여자",
    eye: "green",
  },
  {
    name: "Michael",
    gender: "남자",
    eye: "hazel",
  },
  {
    name: "Emma",
    gender: "여자",
    eye: "blue",
  },
];

타입스크립트 쓰시는 분들은 IPropsType에 아래 부분에 ?  추가하겠습니다.

  data?: Record<string, string | number>[];
  dataOrder?: string[];
  cellSize?: CellType[];

그리고 아래 sorting 하던 부분도 props.data? 이렇게 ? 하나 추가하겠습니다.

  const sortedData = props.data?.map( // 해당부분
      (obj: Record<string, string | number>) => {
        const sortedObj: Record<string, string | number> = {};
        const sortedKeys = Object.keys(obj).sort((a, b) => {
          return props.dataOrder
            ? props.dataOrder.indexOf(a) - props.dataOrder.indexOf(b)
            : 0;
        });

다시 돌아와서 이제 파일을 이런식으로 넘겨줄 예정입니다.

필요없는건 다 떼고, multiData에 이렇게 2차원배열을 넘겨줄 겁니다. 배열 order도 이렇게 넘겨줍니다. 

 <ExcelImporter
    fileName="전체정보표"
    header={PhysicalInfoHeader}
    cellSize={PhysicalInfoCellSize}
    tabName="data"
    btnTxt="전체 정보 엑셀 추출 버튼"
    multiData={[data, data2]} // 추가된 부분
    multiDataOrder={[BasicInfoOrder, PhysicalInfoOrder]} // 추가된 부분
    multiCellSize={[BasicInfoCellSize, PhysicalInfoCellSize]}// 추가된 부분
  />

PhysicalInfoOrder도 언젠가 PM이 변경해달라면 해줘야하니 미리 만들어 뒀습니다.

export const PhysicalInfoOrder = ["name", "gender", "eye"];

console.log를 찍어보니 잘 들어왔습니다.

  console.log(props.multiData);
  console.log(props.multiDataOrder);


플로우를 생각해 보겠습니다.

 

엑셀함수 내에서 이게 싱글데이터인지, 멀티데이터인지 구별해서 멀티데이터면, 탭을 만들어서 데이터를 보여주고, 싱글데이터면 원래 코드를 보여주고 하면 좋을거 같습니다.

 

우리가 props로 넘겨주고 있는 것 중에 싱글데이터면 data에 보내주고있고, 멀티데이터면 multiData에 보내주고 있습니다.

 

그래서 엑셀함수를 싱글데이터를 기준으로 아래처럼 조건문으로 나눠도 좋을거 같습니다.

 

(제가 타입스크립트로 작성하고 타입을 지우면서 자바스크립트로 만들어 올리다보니, 미처 못지운 타입이 있을지 모릅니다. 자바스크립트는 타입은 모두 지워주시면 됩니다.)

더보기
 const excelDownload = async () => {
    const wb = {
      Sheets: {},
      SheetNames: [],
    };

    // 싱글데이터
   if (props.data && props.dataOrder && props.cellSize)  {
      const sortedData = props.data?.map(
        (obj) => {
          const sortedObj = {};
          const sortedKeys = Object.keys(obj).sort((a, b) => {
            return props.dataOrder
              ? props.dataOrder.indexOf(a) - props.dataOrder.indexOf(b)
              : 0;
          });

          const desiredKeys = sortedKeys.filter(
            (key) => props.dataOrder && props.dataOrder.includes(key)
          );

          desiredKeys.forEach((key) => {
            sortedObj[key] = obj[key];
          });
          return sortedObj;
        }
      );

      const ws = XLSX.utils.aoa_to_sheet([
        [`${props.fileName}`],
        [``],
        props.header,
      ]);

      (props.dataOrder ? sortedData : props.data)?.map(
        (v) => {
          XLSX.utils.sheet_add_aoa(ws, [Object.values(v)], {
            origin: -1,
          });

          ws["!cols"] = props.cellSize;
          return false;
        }
      );

      wb.Sheets[props.tabName] = ws;
      wb.SheetNames.push(props.tabName);
    else if (props.multiData && props.multiDataOrder && props.multiCellSize) {
      // 멀티데이터
    }

    const excelButter = XLSX.write(wb, { bookType: "xlsx", type: "array" });
    const excelFile = new Blob([excelButter], { type: excelFileType });
    await new Promise((resolve) => setTimeout(resolve, 1000));
    FileSaver.saveAs(excelFile, excelFileName + excelFileExtension);
  };

이제 멀티데이터를 받아서 정렬을 해주고, 멀티 탭만 만들면 끝이겠습니다.

 

🔅 설명은 주석으로 추가했습니다.

 

더보기
    } else if (props.multiData && props.multiDataOrder && props.multiCellSize) {
      // 멀티데이터 내부를 돌아줍니다.
      props.multiData.forEach(
        (dataSet, index) => {
         // multiDataOrder가 있으면 해당 내용의 index 순서를 multipleOrder 변수에 저장합니다.
          const multipleOrder =
            props.multiDataOrder && props.multiDataOrder[index];

		// 싱글데이터 sorting 하듯이 sorting 합니다.
        // 대부분 같은 코드라서 설명은 생략하겠습니다.
          const sortedData = dataSet.map(
            (obj) => {
              const sortedObj = {};
              const sortedKeys = multipleOrder?.sort((a, b) => {
                return multipleOrder.indexOf(a) - multipleOrder.indexOf(b);
              });

              const desiredKeys = sortedKeys?.filter(
                (key) =>
                  props.multiDataOrder &&
                  props.multiDataOrder[index].includes(key)
              );

              desiredKeys?.forEach((key) => {
                sortedObj[key] = obj[key];
              });
              return sortedObj;
            }
          );

		// 파일 제일 위에 추가될 내용입니다. 
        // 여기서는 props로 배열로 ["파일이름", "파일이름" ]을 넘겨주고
        // index로 찾아도 되겠네요.
          const ws = XLSX.utils.aoa_to_sheet([
            [`${props.fileName} ${index} `],
            [``],
            props.header,
          ]);

          sortedData.forEach((v) => {
            XLSX.utils.sheet_add_aoa(ws, [Object.values(v)], {
              origin: -1,
            });
			
            // 셀 사이즈도 미리 넣어 배열에서 인덱스를 이용해 가져옵니다.
            ws["!cols"] = props.multiCellSize && props.multiCellSize[index];
          });
		
        // 탭 이름입니다.
          const sheetName = `${props.tabName} ${index}`;
          wb.Sheets[sheetName] = ws;
          wb.SheetNames.push(sheetName);
        }
      );
    }

성공입니다. 보니까 데이터가 두개의 탭으로 잘 들어간거 같습니다.

 


🔅 전체코드 (JS):

더보기
import * as XLSX from "xlsx";
import * as FileSaver from "file-saver";


const ExcelImporter = (props) => {
  const excelFileType =
    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8";
  const excelFileExtension = ".xlsx";
  const excelFileName = props.fileName;

  const excelDownload = async () => {
    const wb = {
      Sheets: {},
      SheetNames: [],
    };

    // 싱글데이터
    if (props.data && props.dataOrder && props.cellSize) {
      const sortedData = props.data?.map(
        (obj) => {
          const sortedObj = {};
          const sortedKeys = Object.keys(obj).sort((a, b) => {
            return props.dataOrder
              ? props.dataOrder.indexOf(a) - props.dataOrder.indexOf(b)
              : 0;
          });

          const desiredKeys = sortedKeys.filter(
            (key) => props.dataOrder && props.dataOrder.includes(key)
          );

          desiredKeys.forEach((key) => {
            sortedObj[key] = obj[key];
          });
          return sortedObj;
        }
      );

      const ws = XLSX.utils.aoa_to_sheet([
        [`${props.fileName}`],
        [``],
        props.header,
      ]);

      (props.dataOrder ? sortedData : props.data)?.map(
        (v) => {
          XLSX.utils.sheet_add_aoa(ws, [Object.values(v)], {
            origin: -1,
          });

          ws["!cols"] = props.cellSize;
          return false;
        }
      );

      wb.Sheets[props.tabName] = ws;
      wb.SheetNames.push(props.tabName);
    } else if (props.multiData && props.multiDataOrder && props.multiCellSize) {
      // 멀티데이터
      props.multiData.forEach(
        (dataSet, index) => {
          const multipleOrder =
            props.multiDataOrder && props.multiDataOrder[index];

          const sortedData = dataSet.map(
            (obj) => {
              const sortedObj = {};

              const sortedKeys = multipleOrder?.sort((a, b) => {
                return multipleOrder.indexOf(a) - multipleOrder.indexOf(b);
              });

              const desiredKeys = sortedKeys?.filter(
                (key) =>
                  props.multiDataOrder &&
                  props.multiDataOrder[index].includes(key)
              );

              desiredKeys?.forEach((key) => {
                sortedObj[key] = obj[key];
              });
              return sortedObj;
            }
          );

          const ws = XLSX.utils.aoa_to_sheet([
            [`${props.fileName} ${index} `],
            [``],
            props.header,
          ]);

          sortedData.forEach((v) => {
            XLSX.utils.sheet_add_aoa(ws, [Object.values(v)], {
              origin: -1,
            });

            ws["!cols"] = props.multiCellSize && props.multiCellSize[index];
          });

          const sheetName = `${props.tabName} ${index}`;
          wb.Sheets[sheetName] = ws;
          wb.SheetNames.push(sheetName);
        }
      );
    }

    const excelButter = XLSX.write(wb, { bookType: "xlsx", type: "array" });
    const excelFile = new Blob([excelButter], { type: excelFileType });
    await new Promise((resolve) => setTimeout(resolve, 1000));
    FileSaver.saveAs(excelFile, excelFileName + excelFileExtension);
  };

  const handleDownloadExcel = () => {
    excelDownload();
  };

  return <button onClick={handleDownloadExcel}>{props.btnTxt}</button>;
};

export default ExcelImporter;

🔅 전체코드 (TS):

더보기
import * as XLSX from "xlsx";
import * as FileSaver from "file-saver";

interface IPropsType {
  fileName: string;
  header: string[];
  tabName: string;
  btnTxt: string;
  data?: Record<string, string | number>[];
  dataOrder?: string[];
  cellSize?: CellType[];
  multiData?: Record<string, string | number>[][];
  multiDataOrder?: string[][];
  multiCellSize?: CellType[][];
}

interface CellType {
  wpx: number;
}

interface Worksheet {
  [key: string]: XLSX.WorkSheet;
}

interface Workbook {
  Sheets: Worksheet;
  SheetNames: string[];
}

const ExcelImporter = (props: IPropsType) => {
  const excelFileType =
    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8";
  const excelFileExtension = ".xlsx";
  const excelFileName = props.fileName;

  const excelDownload = async () => {
    const wb: Workbook = {
      Sheets: {},
      SheetNames: [],
    };

    // 싱글데이터
    if (props.data && props.dataOrder && props.cellSize) {
      const sortedData = props.data?.map(
        (obj: Record<string, string | number>) => {
          const sortedObj: Record<string, string | number> = {};
          const sortedKeys = Object.keys(obj).sort((a, b) => {
            return props.dataOrder
              ? props.dataOrder.indexOf(a) - props.dataOrder.indexOf(b)
              : 0;
          });

          const desiredKeys = sortedKeys.filter(
            (key) => props.dataOrder && props.dataOrder.includes(key)
          );

          desiredKeys.forEach((key) => {
            sortedObj[key] = obj[key];
          });
          return sortedObj;
        }
      );

      const ws = XLSX.utils.aoa_to_sheet([
        [`${props.fileName}`],
        [``],
        props.header,
      ]);

      (props.dataOrder ? sortedData : props.data)?.map(
        (v: Record<string, string | number>) => {
          XLSX.utils.sheet_add_aoa(ws, [Object.values(v)], {
            origin: -1,
          });

          ws["!cols"] = props.cellSize;
          return false;
        }
      );

      wb.Sheets[props.tabName] = ws;
      wb.SheetNames.push(props.tabName);
    } else if (props.multiData && props.multiDataOrder && props.multiCellSize) {
      // 멀티데이터
      props.multiData.forEach(
        (dataSet: Record<string, string | number>[], index: number) => {
          const multipleOrder =
            props.multiDataOrder && props.multiDataOrder[index];

          const sortedData = dataSet.map(
            (obj: Record<string, string | number>) => {
              const sortedObj: Record<string, string | number> = {};

              const sortedKeys = multipleOrder?.sort((a, b) => {
                return multipleOrder.indexOf(a) - multipleOrder.indexOf(b);
              });

              const desiredKeys = sortedKeys?.filter(
                (key) =>
                  props.multiDataOrder &&
                  props.multiDataOrder[index].includes(key)
              );

              desiredKeys?.forEach((key) => {
                sortedObj[key] = obj[key];
              });
              return sortedObj;
            }
          );

          const ws = XLSX.utils.aoa_to_sheet([
            [`${props.fileName} ${index} `],
            [``],
            props.header,
          ]);

          sortedData.forEach((v: any) => {
            XLSX.utils.sheet_add_aoa(ws, [Object.values(v)], {
              origin: -1,
            });

            ws["!cols"] = props.multiCellSize && props.multiCellSize[index];
          });

          const sheetName = `${props.tabName} ${index}`;
          wb.Sheets[sheetName] = ws;
          wb.SheetNames.push(sheetName);
        }
      );
    }

    const excelButter = XLSX.write(wb, { bookType: "xlsx", type: "array" });
    const excelFile = new Blob([excelButter], { type: excelFileType });
    await new Promise((resolve) => setTimeout(resolve, 1000));
    FileSaver.saveAs(excelFile, excelFileName + excelFileExtension);
  };

  const handleDownloadExcel = () => {
    excelDownload();
  };

  return <button onClick={handleDownloadExcel}>{props.btnTxt}</button>;
};

export default ExcelImporter;

이렇게 리액트 엑셀 다루기가 끝났습니다.

 

테이블이나 차트를 엑셀 데이터로 추출해야하는 일은 생각보다 많습니다.

 

상품의 수량 등등을 엑셀화 하는 작업을 많이 하고 있기도 하고,

 

블로그만 하더라도 방문자 통계 등등을 엑셀화 하는 작업도 많이 있겠네요.

 

웹개발로 전향하여 겨우 커리어 6개월차 주니어👶라 아직 생각대로 만들지도 못하고, 투박한 부분도 많습니다.

 

🔅 지적은 언제든지 환영입니다. 🔅 

728x90