안녕하세요, 자바파커입니다.
"관리자 페이지에서 엑셀 다운로드 좀 붙여주세요." "고객사에서 엑셀 업로드로 일괄 등록하고 싶다는데요…"
프론트엔드 개발을 오래 하신 분이라면 이 문장은 거의 분기별로 한 번씩 듣게 됩니다. 서버에서 CSV로 내려주면 한글이 깨지고, POI로 만들면 배포 부담이 커지고 — 결국 다시 브라우저에서 직접 엑셀을 만들자는 결론에 도달하죠.
결론부터 말씀드리면 — 단순 읽기/쓰기는 SheetJS(xlsx) CE면 충분하고, 셀 서식·컬럼 폭·이미지까지 건드려야 한다면 ExcelJS가 더 편합니다. 이 글에서는 두 라이브러리의 실전 패턴과 선택 기준을 2026년 시점에서 정리합니다.
왜 아직도 SheetJS 인가
2026년 현재 브라우저 기반 엑셀 라이브러리 생태계에서 사실상 표준은 SheetJS (xlsx) 입니다. 이유는 세 가지입니다.
- 포맷 커버리지가 압도적 — XLSX, XLSM, XLSB, XLS, ODS, CSV, HTML 등 대부분의 스프레드시트 포맷을 한 API로 처리합니다.
- 의존성이 없다 — 번들 크기는 작지 않지만(약 600KB gzipped), 런타임 의존이 0이라 Webpack/Vite/esbuild 어디에 넣어도 안전합니다.
- 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.tgzimport * as XLSX from "xlsx"ExcelJS (대안)
npm install exceljsimport 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 엑셀이라도 파싱 중에 탭이 멈추는 이유입니다.
실전 대응 전략:
- 10MB가 넘어가면 서버 파싱으로 우회. 업로드 후 백엔드(POI, openpyxl 등)에서 처리.
- CSV로 유도 — 사용자에게 템플릿을 CSV로 제공하면 파싱도 빠르고 메모리도 덜 씁니다.
- 스트리밍이 필요하면 Node.js 환경에서 ExcelJS의
stream.xlsx.WorkbookReader사용 (브라우저에선 불가). - 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로 충분합니다.
관련 포스팅
- Secure Context — HTTP vs HTTPS와 브라우저 API — 파일 다운로드 동작에 영향을 줄 수 있는 브라우저 보안 컨텍스트 이슈를 정리했습니다.
참고 자료
관리자 페이지에서 엑셀 다운로드를 구현해보신 분들, 어떤 라이브러리를 쓰고 계신가요? 저는 읽기·쓰기 비중이 7:3 정도라 여전히 SheetJS를 기본으로 쓰고, 리포트성 요구가 들어올 때만 ExcelJS로 갈아타는 편입니다. 여러분의 선택 기준이 궁금합니다. 댓글로 공유해주세요!