SheetJS 실전 가이드 — 브라우저에서 엑셀 읽고 쓰기 (xlsx vs ExcelJS 비교)

@JavaPark · November 09, 2023 · 13 min read

안녕하세요, 자바파커입니다.

"관리자 페이지에서 엑셀 다운로드 좀 붙여주세요." "고객사에서 엑셀 업로드로 일괄 등록하고 싶다는데요…"

프론트엔드 개발을 오래 하신 분이라면 이 문장은 거의 분기별로 한 번씩 듣게 됩니다. 서버에서 CSV로 내려주면 한글이 깨지고, POI로 만들면 배포 부담이 커지고 — 결국 다시 브라우저에서 직접 엑셀을 만들자는 결론에 도달하죠.

결론부터 말씀드리면 — 단순 읽기/쓰기는 SheetJS(xlsx) CE면 충분하고, 셀 서식·컬럼 폭·이미지까지 건드려야 한다면 ExcelJS가 더 편합니다. 이 글에서는 두 라이브러리의 실전 패턴과 선택 기준을 2026년 시점에서 정리합니다.

왜 아직도 SheetJS 인가

2026년 현재 브라우저 기반 엑셀 라이브러리 생태계에서 사실상 표준은 SheetJS (xlsx) 입니다. 이유는 세 가지입니다.

  1. 포맷 커버리지가 압도적 — XLSX, XLSM, XLSB, XLS, ODS, CSV, HTML 등 대부분의 스프레드시트 포맷을 한 API로 처리합니다.
  2. 의존성이 없다 — 번들 크기는 작지 않지만(약 600KB gzipped), 런타임 의존이 0이라 Webpack/Vite/esbuild 어디에 넣어도 안전합니다.
  3. CE 버전은 Apache 2.0 — 상업 프로젝트에서도 부담 없이 쓸 수 있습니다. Pro 버전은 스타일링/차트/서식 기능이 추가되지만, 대부분의 관리자 페이지는 CE로 충분합니다.

단, 한 가지 유의점이 있습니다. xlsx는 2023년부터 npm 공식 레지스트리에서 호스팅되지 않고 SheetJS 자체 CDN을 사용하도록 바뀌었습니다. 설치 방식이 예전 포스트들과 다르니 아래 최신 방식을 쓰세요.

설치 — 2026년 최신 방식

CDN (가장 빠른 시작)

<script src="https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js"></script>

예전 cdnjs 경로(cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.0/...)는 업데이트가 멈춰 있으므로 공식 CDN을 사용하는 것을 권장합니다.

npm

# SheetJS 공식 CDN 레지스트리를 통해 설치
npm install --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
import * as XLSX from "xlsx"

ExcelJS (대안)

npm install exceljs
import ExcelJS from "exceljs"

ExcelJS는 MIT 라이선스, 순수 JS, npm 공식 레지스트리에 정상 배포되어 있습니다.

읽기 — 업로드한 엑셀을 JSON으로

브라우저에서 사용자가 선택한 엑셀 파일을 JSON 배열로 바꾸는 가장 기본 패턴입니다. 예전에는 readAsBinaryString을 많이 썼지만 deprecated 되었고, 2026년 기준 권장은 readAsArrayBuffer 입니다.

<input type="file" id="file-input" accept=".xlsx,.xls,.csv" />
<pre id="output"></pre>
document.getElementById("file-input").addEventListener("change", handleFile)

function handleFile(event) {
  const file = event.target.files[0]
  if (!file) return

  const reader = new FileReader()
  reader.onload = e => {
    // ArrayBuffer → Uint8Array
    const data = new Uint8Array(e.target.result)
    const workbook = XLSX.read(data, { type: "array" })

    const sheetName = workbook.SheetNames[0]
    const sheet = workbook.Sheets[sheetName]

    // 헤더를 키로 쓰는 객체 배열
    const rows = XLSX.utils.sheet_to_json(sheet, { defval: "" })
    document.getElementById("output").textContent = JSON.stringify(
      rows,
      null,
      2
    )
  }
  reader.readAsArrayBuffer(file)
}

포인트

  • { defval: "" } 를 주면 빈 셀이 undefined가 아니라 빈 문자열로 들어와 서버 전송 시 편합니다.
  • 헤더를 직접 지정하고 싶다면 { header: ["id", "name", "email"] } 처럼 넘길 수 있습니다.
  • CSV는 한글 인코딩이 꼬이는 경우가 많은데, SheetJS는 BOM과 EUC-KR을 자동 감지하므로 대부분 그대로 읽힙니다.

