React/Library

리액트 엑셀 - 3. 정렬하기 (xlsx, file-saver)

또롱또 2023. 6. 27. 23:55
728x90

3편 정렬하기 입니다.

잘못들어온 데이터가 아닌, 기본 구조를 변경해 달라는 요청은 자주 기각되기 때문에, 때로는 알아서 구조에 맞출 필요가 있습니다.

 

아무리 이 업계에서 오래된 사람이라도 모든 케이스들을 예상하기는 어렵기 때문에,


처음에 짠 구조와 다르게 데이터 필드가 추가되기도 삭제되기도 합니다. 


또한 PM에 의해서 구조 전체가 뒤집힐 때도 많습니다. (🔅진짜 많음, 주니어 반박불가🔅)

 

물론 이런 다이나믹한 케이스들에도 유연하게 대처가 가능해야 합니다.

네 지금이 그런 케이스 라고 해보겠습니다.

 


먼저 우리 프로젝트 매니저는 정렬 순서를 이번주, 다음주, 다다음주 다 다르게 할거라 합니다.

즉, 정렬은 어차피 바뀔텐데, 매번 데이터를 일일이 작업할 수는 없으니,

아예 배열로 데이터 순서를 넣어두고, 그 순서대로 출력되게 하는건 어떨까요?

data_structure.js / data_struture.ts 파일에 

export const BasicInfoOrder = ["name", "region", "occupation", "age"];

 

이렇게 데이터 순서를 추가합니다.

이러면 만약 다다음주에 담당자가 아 나는 국적을 제일 뒤로 보낼래 라고 말하면,

export const BasicInfoOrder = ["name",  "occupation", "age" , "region"];

 

이렇게 이 배열 순서만 바꿔주면 됩니다.

더 좋은 케이스로는 이렇게 코드내에서 변경이 아니라, 직접 이 변경하는 input과 버튼들을 브라우저에 띄워서 

변경하는게 좋겠지만, 이번에는 거기까지는 하지 않겠습니다.


그러면 이제 들어올 순서는 정했고, 이 순서대로 데이터를 정렬해 주면 됩니다.

새로운 props로 이 데이터order를 받습니다.

그리고 아래가 데이터를 정렬하는 코드 입니다. 자세한 내용은 주석으로 추가했습니다.

const sortedData = props.data.map((obj) => {
     // 정렬된 object를 담을 변수
      const sortedObj = {};
      // Object.keys()를 통해, data배열 안의 object들에게서 key를 뽑아냅니다.
      // 그리고 만약 props로 dataOrder가 있을경우, sort()를 통해 dataOrder의 순서대로 keys를 정렬합니다.
     // 만약 dataOrder가 없으면 그냥 숫자 0을 반환해서 원래 들어온 배열의 순서를 유지합니다.
      const sortedKeys = Object.keys(obj).sort((a, b) => {
        return props.dataOrder
          ? props.dataOrder.indexOf(a) - props.dataOrder.indexOf(b)
          : 0;
      });
      
      // 만약 pm이 안보고 싶은 필드가 있을지도 모르니까, sortedKey안에서 제가 원하는 헤더들만
     // 필터링 해서 새로운 key 변수에 담아줍니다.
      const desiredKeys = sortedKeys.filter(
        (key) => props.dataOrder && props.dataOrder.includes(key)
      );

      // 모아둔 키 내부를 돌면서, props.data의 object랑 key랑 맞는 데이터를 할당합니다.
      desiredKeys.forEach((key) => {
        sortedObj[key] = obj[key];
      });

      // 새롭게 정렬된 object를 return 합니다.
      return sortedObj;
    });

그다음으로 한 곳만 더 고치면 됩니다.


아래 부분인데요, 어떤 파일은 이렇게 데이터 순서가 필요없이 정리가 잘 된 데이터 일 수도 있습니다.

그럴경우에 위처럼 정렬연산을 하는건 낭비이므로, props에 dataOrder가 들어있으면 

정렬된 데이터에서 looping을 하고, 만약 그게 없으면 그냥 들어오는 순수 data에서 loping을 합니다.

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

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

테스트를 해보겠습니다.

 

테스트에 사용될 데이터 입니다.

더보기
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",
  },
];

App.js / App.tsx에서 불러주던 컴포넌트에는 한가지 필드를 추가해 주었습니다. (dataOrder)

const App = () => {
  return (
    <>
      <ExcelImporter
        fileName="기본정보표"
        header={BasicInfoHeader}
        cellSize={BasicInfoCellSize}
        tabName="data"
        btnTxt="기본 정보 엑셀 추출 버튼"
        data={data}
        dataOrder={BasicInfoOrder} // 추가된 필드
      />
      <ExcelImporter
        fileName="신체정보표"
        header={PhysicalInfoHeader}
        cellSize={PhysicalInfoCellSize}
        tabName="data"
        btnTxt="신체 정보 엑셀 추출 버튼"
        data={data2}
      />
    </>
  );
};

 

성공입니다. 이제 데이터가 정리가 안되어도, 프론트엔드에서 정리된 데이터를 추출하는게 가능해 졌습니다.


🔅 전체코드 (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: Workbook = {
      Sheets: {},
      SheetNames: [],
    };

    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);

    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[];
  cellSize: CellType[];
  tabName: string;
  btnTxt: string;
  data: Record<string, string | number>[];
  dataOrder?: string[];
}

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: [],
    };

    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);

    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;

세번째 목표인 정렬도 끝이 났습니다.

 

프론트엔드는 많은 사람들과 소통을 하는 포지션에 있습니다.

 

프론트엔드는 디자이너의 요구사항도 들어줘야하고, PM의 구조설계대로 데이터를 시각화 해야하고, 서버팀과 데이터를 주고 받기도 해야합니다. 더 나아가면, QM이나 테스터 들과도 소통을 해야하는 포지션 입니다.

 

그만큼 소통의 문제로 고생을 할 확률이 아주 큰 포지션 이지만, 만약 소통이 불가할 경우 (생각보다 많습니다..)

 

이렇게 유연하게 고생을 1 stack 추가해서 해결 할 수도 있습니다.

 

다음 시간에는 엑셀다루기 마지막 편 입니다.

 

PM이 엑셀파일 한개에 기본정보랑 신체정보 모두 담아서 보고싶다고 합니다. 🤷‍♂️

 

탭을 여러개 활용하는 멀티 엑셀탭을 활용하는 법에 대해 알아보겠습니다.

728x90