FileReader API로 엑셀 파일 읽기

배준형·2023년 12월 5일
0

서문

최근 지인에게 데이터 분석을 위한 데이터 시각화를 해주는 웹 사이트 요청을 받았습니다. 엑셀 파일을 올리면 엑셀 파일을 분석해서 데이터 시각화를 해주고, 특정 데이터들을 강조하여 보여줄 수 있는 웹사이트가 필요하다는 것.

관련해서 D3.js를 이용해 데이터를 시각화해 본 적은 있지만 엑셀 파일을 올리고, 해당 파일을 분석하는 것은 해본 적이 없어서 간단하게나마 정리해보려고 합니다.


FileReader API

FileReader API는 웹 브라우저에서 제공하는 API 중 하나로, 사용자가 웹 사이트에 업로드 한 파일을 웹 애플리케이션에서 비동기적으로 읽기 위한 목적으로 사용됩니다. 파일을 읽기 위한 여러 메서드와 이벤트 핸들러를 제공하며, 다양한 데이터 형식으로 파일의 내용을 읽어올 수 있습니다.

readAsArrayBuffer(file), readAsText(file, [encoding]), readAsDataURL(file) 같은 메서드들을 이용하여 파일을 읽어옵니다. 각각은 파일을 다른 형식으로 읽어오는데 사용되며, 이진 데이터 처리부터 텍스트 형식의 파일까지 다양한 포맷을 다룰 수 있습니다.

읽어온 데이터들을 바탕으로 onload, onerror, onloadend 이벤트를 이용하여 파일 읽기 작업이 성공하거나 오류가 발생했을 때 이벤트가 트리거시킬 수 있고, 각각의 핸들러를 통해 데이터에 접근하거나 오류 정보를 확인할 수 있습니다.


간단한 React 코드 FileReader API 예제

util.ts

export const readFile = (file: File) => {
  const reader = new FileReader();
  reader.readAsText(file);
  reader.onload = (e) => {
    if (!e.target) return;
    const content = e.target.result;
    console.log(content);
  };
};

FileInput.tsx

import { useState } from "react";
import { readFile } from "../../utils/fileReader";

const FileInput = () => {
  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const files = event.target.files;
    if (files) {
      readFile(files[0]);
    }
  };

  return (
    <div>
      <input type="file" onChange={handleFileChange} />
      {file && <p>{file.name}</p>}
    </div>
  );
};

export default FileInput;
  • file type의 input 태그 onChange 이벤트를 통해 file을 받아오면, FileReader API의 readAsText로 파일의 text를 읽어옵니다.

위와 같은 hello.txt 파일을 저장한 다음, 업로드 해보면 아래와 같이 정상적으로 잘 출력되는 것을 확인할 수 있습니다.


.xlsx 파일 읽어보기

위 내용을 바탕으로 xlsx 파일도 한 번 읽어보겠습니다. 데이터는 임의로 period, value 두 개의 행을 갖는 엑셀 파일을 준비했습니다.


.xlsx 파일 읽기 - readAsText(File)

위와 똑같이 파일을 업로드 해보면 아래와 같이 글씨가 다 깨져버립니다.

readAsText 메서드를 사용해서 그런 걸까요?? 아까 주요 메서드들 중 readAsArrayBuffer(File)

메서드가 있었던 것 같은데, readAsArrayBuffer로 바꿔놓고 다시 파일을 업로드해보겠습니다.


.xlsx 파일 읽기 - readAsArrayBuffer(File)

이번엔 무슨 데이터인지 알아볼 수 없는 이진 데이터들로 이루어져 있습니다. Int8Array 라고 적혀있는 배열은 9487 length를 갖는 배열인데, 무엇을 의미하는 건지 알아내기 어렵습니다. 왜 이런걸까요??

지금까지의 기능을 이용해서 파일을 읽어올 수 있습니다만, .xlsx 형식을 갖는 엑셀 파일을 읽어오기 위해선 FileReader API 만으로는 부족합니다. xlsx 파일 포맷은 매우 복잡한 이진 포맷으로, 이를 직접 파싱하는 것은 매우 복잡한 작업입니다. 그래서 주로 라이브러리를 사용하는데, 잘 알려진 라이브러리로 xlsx 라이브러리가 있습니다.


xlsx 라이브러리를 이용한 xlsx 파일 읽어들이기

위의 readFile 함수를 xlsx 라이브러리를 사용하는 것으로 바꿔서 확인해봅시다.

util.ts

import * as XLSX from "xlsx";

