본문으로 건너뛰기
언어

@plott/webkit-tabular0.4.0

표 데이터 내보내기·가져오기 — 열 정의 하나로 CSV 쓰기(수식 주입 차단·BOM)·CSV 읽기·엑셀 셀 값 정리(UTC 날짜·앞자리 0 보존)·행별 오류 보고. 의존성 없음(exceljs 는 구조 타입으로만 맞춘다)

설치

pnpm
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

붙이기

열 정의

ts
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헤더 글자
typetext·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 내보내기·가져오기

ts
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)

옵션기본값뜻
bomtrue맨 앞에 UTF-8 BOM
eol"\r\n"줄 끝
delimiter","칸 구분자(한 글자, 따옴표·개행 제외)
headertrue헤더 줄을 쓸지

parseCsv(text, { delimiter, skipEmptyLines }) 는 string[][] 를 돌려줍니다. 따옴표 칸과 "", CRLF·LF·CR, 맨 앞 BOM, 빈 줄을 다룹니다.

옵션기본값뜻
delimiter","칸 구분자
skipEmptyLinestrue빈 줄(글자·구분자·따옴표가 없는 줄)을 어디서든 건너뜁니다. false 면 사이의 빈 줄을 [""] 로 남깁니다. 끝에 이어진 빈 줄은 false 에서도 모두 버립니다. "" 만 있는 줄은 빈 줄이 아니라 한 칸짜리 행입니다

엑셀 내보내기·가져오기

ts
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 대신 직접 정합니다
maxRows100000행 수 상한
maxCols200열 수 상한
maxCells2000000행 × 열 상한

한도를 넘으면 반복을 시작하기 전에 ExcelLimitError(limit·actual·max)로 던집니다. 올린 파일의 사용 범위 rowCount·columnCount 는 먼 구석 한 칸(XFD1048576)만 채워도 커져서, 그대로 돌면 서버가 수백억 번 칸을 읽습니다. 한도를 늘리려면 옵션을 올리세요(끄려면 Infinity).

cellValue(v) 는 exceljs 칸 값의 여러 모양(서식 있는 글 richText, 수식, 링크, 오류)을 평범한 값으로 바꿉니다. readWorksheet 가 칸마다 부르므로 보통 직접 쓸 일은 없습니다. 일련번호를 직접 바꿔야 하면 excelSerialToDateOnly(serial, { date1904 }), Date 는 excelDateToDateOnly(date) 입니다.

가져오기 결과

readRows(matrix, columns, opts) 는 행렬을 열 정의대로 객체 행으로 바꿉니다.

옵션기본값뜻
headerRow0헤더가 있는 줄(행렬 인덱스)
match"header"header: 헤더 글자로 열을 찾습니다. index: 열 정의 순서가 곧 시트 열 순서(헤더 줄은 건너뜁니다)
date1904readWorksheet 가 실어 둔 값, 없으면 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 은 거절
integernumber 와 같고 정수만
dateYYYY-MM-DD, YYYY/MM/DD, YYYY.MM.DD(월·일 0 채움은 선택. 순서는 dateFormats), Date(UTC 성분), 엑셀 일련번호. 없는 날짜는 거절
booleanTRUE·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), 잘못된 구분자뿐입니다.