쓰기 — JSON을 엑셀로 내려받기

반대 방향, 관리자 화면에서 조회한 데이터를 그대로 엑셀로 내려주는 패턴입니다.

function exportToExcel(rows, fileName = "export.xlsx") {
  // 1) 워크북 생성
  const workbook = XLSX.utils.book_new()

  // 2) JSON → 시트
  const sheet = XLSX.utils.json_to_sheet(rows)

  // 3) 시트 등록
  XLSX.utils.book_append_sheet(workbook, sheet, "Sheet1")

  // 4) 파일 다운로드 트리거
  XLSX.writeFile(workbook, fileName, { compression: true })
}

// 사용 예
exportToExcel([
  { id: 1, name: "홍길동", email: "hong@example.com" },
  { id: 2, name: "김자바", email: "java@example.com" },
])

compression: true를 주면 XLSX 내부 zip을 압축해 파일 크기가 30~60% 정도 줄어듭니다. 수천 행 이상 내려줄 땐 거의 필수입니다.

aoa_to_sheet — 표 레이아웃을 직접 구성

JSON이 아니라 "N행 M열"의 2차원 배열로 넘기면 헤더 병합·빈 행 삽입 같은 레이아웃을 자유롭게 짤 수 있습니다.

const aoa = [
  ["ID", "이름", "이메일"],
  [1, "홍길동", "hong@example.com"],
  [2, "김자바", "java@example.com"],
]
const sheet = XLSX.utils.aoa_to_sheet(aoa)

컬럼 폭·헤더 스타일 팁

CE 버전은 셀 서식(폰트, 색, 굵기)은 지원하지 않습니다. 하지만 컬럼 폭과 병합 정도는 CE에서도 가능합니다.

// 컬럼 폭(글자 수 기준)
sheet["!cols"] = [{ wch: 6 }, { wch: 14 }, { wch: 28 }]

// 헤더 행 고정(첫 행)
sheet["!freeze"] = { xSplit: 0, ySplit: 1 }

// 셀 병합: A1:C1
sheet["!merges"] = [{ s: { r: 0, c: 0 }, e: { r: 0, c: 2 } }]

헤더를 굵게/배경색으로 강조하고 싶다면 여기서부터는 ExcelJS 영역입니다.

ExcelJS 로 헤더 스타일까지

import ExcelJS from "exceljs"

async function exportStyled(rows) {
  const wb = new ExcelJS.Workbook()
  const ws = wb.addWorksheet("회원")

  ws.columns = [
    { header: "ID", key: "id", width: 6 },
    { header: "이름", key: "name", width: 14 },
    { header: "이메일", key: "email", width: 28 },
  ]
  ws.addRows(rows)

  // 헤더 스타일
  ws.getRow(1).eachCell(cell => {
    cell.font = { bold: true, color: { argb: "FFFFFFFF" } }
    cell.fill = {
      type: "pattern",
      pattern: "solid",
      fgColor: { argb: "FF2F5597" },
    }
    cell.alignment = { vertical: "middle", horizontal: "center" }
  })

  const buffer = await wb.xlsx.writeBuffer()
  const blob = new Blob([buffer], {
    type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  })
  const url = URL.createObjectURL(blob)
  const a = document.createElement("a")
  a.href = url
  a.download = "members.xlsx"
  a.click()
  URL.revokeObjectURL(url)
}

SheetJS vs ExcelJS — 2026 비교표

항목 SheetJS (xlsx) CE ExcelJS xlsx-populate
라이선스 Apache 2.0 MIT MIT
배포 공식 CDN (cdn.sheetjs.com) npm npm
지원 포맷 XLSX/XLSM/XLSB/XLS/ODS/CSV XLSX/CSV XLSX
읽기 속도 매우 빠름 보통 빠름
셀 서식(색/폰트) CE는 미지원 (Pro 필요) 완전 지원 부분 지원
차트 Pro만 미지원 미지원
번들 크기(gzip) 약 600KB 약 900KB 약 250KB
학습 곡선 낮음 (utils 중심) 중간 (객체 모델) 낮음
추천 상황 읽기·쓰기 위주 관리자 화면 스타일 지정 리포트 경량이 우선

