본문 바로가기
개발/자바스크립트

[자바스크립트] image, data를 Excel로 내보내기 ( feat.exceljs )

by 핸디(Handy) 2021. 3. 9.

들어가며

시각화 대시보드를 만드는 팀에 속해있는데, 새로운 기능 개발 요건이 들어왔습니다.

요구된 기능은 아래와 같습니다.

기능 목표 : 대시보드에 있는 차트와 테이블을 Excel 파일로 떨궈주세요.
세부 목표 : 차트와 테이블은 이미지로 넣되, 테이블의 경우 전체 데이터를 보여주세요

이번 글에서는 해당 기능을 위한 간단한 튜토리얼 겸 기능 검토에 대한 이야기입니다.

기능 목표 및 검토

Excel 라이브러리 확인

대시보드에서 차트와 데이블을 Excel로 떨구는 작업이 필요하다고 되어있습니다.

바로 인터넷을 찾아보니 필요한 기능들을 제공하는 라이브러리를 찾아봅니다.

npm trend 기준

이름 지원여부 주소
excellentexport xlsx, xlx, csv 가능
이미지 안됨
https://github.com/jmaister/excellentexport
sheetJS xlsx, xlx, csv 가능
이미지 됨 but Pro버젼
github.com/SheetJS/sheetjs
exceljs xlsx, xlx, csv 가능
이미지 됨 
github.com/exceljs/exceljs
직접하기 다됨, 공수가 많이듬 my brain

찾아보니 다행스럽게도 다양한 라이브러리들이 존재했고, 그중에 excel.js로 기능을 구현하고자 결정했습니다.

표를 보시면 정리되어있듯이 원하는 기능을 충족하고 있었고 일단 다운로드가 맞으면 신뢰가 갑니다.

excel.js 테스트

기능 검토를 위한 로직은 다음과 같습니다.

  1. querySelectorAll로 차트가 그려진 div 찾기
  2. html2canvas 라이브러리를 이용하여 이미지로 뽑아오기
  3. buffer로 보내 Blob 파일 형식으로 만들어서 다운로드하기

그래서 사용한 라이브러리를 보시면 총 3개입니다(exceljs, html2canvas, plotlyjs)

  • excel.js : excel로 이미지와 데이터를 내보낼 라이브러리
  • html2canvas.js : excel로 내보낼 이미지를 만드는 라이브러리
  • plotly.js : 내보낼 차트를 만드는 라이브러리
import exceljs from "exceljs";

function downloadWorkbook() {
  // excel 파일 생성
  let workbook = new exceljs.Workbook();
 
  //차트 시트탭 2개를 만듬
  const imageSheet = workbook.addWorksheet("ImageSheet");
  const dataSheet = workbook.addWorksheet("DataSheet");

  //테이블의 경우, 데이터를 넣어줌
  rawDate.forEach((item, index) => {
    dataSheet.getColumn(index + 1).values = [item.header, ...item.data];
  });

  // 차트 라이브러리의 div를 찾아와 이미지로 변환
  let promise = [];
  document.querySelectorAll(".plot-container").forEach((item, index) => {
    promise.push(
      html2canvas(item).then((c) => {
        let image = c.toDataURL();
        const imageId2 = workbook.addImage({
          base64: image,
          extension: "png",
        });

        imageSheet.addImage(imageId2, position[index]);
      })
    );
  });

  // excel로 만드는건 비동기처리로
  Promise.all(promise).then(() => {
    workbook.xlsx.writeBuffer().then((b) => {
      let a = new Blob([b]);
      let url = window.URL.createObjectURL(a);

      let elem = document.createElement("a");
      elem.href = url;
      elem.download = `${new Date().toString().replaceAll(" ", "")}.xlsx`;
      document.body.appendChild(elem);
      elem.style = "display: none";
      elem.click();
      elem.remove();
    });
  });

  return workbook;
}

//테스트할 차트 3개 추가
createChart("chart01");
createChart("chart02");
createChart("chart03");
function createChart(id) {
  let elem = document.getElementById(id);
  if (elem) {
    Plotly.newPlot(
      elem,
      [
        {
          x: [1, 2, 3, 4],
          y: [10, 15, 13, 17],
          type: "scatter",
        },
        {
          x: [1, 2, 3, 4],
          y: [16, 5, 11, 9],
          type: "scatter",
        },
      ],
      { header: id }
    );
  }
}

