최근 지인에게 데이터 분석을 위한 데이터 시각화를 해주는 웹 사이트 요청을 받았습니다. 엑셀 파일을 올리면 엑셀 파일을 분석해서 데이터 시각화를 해주고, 특정 데이터들을 강조하여 보여줄 수 있는 웹사이트가 필요하다는 것.
관련해서 D3.js를 이용해 데이터를 시각화해 본 적은 있지만 엑셀 파일을 올리고, 해당 파일을 분석하는 것은 해본 적이 없어서 간단하게나마 정리해보려고 합니다.
FileReader API는 웹 브라우저에서 제공하는 API 중 하나로, 사용자가 웹 사이트에 업로드 한 파일을 웹 애플리케이션에서 비동기적으로 읽기 위한 목적으로 사용됩니다. 파일을 읽기 위한 여러 메서드와 이벤트 핸들러를 제공하며, 다양한 데이터 형식으로 파일의 내용을 읽어올 수 있습니다.
readAsArrayBuffer(file)
, readAsText(file, [encoding])
, readAsDataURL(file)
같은 메서드들을 이용하여 파일을 읽어옵니다. 각각은 파일을 다른 형식으로 읽어오는데 사용되며, 이진 데이터 처리부터 텍스트 형식의 파일까지 다양한 포맷을 다룰 수 있습니다.
읽어온 데이터들을 바탕으로 onload
, onerror
, onloadend
이벤트를 이용하여 파일 읽기 작업이 성공하거나 오류가 발생했을 때 이벤트가 트리거시킬 수 있고, 각각의 핸들러를 통해 데이터에 접근하거나 오류 정보를 확인할 수 있습니다.
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;
위와 같은 hello.txt 파일을 저장한 다음, 업로드 해보면 아래와 같이 정상적으로 잘 출력되는 것을 확인할 수 있습니다.
위 내용을 바탕으로 xlsx 파일도 한 번 읽어보겠습니다. 데이터는 임의로 period
, value
두 개의 행을 갖는 엑셀 파일을 준비했습니다.
readAsText(File)
위와 똑같이 파일을 업로드 해보면 아래와 같이 글씨가 다 깨져버립니다.
readAsText
메서드를 사용해서 그런 걸까요?? 아까 주요 메서드들 중 readAsArrayBuffer(File)
메서드가 있었던 것 같은데, readAsArrayBuffer로 바꿔놓고 다시 파일을 업로드해보겠습니다.
readAsArrayBuffer(File)
이번엔 무슨 데이터인지 알아볼 수 없는 이진 데이터들로 이루어져 있습니다. Int8Array
라고 적혀있는 배열은 9487 length를 갖는 배열인데, 무엇을 의미하는 건지 알아내기 어렵습니다. 왜 이런걸까요??
지금까지의 기능을 이용해서 파일을 읽어올 수 있습니다만, .xlsx
형식을 갖는 엑셀 파일을 읽어오기 위해선 FileReader API 만으로는 부족합니다. 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 이벤트 핸들러가 호출됩니다.결과를 보면 무엇을 의미하는지 파악할 수 있는 배열 형태로 데이터를 읽어들이는 모습입니다.
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;
최근에는 회사에서 비교적 간단한 작업들을 반복적으로 수행하게 되면서, 업무량 자체는 많아지지만 동시에 시간은 점점 부족해지는 상황을 경험했습니다. 서비스 구현 자체는 어렵지 않기에 어떤 작업을 해야 성장할 수 있을지, 회사 업무 외적으로 시도해볼 수 있는 일들이 있는지 궁금해지기 시작했습니다. 그래서 이러한 간단한 일들이라도 처음 시도해보는 것들에 대해서는 조금이나마 정리하고 이해하려는 노력이 필요하다는 것을 조금씩 느끼고 있는 것 같습니다.
사소한 작업이라 할지라도, 그것들을 하나씩 수행하면서 생기는 궁금증을 해결하고, 새롭게 알게 된 사실들을 정리하는 과정은 조금 느리더라도 더 오래 기억할 수 있게 해주는 것 같습니다. 조금씩이지만 앞으로 어떤 것들을 배웠는지 정리하면서 점점 더 성장할 수 있을거라 기대합니다.