한 문장 요약: "단순 업/다운로드는 SheetJS, 보기 좋은 리포트는 ExcelJS."

대용량 파일 주의 — 브라우저 메모리

FileReader로 읽을 수 있는 파일 크기는 이론적으로 수백 MB까지 가능하지만, XLSX.read가 파싱하는 순간 메모리 사용량이 파일 크기의 3~5배로 뛰는 경우가 많습니다. 50MB 엑셀이라도 파싱 중에 탭이 멈추는 이유입니다.

실전 대응 전략:

  1. 10MB가 넘어가면 서버 파싱으로 우회. 업로드 후 백엔드(POI, openpyxl 등)에서 처리.
  2. CSV로 유도 — 사용자에게 템플릿을 CSV로 제공하면 파싱도 빠르고 메모리도 덜 씁니다.
  3. 스트리밍이 필요하면 Node.js 환경에서 ExcelJS의 stream.xlsx.WorkbookReader 사용 (브라우저에선 불가).
  4. Web Worker에서 파싱 — 메인 스레드가 먹통이 되는 건 막을 수 있습니다.
// worker.js
importScripts(
  "https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js"
)
self.onmessage = e => {
  const wb = XLSX.read(e.data, { type: "array" })
  const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]])
  self.postMessage(rows)
}

React 에서 쓰기

import * as XLSX from "xlsx"

export function ExcelExportButton({ rows }) {
  const onClick = () => {
    const ws = XLSX.utils.json_to_sheet(rows)
    ws["!cols"] = [{ wch: 6 }, { wch: 14 }, { wch: 28 }]
    const wb = XLSX.utils.book_new()
    XLSX.utils.book_append_sheet(wb, ws, "목록")
    XLSX.writeFile(wb, `export-${Date.now()}.xlsx`, { compression: true })
  }
  return <button onClick={onClick}>엑셀 다운로드</button>
}

Vue 3 에서 쓰기

<script setup>
import * as XLSX from "xlsx"

const props = defineProps({ rows: Array })

function download() {
  const ws = XLSX.utils.json_to_sheet(props.rows)
  const wb = XLSX.utils.book_new()
  XLSX.utils.book_append_sheet(wb, ws, "목록")
  XLSX.writeFile(wb, "export.xlsx", { compression: true })
}
</script>

<template>
  <button @click="download">엑셀 다운로드</button>
</template>

FAQ

Q1. 한글이 깨집니다. 어떻게 해야 하나요?

대부분 CSV로 다운로드할 때입니다. Excel이 UTF-8 CSV를 열 때 BOM이 없으면 EUC-KR로 추정해서 깨지는 현상인데, SheetJS는 CSV 쓰기 시 자동으로 BOM을 붙여줍니다. 직접 문자열로 CSV를 만드는 경우라면 앞에 \uFEFF를 붙여주세요. XLSX 포맷은 내부적으로 UTF-8이 강제라 한글이 깨질 일이 사실상 없습니다.

Q2. 수십 MB짜리 엑셀을 꼭 브라우저에서 처리해야 한다면?

솔직히 말씀드리면 권장하지 않습니다. 그래도 해야 한다면 (1) Web Worker로 분리, (2) sheet_to_json 대신 sheet_to_csv로 스트리밍 파싱, (3) 여러 시트로 분할된 파일이라면 필요한 시트만 sheetRows 옵션으로 제한해서 읽기 — 이 세 가지를 조합하세요.

Q3. 셀 색/폰트를 꼭 넣어야 한다면 Pro를 사야 하나요?

아니요. ExcelJS(MIT)로 공짜로 가능합니다. SheetJS Pro는 차트·피벗·수식 서식 등 고급 기능이 필요한 B2B SaaS에서나 가치가 있고, 일반적인 관리자 페이지 리포트는 ExcelJS로 충분합니다.

관련 포스팅

참고 자료


관리자 페이지에서 엑셀 다운로드를 구현해보신 분들, 어떤 라이브러리를 쓰고 계신가요? 저는 읽기·쓰기 비중이 7:3 정도라 여전히 SheetJS를 기본으로 쓰고, 리포트성 요구가 들어올 때만 ExcelJS로 갈아타는 편입니다. 여러분의 선택 기준이 궁금합니다. 댓글로 공유해주세요!

@JavaPark
AI 시대의 개발자 도구, 실전 경험을 공유합니다