@plott/webkit-tabular0.4.0
표 데이터 내보내기·가져오기 — 열 정의 하나로 CSV 쓰기(수식 주입 차단·BOM)·CSV 읽기·엑셀 셀 값 정리(UTC 날짜·앞자리 0 보존)·행별 오류 보고. 의존성 없음(exceljs 는 구조 타입으로만 맞춘다)
설치
pnpm add @plott/webkit-tabular@0.4.0진입점(exports)
| import | 타입 | 파일 |
|---|---|---|
@plott/webkit-tabular | ./dist/index.d.ts | ./dist/index.js |
표 데이터를 CSV·엑셀로 내보내고 가져오는 도구입니다. 읽는 사람은 목록 내보내기나 일괄 등록 화면을 만드는 프론트엔드 개발자입니다.
열 정의(column definition) 하나가 CSV 내보내기, CSV·엑셀 가져오기, 엑셀 내보내기를 모두 이끕니다. 날짜 계산은 @plott/webkit-time 의 DateOnly(YYYY-MM-DD)를 씁니다. 그 밖의 런타임 의존성은 없습니다. 엑셀 파일은 exceljs 로 읽고 쓰되, 이 패키지는 exceljs 를 import 하지 않고 구조 타입(structural type)으로만 맞춥니다. 앱이 쓰는 exceljs 버전과 엮이지 않습니다.
진입점
| import | 어디서 | 주요 export |
|---|---|---|
@plott/webkit-tabular | 서버·브라우저 어디서나 | 열 정의 defineColumns·normalizeHeader, CSV toCsv·parseCsv·CsvError, 가져오기 readRows, 엑셀 readWorksheet·ExcelLimitError·writeWorksheet·cellValue·excelSerialToDateOnly·excelDateToDateOnly |
붙이기
열 정의
import { defineColumns } from "@plott/webkit-tabular";import type { DateOnly } from "@plott/webkit-time";
export type Guest = { name: string; phone: string; age: number | null; checkIn: DateOnly | null; vip: boolean };
export const guestColumns = defineColumns<Guest>([ { key: "name", header: "이름", type: "text", required: true }, { key: "phone", header: "전화번호", type: "text", aliases: ["연락처"] }, { key: "age", header: "나이", type: "integer" }, { key: "checkIn", header: "체크인", type: "date" }, { key: "vip", header: "VIP", type: "boolean" },]);| 필드 | 뜻 |
|---|---|
key | 행 객체의 키. 비어 있거나 다른 열과 겹치거나 __proto__ 이면 defineColumns 가 던집니다 |
header | 헤더 글자 |
type | text·number·integer·date·boolean |
required | 가져올 때 빈 칸이면 required 오류 |
aliases | 헤더 대신 받아주는 다른 이름(옛 이름 등). header 와 정규화 뒤 겹치면 defineColumns 가 던집니다 |
get | 값을 꺼내는 방법. 기본은 row[key] |
format | 내보낼 값을 직접 정합니다. 결과는 열 type 에 맞게 씁니다 |
parse | 가져올 때 내장 변환 대신 씁니다. 던지면 그 칸은 type 오류가 됩니다 |
width | 엑셀 열 너비. 기본은 헤더의 글자 너비(한글·한자·전각·이모지는 2칸, 나머지 1칸)에 2를 더한 값이고 최소 10 입니다 |
dateFormat | 엑셀로 내보낼 때 date 열의 서식. 기본 yyyy-mm-dd |
CSV 내보내기·가져오기
import { parseCsv, readRows, toCsv, type Columns } from "@plott/webkit-tabular";import type { DateOnly } from "@plott/webkit-time";
type Guest = { name: string; phone: string; age: number | null; checkIn: DateOnly | null; vip: boolean };
declare const guestColumns: Columns<Guest>;declare const guests: Guest[];declare const text: string;
export function download() { const csv = toCsv(guests, guestColumns); return new Response(csv, { headers: { "content-type": "text/csv; charset=utf-8" } });}
export const { rows, errors, missingHeaders, unknownHeaders } = readRows<Guest>(parseCsv(text), guestColumns);toCsv 는 BOM 을 포함한 UTF-8 문자열을 돌려줍니다.
toCsv(rows, columns, opts)
| 옵션 | 기본값 | 뜻 |
|---|---|---|
bom | true | 맨 앞에 UTF-8 BOM |
eol | "\r\n" | 줄 끝 |
delimiter | "," | 칸 구분자(한 글자, 따옴표·개행 제외) |
header | true | 헤더 줄을 쓸지 |
parseCsv(text, { delimiter, skipEmptyLines }) 는 string[][] 를 돌려줍니다. 따옴표 칸과 "", CRLF·LF·CR, 맨 앞 BOM, 빈 줄을 다룹니다.
| 옵션 | 기본값 | 뜻 |
|---|---|---|
delimiter | "," | 칸 구분자 |
skipEmptyLines | true | 빈 줄(글자·구분자·따옴표가 없는 줄)을 어디서든 건너뜁니다. false 면 사이의 빈 줄을 [""] 로 남깁니다. 끝에 이어진 빈 줄은 false 에서도 모두 버립니다. "" 만 있는 줄은 빈 줄이 아니라 한 칸짜리 행입니다 |
엑셀 내보내기·가져오기
import ExcelJS from "exceljs";import { readRows, readWorksheet, writeWorksheet } from "@plott/webkit-tabular";
const wb = new ExcelJS.Workbook();writeWorksheet(wb.addWorksheet("고객"), guests, guestColumns);const buffer = await wb.xlsx.writeBuffer();
const book = new ExcelJS.Workbook();await book.xlsx.load(file);const ws = book.worksheets[0]!;const result = readRows<Guest>(readWorksheet(ws, { workbook: book }), guestColumns);앞의 세 줄(writeWorksheet, writeBuffer)이 내보내기이고, book.xlsx.load 부터가 가져오기입니다.
readWorksheet(ws, opts) 옵션
| 옵션 | 기본값 | 뜻 |
|---|---|---|
workbook | 없음 | 통합 문서. properties.date1904 를 결과 행렬에 실어 두면 readRows 가 알아서 씁니다(직접 date1904 를 넘기면 그 값이 먼저) |
date1904 | 없음 | workbook 대신 직접 정합니다 |
maxRows | 100000 | 행 수 상한 |
maxCols | 200 | 열 수 상한 |
maxCells | 2000000 | 행 × 열 상한 |
한도를 넘으면 반복을 시작하기 전에 ExcelLimitError(limit·actual·max)로 던집니다. 올린 파일의 사용 범위 rowCount·columnCount 는 먼 구석 한 칸(XFD1048576)만 채워도 커져서, 그대로 돌면 서버가 수백억 번 칸을 읽습니다. 한도를 늘리려면 옵션을 올리세요(끄려면 Infinity).
cellValue(v) 는 exceljs 칸 값의 여러 모양(서식 있는 글 richText, 수식, 링크, 오류)을 평범한 값으로 바꿉니다. readWorksheet 가 칸마다 부르므로 보통 직접 쓸 일은 없습니다. 일련번호를 직접 바꿔야 하면 excelSerialToDateOnly(serial, { date1904 }), Date 는 excelDateToDateOnly(date) 입니다.
가져오기 결과
readRows(matrix, columns, opts) 는 행렬을 열 정의대로 객체 행으로 바꿉니다.
| 옵션 | 기본값 | 뜻 |
|---|---|---|
headerRow | 0 | 헤더가 있는 줄(행렬 인덱스) |
match | "header" | header: 헤더 글자로 열을 찾습니다. index: 열 정의 순서가 곧 시트 열 순서(헤더 줄은 건너뜁니다) |
date1904 | readWorksheet 가 실어 둔 값, 없으면 false | 엑셀 일련번호를 1904 체계로 읽습니다 |
locale | 없음(천 단위 ,, 소수점 .) | 글자로 온 수의 천 단위·소수점 기호를 이 로케일(de 는 1.234,5)에서 가져옵니다. 틀린 로케일은 RangeError |
dateFormats | ["ymd"] | 글자로 온 날짜의 순서를 앞에서부터 시도합니다. ymd(2026/11/03), dmy(03/11/2026), mdy(11/03/2026). 모르는 이름은 RangeError |
booleanWords | { true: ["true","y","1"], false: ["false","n","0"] } | 불리언으로 읽는 낱말(대소문자 무시). 주면 기본 낱말을 대신하고, 두 쪽에 같은 낱말이 있으면 RangeError |
| 결과 | 뜻 |
|---|---|
rows | 오류가 없는 행만. 선택 열의 빈 칸은 null |
errors | 행별 오류 { row, key, header, code, value }. row 는 사용자가 시트에서 보는 번호(1부터, 헤더 줄 포함), code 는 required(빈 칸) 또는 type(변환 실패) |
missingHeaders | 시트에서 못 찾은 필수 열의 헤더 |
unknownHeaders | 어느 열 정의와도 맞지 않은 시트 헤더 |
필수 헤더가 시트에 없으면 missingHeaders 에 담기고, 비어 있지 않은 모든 데이터 행에 그 열의 required 오류가 붙어 rows 는 비게 됩니다. 화면에는 missingHeaders 를 먼저 보여주세요(«행 3: 이름 없음» 수백 줄보다 «이름 열을 찾지 못했습니다» 한 줄이 낫습니다).
값 변환은 열 type 별로 다음과 같습니다.
| type | 받는 값 |
|---|---|
text | 문자열(앞뒤 공백 제거). 숫자는 지수 표기·.0 없이 글자로. 날짜 칸(Date)은 YYYY-MM-DD |
number | 유한한 숫자, -12.5 같은 평범한 숫자 문자열, 1,234.5 처럼 3자리씩 맞는 천 단위 기호(locale 로 바꿈). 1,23 은 거절 |
integer | number 와 같고 정수만 |
date | YYYY-MM-DD, YYYY/MM/DD, YYYY.MM.DD(월·일 0 채움은 선택. 순서는 dateFormats), Date(UTC 성분), 엑셀 일련번호. 없는 날짜는 거절 |
boolean | TRUE·FALSE·Y·N·1·0(대소문자 무시. 낱말은 booleanWords), 불리언 |
기본으로 지키는 것
- 수식 주입 차단: CSV 에서
=+-@탭 CR LF 와 전각=+-@로 시작하는 글자 칸은(앞쪽 공백이 있어도) 앞에'를 붙입니다. 엑셀이 그 칸을 수식으로 실행하기 때문입니다. 숫자 열의 숫자(음수 포함)는 그대로 쓰고, 글자 열의-1은 글자이므로 붙입니다.|(DDE)는 따로 막지 않습니다. 수식이 되려면=로 시작해야 하고 그건 이미 막힙니다. 엑셀 파일의 문자열 칸은 수식이 아니므로 붙이지 않습니다. - 수식 방지
'는 왕복해도 같은 값: 이미'로 시작하는 글자'=x는''=x로 씁니다. 가져올 때 글자 열은'를 정확히 하나만 벗기므로=x·'=x·''=x·'plain이 모두 내보낸 값 그대로 돌아옵니다(앞뒤 공백 제거는 수식 시작이 아닌 칸에 그대로 적용됩니다). - 빈 줄:
parseCsv는 기본으로 빈 줄을 어디서든 건너뜁니다(끝에 이어진 빈 줄은skipEmptyLines: false에서도 버립니다). - 시트 크기 한도:
readWorksheet는maxRows·maxCols·maxCells를 넘는 시트를 읽기 전에ExcelLimitError로 거절합니다. - CSV 따옴표: 구분자·따옴표·CR·LF 가 든 칸은 따옴표로 감싸고 따옴표는 두 번 씁니다(RFC 4180).
- BOM:
toCsv는 UTF-8 BOM 을 붙입니다. 없으면 윈도 엑셀이 한글을 CP949 로 읽어 깨뜨립니다.parseCsv는 맨 앞 BOM 을 벗깁니다. - 읽을 수 없는 CSV 는 오류: 닫히지 않은 따옴표는 «행 없음» 이 아니라 시작한 줄 번호가 담긴
CsvError로 던집니다. - 날짜는 UTC 로: 엑셀의
Date칸은 UTC 성분으로 읽고, 일련번호는 직접 바꿉니다(1900 체계의 가짜 1900-02-29, 곧 일련번호 60 은 던지고 61 이 1900-03-01). 로컬 시간대로 읽으면 6월 1일이 5월 31일이 됩니다. 엑셀로 쓰는 날짜 열은 UTC 자정의Date에 서식yyyy-mm-dd(dateFormat으로 바꿈)입니다. - 날짜 열 값은
DateOnly만: 내보낼 때 날짜 열 값이YYYY-MM-DD가 아니면 열 이름을 담아 던집니다. 모양을 짐작해 다른 날로 쓰지 않습니다. - 빈 칸이 열을 밀지 않는다:
readWorksheet는 칸을 1부터columnCount까지 번호로 하나씩 읽습니다. 행의 값 배열을 순회하면 빈 칸을 건너뛰어 뒤 칸이 앞으로 당겨집니다. - 앞자리 0 보존: 글자 열은 엑셀에 문자열과 서식
@로 씁니다. 숫자로 저장되면 전화번호·ID 의 앞자리 0 이 사라집니다. - 헤더는 정규화해서 맞춤: NFKC(전각
ID가ID와 같아집니다), 앞뒤 공백 제거, 연속 공백 하나로, 대소문자 무시. 열 이름이 바뀐 파일은aliases로 받고, 못 찾은 열은missingHeaders·행별 오류로 알립니다. 가져오기는 «성공 N건» 대신 행별 오류 보고를 돌려줍니다. - 열 정의 검사: 키가 비어 있거나 겹치면
defineColumns가 던집니다. 같은 키 두 열이 서로를 덮어쓰기 때문입니다. 헤더·별칭이 정규화 뒤 겹치거나 비어 있어도, 키가__proto__여도 던집니다. - 내보낼 값의 검사: 유한하지 않은 숫자(
NaN·Infinity)와Date·객체·함수 값은 열 이름을 담아 던집니다.bigint는 글자로 씁니다. - 구분자 검사:
toCsv·parseCsv의delimiter가 한 글자가 아니거나 따옴표·CR·LF 이면RangeError입니다. - 오류 메시지는
@plott/webkit-tabular:로 시작합니다.
그 밖
- 런타임 의존성은
@plott/webkit-time하나입니다.exceljs는 구조 타입으로만 맞춥니다. - 단위 테스트는
TZ=America/Los_Angeles에서 돕니다. 엑셀 날짜를 로컬 getter 로 읽으면 이 시간대에서 하루가 밀립니다. - 가져오기는 오류를 던지지 않고
errors·missingHeaders·unknownHeaders로 돌려줍니다. 던지는 것은 열 정의 오류, 읽을 수 없는 CSV(CsvError), 한도를 넘는 시트(ExcelLimitError), 잘못된 구분자뿐입니다.