ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 엑셀 파일 다운로드 기능 구현 여정 🚀
    개발여정 2024. 7. 12. 20:15

    회사 프로젝트에서 사용자가 화면에서 데이터를 확인하는 것뿐만 아니라 검색 결과를 엑셀 파일로 다운로드할 수 있는 기능을 구현해야 했습니다.

     

    완성 코드 미리보기 👀

    https://codesandbox.io/p/sandbox/excel-download-poc-yv2y26?file=%2Fsrc%2FExcelSheet.ts%3A18%2C30

     

    무엇을 고려해야 할까...? 🤔

    어떻게 구현할지 고민하기에 앞서 만족되어야 하는 조건을 다음과 같이 고려해봤습니다.

    • 클라이언트 사이드에서 엑셀에 필요한 데이터를 엑셀 파일로 변환할 수 있을 것
    • 변환한 엑셀 파일을 저장할 수 있는 기능을 제공할 것
    • 다양한 브라우저에서 엑셀 다운로드 기능이 지원될 것
    • 타입스크립트를 지원할 것

     

    어떻게 구현하지...? 🔍

    엑셀다운로드 기능 구현을 위해서 라이브러리를 찾아봤었고 각 라이브러리의 문서를 읽어보며 어떤 특징이 있는지 빠르게 파악했습니다.

    1. exceljs
      • Node.js 환경에서 사용되며, 서버 측에서 엑셀 파일을 생성하는 데 적합
    2. xlsx
      • 복잡한 스프레드시트로부터 데이터를 추출하거나 새로운 스프레드시트를 생성하는 라이브러리
      • 이미지, 그래프 피봇테이블, 함수 등의 일반적인 스프레드시트 작업들을 수행 가능
      • 타입스크립트 지원
      • 클라이언트 사이드에서 파일을 저장할 수 있도록 하는 유틸리티 함수를 제공
      • 지원하는 브라우저는 다음과 같음

     

    xlsx... 너로 정했다!!

    위 두 라이브러리를 고려한 결과, 위에서 만족해야하는 조건 중 클라이언트 사이드에서 데이터를 엑셀로 변환하고 저장하는 기능을 가장 잘 지원하는 xlsx로 결정했습니다.

     

    xlsx 라이브러리를 사용하면 다음과 같이 엑셀 파일 생성부터 다운로드까지 간단하게 구현할 수 있습니다.

    // 1. 파일 생성
    const workbook = XLSX.utils.book_new();
    
    // 2. 시트 생성
    const worksheet = XLSX.utils.aoa_to_sheet(excelData);
    
    // 3. 파일에 시트 추가
    XLSX.utils.book_append_sheet(workbook, worksheet, sheetName, true);
    
    // 4. 파일 다운로드
    XLSX.writeFile(workbook, fileName);

     

    구현을 해보자!

    이처럼 xlsx에서 제공하는 함수를 호출하여 간단히 기능을 구현할 수 있지만, 재사용성과 유지보수성을 고려했을 때 좀 더 구조적으로 만들 필요성을 느껴 다음과 같이 구조화를 했습니다.

    • ExcelFile: 엑셀 파일 전체를 관리하고 생성하는 역할
    • ExcelSheet: 각 시트의 데이터를 관리하고 시트를 생성하는 역할
    • ExcelCell: 각 셀의 데이터를 관리하고 엑셀 셀 객체로 변환하는 역할

     

    ExcelFile 클래스

    import * as XLSX from 'xlsx';
    import ExcelSheet from './ExcelSheet';
    
    export default class ExcelFile {
      private workbook: XLSX.WorkBook = XLSX.utils.book_new(); // 엑셀 파일
      private fileName: string; // 파일 이름
      private fileExt: XLSX.BookType; // 파일 확장자
    
      public constructor(fileName: string, fileExt: XLSX.BookType = 'xlsx') {
        this.fileName = fileName;
        this.fileExt = fileExt;
      }
    
      // 파일 전체 이름을 반환
      public getFullFileName(): string {
        return `${this.fileName}.${this.fileExt}`;
      }
    
      // 엑셀 파일을 반환
      public getWorkbook(): XLSX.WorkBook {
        return this.workbook;
      }
    
      // 엑셀 시트를 추가
      public appendExcelSheet(sheet: ExcelSheet): void {
        XLSX.utils.book_append_sheet(this.workbook, sheet.getWorksheet(), sheet.getName(), true);
      }
      
      // 엑셀 파일 다운로드
      public downloadFile(): void {
        XLSX.writeFile(this.getWorkbook(), this.getFullFileName());
      }
    }

    ExcelSheet 클래스

    import * as XLSX from 'xlsx';
    import ExcelCell from './ExcelCell';
    
    export default class ExcelSheet {
      private sheetName: string; // 엑셀 시트 이름
      private sheetData: ExcelCell[][]; // 엑셀 시트 데이터
    
      public constructor(sheetName: string, sheetData: ExcelCell[][] = []) {
        this.sheetName = sheetName;
        this.sheetData = sheetData;
      }
    
      // 엑셀 시트 이름을 반환
      public getName(): string {
        return this.sheetName;
      }
    
      // 한 행의 데이터를 추가
      public appendRowData(row: ExcelCell[]): void {
        this.sheetData.push(row);
      }
    
      // 엑셀 시트를 반환
      public getWorksheet(): XLSX.WorkSheet {
        return XLSX.utils.aoa_to_sheet(this.sheetData.map((row) => row.map((cell) => cell.toCellObject())));
      }
    }

    ExcelCell 클래스

    import * as XLSX from 'xlsx';
    
    export type ExcelValueType = string | number | boolean | Date;
    
    export type CellFormat = 'string' | 'number' | 'boolean' | 'date';
    
    export default class ExcelCell {
      private value: ExcelValueType;
      private format: CellFormat;
    
      public constructor(value: any, format: CellFormat = 'string') {
        this.value = value;
        this.format = format;
      }
    
      private changeFormatType(format: CellFormat): XLSX.ExcelDataType {
        switch (format) {
        case 'string':
          return 's';
        case 'number':
          return 'n';
        case 'boolean':
          return 'b';
        case 'date':
          return 'd';
        }
      }
    
      public toCellObject(): XLSX.CellObject {
        return {
          v: this.value,
          t: this.changeFormatType(this.format),
        };
      }
    }
     
     

    이와 같이 구조화한 클래스들을 활용하여 다음과 같이 적용할 수 있었습니다.

    // 1. acquire data
    const postsData = [
      [new ExcelCell("Title"), new ExcelCell("Author")],
      [new ExcelCell("Post 1"), new ExcelCell("John")],
      [new ExcelCell("Post 2"), new ExcelCell("Jane")],
    ];
    
    // 2. process and package data
    const excelFile = new ExcelFile("blog_posts", "xlsx");
    const postsSheet = new ExcelSheet("Posts", postsData);
    excelFile.appendExcelSheet(postsSheet);
    
    // 3. download file
    excelFile.downloadFile();
     
     

    마무리

    이렇게 클래스를 이용하여 코드를 구성하여 엑셀 다운로드 기능을 완성할 수 있었습니다. 감사합니다.

    '개발여정' 카테고리의 다른 글

    React Native로 모바일과 웹을 한번에 개발하기  (0) 2024.07.19
    Masonry Layout 구현 여정 🚀  (0) 2024.07.05
    OCR 구현 여정 🚀  (0) 2024.06.28

    댓글

Designed by Tistory.