export const readFile = (file: File) => {
  const reader = new FileReader();
  reader.onload = (e) => {
    if (!e.target?.result) return;

    const data = new Uint8Array(e.target.result as ArrayBuffer);
    const workbook = XLSX.read(data, { type: "array" });

    const firstWorksheet = workbook.Sheets[workbook.SheetNames[0]];
    const jsonData = XLSX.utils.sheet_to_json(firstWorksheet, { header: 1 });

    console.log(jsonData);
  };
  reader.readAsArrayBuffer(file);
};
  • const data = new Uint8Array(e.target.result as ArrayBuffer);: 읽어온 파일의 내용을 Uint8Array 형식으로 변환합니다. 해당 형식은 xlsx 라이브러리가 엑셀 파일을 처리하기 위해 필요한 데이터 형식입니다.
  • const workbook = XLSX.read(data, { type: "array" });: xlsx 라이브러리의 read 함수를 사용해서 Uint8Array 형식의 데이터를 워크북 객체로 변환합니다.
  • const firstWorksheet = workbook.Sheets[workbook.SheetNames[0]];: 워크북 객체에서 첫 번째 워크시트를 가져옵니다.
  • const jsonData = XLSX.utils.sheet_to_json(firstWorksheet, { header: 1 });: 첫 번째 워크시트를 JSON 형태로 변환합니다. { header: 1 } 옵션을 사용해서 각 행을 배열로 변환합니다.
  • reader.readAsArrayBuffer(file);: reader 객체를 사용해서 파일을 ArrayBuffer 형식으로 읽습니다. 이 작업이 성공적으로 완료되면 위에서 설정한 onload 이벤트 핸들러가 호출됩니다.

결과를 보면 무엇을 의미하는지 파악할 수 있는 배열 형태로 데이터를 읽어들이는 모습입니다.

Drag Event를 추가하여 마우스로 파일 업로드하기

import { useRef, useState } from "react";
import { readFile } from "../../utils/fileReader";

const FileInput = () => {
  const [isDragOver, setIsDragOver] = useState(false);
  const [file, setFile] = useState<File | null>(null);
  const inputRef = useRef<HTMLInputElement>(null);

  const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
    e.preventDefault();
    setIsDragOver(true);
  };

  const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
    e.preventDefault();
    setIsDragOver(false);
    const files = e.dataTransfer.files;
    if (files) {
      setFile(files[0]);
      readFile(files[0]);
    }
  };

  const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
    e.preventDefault();
    setIsDragOver(false);
  };

  const handleClick = () => {
    inputRef.current?.click();
  };

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = e.target.files;
    if (files) {
      setFile(files[0]);
      readFile(files[0]);
    }
  };

  return (
    <>
      <div
        className={`flex h-72 w-72 items-center justify-center bg-slate-400 text-2xl font-bold text-white
          ${
            isDragOver && "border-4 border-dashed border-slate-700 bg-slate-600"
          }
        `}
        onDragOver={handleDragOver}
        onDrop={handleDrop}
        onDragLeave={handleDragLeave}
        onClick={handleClick}
      >
        여기에 파일을 놓으세요
      </div>
      <input
        type="file"
        className="hidden"
        accept=".xlsx, .xls, .csv"
        ref={inputRef}
        onChange={handleFileChange}
      />
      {file && <p>{file.name}</p>}
    </>
  );
};

export default FileInput;
  • input 태그는 숨겨놓고, div 태그를 클릭했을 때 ref를 통해 click 되도록 해줍니다.
  • dragEvent를 바인딩하여 onDrop 이벤트에 file을 받아옵니다.

생각 정리

최근에는 회사에서 비교적 간단한 작업들을 반복적으로 수행하게 되면서, 업무량 자체는 많아지지만 동시에 시간은 점점 부족해지는 상황을 경험했습니다. 서비스 구현 자체는 어렵지 않기에 어떤 작업을 해야 성장할 수 있을지, 회사 업무 외적으로 시도해볼 수 있는 일들이 있는지 궁금해지기 시작했습니다. 그래서 이러한 간단한 일들이라도 처음 시도해보는 것들에 대해서는 조금이나마 정리하고 이해하려는 노력이 필요하다는 것을 조금씩 느끼고 있는 것 같습니다.

사소한 작업이라 할지라도, 그것들을 하나씩 수행하면서 생기는 궁금증을 해결하고, 새롭게 알게 된 사실들을 정리하는 과정은 조금 느리더라도 더 오래 기억할 수 있게 해주는 것 같습니다. 조금씩이지만 앞으로 어떤 것들을 배웠는지 정리하면서 점점 더 성장할 수 있을거라 기대합니다.

profile
프론트엔드 개발자 배준형입니다.

0개의 댓글