// Excel에서 이미지의 위치
let position = [
  {
    tl: { col: 1.5, row: 1.5 }, // top, left
    br: { col: 3, row: 3 }, // bottom, right
  },
  {
    tl: { col: 3, row: 3 },
    br: { col: 6, row: 6 },
  },
  {
    tl: { col: 6, row: 6 },
    br: { col: 9, row: 9 },
  },
];

// 테이블의 테스트 데이터
let rawDate = [
  { header: "시도", data: ["서울", "서울", "서울", "서울", "서울", "서울", "서울", "서울", "서울", "서울"] },
  { header: "시군구", data: ["송파구", "송파구", "송파구", "송파구", "송파구", "송파구", "송파구", "서초구", "서초구", "서초구"] },
  { header: "사고유형", data: ["측면충돌", "추돌", "기타", "전도전복", "공작물충돌", "주/정차차량 충돌", "기타", "횡단중", "차도통행중", "길가장자리구역통행중"] },
];

// 클릭시 이벤트 코드
if (document.getElementById("download")) document.getElementById("download").addEventListener("click", downloadWorkbook);

결과 확인

원하는 위치에 이미지와 데이터가 들어간 것을 확인할 수 있습니다.

결과화면

로직 분리하기

위의 코드의 경우 차트 그리는 것부터 엑셀 다운로드까지 한번에 있는 코드라 실제 프로덕션에서 쓰기엔 불편합니다.

그래서 저는 아래와 같이 util 함수로 만들어서 사용하고 있는데요.

import * as ExcelJS from "exceljs";

interface downloadDomDataProp {
  fileName: string;
  excelInfoList: {
    sheetName?: string;
    excelData?: ExcelData[];
  }[];
}

export interface ExcelData {
  header: string;
  data: string[] | number[];
}

const downloadDomData = ({ fileName, excelInfoList }: downloadDomDataProp) => {
  let workbook = new ExcelJS.Workbook();

  // excel 파일 생성후 데이터 씀
  excelInfoList.forEach((excelData, index) => {
    const sheetName = excelData.sheetName || `DataSheet_${index + 1}`;
    const dataSheet = workbook.addWorksheet(sheetName);
    excelData.excelData?.forEach((item, dataIndex) => {
      dataSheet.getColumn(dataIndex + 1).values = [item.header, ...item.data];
    });
  });

  // 다운로드 링크 생성후 다운로드 실행
  workbook.xlsx.writeBuffer().then((b) => {
    let a = new Blob([b]);
    let url = window.URL.createObjectURL(a);
    let elem = document.createElement("a");
    elem.href = url;
    elem.download = `${fileName}.xlsx`;
    document.body.appendChild(elem);
    elem.click();
    elem.remove();
  });

  return workbook;
};
export default downloadDomData;

파일명과 시트당 들어가야할 데이터셋을 보내주면 링크를 생성하고 클릭하여 다운로드를 해주는 로직입니다.

excel 다운로드 함수 호출 예시

그리고 유틸함수를 이용하여 종속없이 원하는 데이터를 조립해서 다운로드할수 있도록 제공하고 있습니다.

마치며

생각보다 간단한 기능이어서 금방 구현을 마무리되었습니다.

추가적으로 excel로 만드는 프로세스가 리소스를 많이 잡아먹는다면 해당 기능을 웹 워커로 돌려도 될 듯합니다. 다만 차트를 이미지로 만드는 작업은 dom를 직접 건드리는 것이라 웹 워커로는 못 돌리니 코드를 분리를 하긴 해야 하겠네요.

또한 리액트에서도 해당 기능을 사용하려면 똑같이 querySelector로 영역을 잡아채던지 ref로 해당 영역을 넘겨주면 동작합니다.

혹여나 전체 코드가 필요하신 분이 계실지 몰라 코드 주소도 올려놓겠습니다.

<전체 코드>
github.com/gyeongseokKang/exportExcel_javascript

댓글