본문으로 건너뛰기
언어

@plott/webkit-format0.4.0

화면에 값을 쓰는 규칙 — 로케일(BCP 47)과 시간대를 명시하는 Intl 포매터(숫자·통화·면적·퍼센트·날짜·기간·시각·길이·목록·따옴표), 금액 최소 단위 변환, 글자 수·정렬·카드 번호 가림, 한국 형식(전화번호·사업자·법인·주민등록번호·조사·초성·금융기관 코드·번호판), React 공급자(/react)

설치

pnpm
pnpm add @plott/webkit-format@0.4.0

진입점(exports)

import타입파일
@plott/webkit-format./dist/index.d.ts./dist/index.js
@plott/webkit-format/kr./dist/kr/index.d.ts./dist/kr/index.js
@plott/webkit-format/react./dist/react/index.d.ts./dist/react/index.js

값을 화면 글자로 쓰는 패키지입니다. 읽는 사람은 금액·날짜·시각·길이·목록을 화면에 그리는 프론트엔드 개발자입니다. 날짜 계산은 @plott/webkit-time, 문장(메시지)은 @plott/webkit-i18n 이 합니다.

런타임 의존성은 @plott/webkit-time 하나입니다(Intl 로 씁니다). /react 만 react 를 선택 peer 로 씁니다.

진입점

import주요 export
@plott/webkit-formatcreateFormat(로케일·시간대를 묶은 포매터), currencyDigits·toMinor·fromMinor(금액 최소 단위), parseAmount·parseAmountResult(입력한 금액 읽기), quote·quotes(따옴표), normalizeDigits(전각 등 숫자를 ASCII 로), graphemeCount(글자 수), collatorSort(로케일 정렬), maskCardNumber(카드 번호 가림)
@plott/webkit-format/reactFormatProvider(앱에 로케일·시간대·통화를 한 번 정함), useFormat(그 포매터 읽기). 클라이언트 컴포넌트
@plott/webkit-format/kr한국 형식: formatPhoneKr·isPhoneKr·phoneKindKr·toE164Kr·fromE164Kr·phoneDigits, formatBizNoKr·isBizNoKr(사업자등록번호 검증식), formatRrnKr·maskRrnKr·isRrnShapeKr(주민등록번호), josa·withJosa·createJosa·hasFinalConsonant·finalConsonantOf(조사), BANKS_KR·LEGACY_BANKS_KR·bankNameKr·bankSuccessorKr·isBankCodeKr(금융기관 코드), parsePlateKr·isPlateKr(자동차 번호판), pyeongFromSquareMeters·squareMetersFromPyeong(평↔㎡), initialsKr·matchInitialsKr(초성), maskNameKr·maskAccountNumberKr(이름·계좌번호 가림), isPostalCodeKr(우편번호), isCorpRegNoKr(법인등록번호 검증식)

붙이기

ts
import { createFormat } from "@plott/webkit-format";import { dateOnly, timeOfDay } from "@plott/webkit-time";import { KST } from "@plott/webkit-time/kr";
declare const locales: { tagOf(locale: string): string };declare const locale: string;declare const paidAt: number;declare const createdAt: number;declare const now: number;
const start = dateOnly("2026-11-03");const end = dateOnly("2026-11-05");
const f = createFormat({ locale: locales.tagOf(locale), timeZone: KST, currency: "KRW" });const g = createFormat("ko-KR", "Asia/Seoul");
f.currency(51000);f.currency(-3000, { sign: "always" });f.percent(0.153);f.date(start);f.dateRange(start, end);f.time(timeOfDay("15:00"));f.dateTime(paidAt);f.duration(90);f.relative(createdAt, now);f.list(["A", "B", "C"]);f.displayName("JP", "region");
호출ko 결과
f.currency(51000)₩51,000 (모든 언어에서 ₩)
f.currency(-3000, { sign: "always" })-₩3,000
f.percent(0.153) / f.percent(0.153, 1)15% / 15.3%
f.date(start)2026년 11월 3일
f.dateRange(a, b)26. 11. 03. ~ 26. 11. 05.
f.dateTime(paidAt)2026년 11월 3일 14:30 (포매터의 시간대)
f.duration(90)1시간 30분
f.relative(createdAt, now)3분 전, 어제
f.list(["A", "B", "C"])A, B 및 C

createFormat 의 선택 옵션입니다.

옵션기본값뜻
currency없음f.currency 가 쓰는 ISO 4217 코드
rangeSeparator로케일 표(ko ~, ja ~, 그 밖 –)Intl 이 범위 구분자를 주지 않을 때 쓰는 글자
fileSizeBase1024f.fileSize 가 단위를 올리는 기준(1000 또는 1024)
relativeThresholds{ seconds: 45, minutes: 45, hours: 22, days: 26, months: 11 }f.relative 가 단위를 바꾸는 경계. 일부만 넘길 수 있고 양의 유한수가 아니면 던짐
hourCycle"h23""h23" 은 모든 로케일에서 24시간제(14:30), "locale" 은 로케일의 기본 시간제(en-US 2:30 PM, ko 오후 2:30). 그 밖의 값은 RangeError

포매터는 화면(로케일)마다 하나 만들어 씁니다. 안의 Intl 객체는 로케일·옵션별로 한 번만 만들어 다시 씁니다. f.resolvedLocale 은 Intl 이 실제로 고른 로케일입니다(지원하지 않는 태그면 기본 로케일로 떨어집니다).

그 밖의 포매터 메서드입니다.

호출결과
f.area(30, "square-meter") / f.area(1234.5, "square-foot")30 m² / 1,234.5 ft² (en). 숫자는 로케일 서식, 단위는 기호 m²·ft². Intl 에 면적 단위가 없어 이렇게 붙입니다. 모르는 단위는 RangeError
f.currencyParts(51000)formatToParts 와 같은 [{ type: "currency", value: "₩" }, { type: "integer", value: "51" }, …]. 통화 기호만 따로 그릴 때 씁니다
f.dateTime("2026-11-03T05:30:00Z")순간을 가리키는 ISO 8601 문자열(끝에 Z 나 +09:00)도 받습니다. 오프셋 없는 문자열(2026-11-03T14:30:00)은 RangeError 입니다

금액의 최소 단위(minor unit) 변환입니다. 자릿수는 Intl 이 아는 통화별 값입니다(KRW 0, USD 2, KWD 3).

ts
import { currencyDigits, fromMinor, toMinor } from "@plott/webkit-format";
currencyDigits("USD");toMinor(12.34, "USD");fromMinor(1234, "USD");

결과는 2, 1234, 12.34 입니다. toMinor 는 소수 글자를 정수로 셈해 1.15 가 115 로 나옵니다.

React 에서 한 번 정하기

tsx
import type { ReactNode } from "react";import { FormatProvider, useFormat } from "@plott/webkit-format/react";
declare const tag: string;declare const children: ReactNode;
export function Root() {  return (    <FormatProvider locale={tag} timeZone="UTC" currency="USD">      {children}    </FormatProvider>  );}
export function Price({ amount }: { amount: number }) {  return <p>{useFormat().currency(amount)}</p>;}

FormatProvider 는 createFormat 의 옵션을 그대로 props 로 받고, 값이 바뀔 때만 새 포매터를 만듭니다. useFormat() 은 같은 createFormat 결과를 돌려줍니다.

입력 도우미

호출결과
normalizeDigits("123")123 (숫자가 아닌 글자는 그대로)
graphemeCount("👨‍👩‍👧‍👦🇰🇷", "en")2 (사용자가 보는 글자 수, Intl.Segmenter)
collatorSort(["z", "ä", "a"], "sv")["a", "z", "ä"] (새 배열, Intl.Collator. 옵션 numeric·sensitivity 등과 key)
maskCardNumber("1234-5678-9012-3456")**** **** **** 3456 (옵션 visibleFirst 0–6, visibleLast 0–4, mask, separator)

로케일을 받는 도우미(graphemeCount·collatorSort)는 로케일이 없으면 던집니다.

한국 형식

ts
import {  formatPhoneKr,  initialsKr,  isBizNoKr,  isCorpRegNoKr,  isPostalCodeKr,  maskAccountNumberKr,  maskNameKr,  maskRrnKr,  matchInitialsKr,  toE164Kr,  withJosa,} from "@plott/webkit-format/kr";
declare const branch: { name: string };
formatPhoneKr("0212345678");toE164Kr("010-1234-5678");isBizNoKr("120-81-47521");maskRrnKr("9001011234567");withJosa(branch.name, "으로/로");initialsKr("홍길동");matchInitialsKr("서울특별시 강남구", "ㄱㄴㄱ");maskNameKr("홍길동");maskAccountNumberKr("110-123-456789");isPostalCodeKr("06236");isCorpRegNoKr("130111-0006246");
호출결과
formatPhoneKr("0212345678")02-1234-5678
toE164Kr("010-1234-5678")+821012345678
isBizNoKr("120-81-47521")true
maskRrnKr("9001011234567")900101-1******
withJosa(branch.name, "으로/로")서울로 (branch.name 이 «서울» 일 때)
initialsKr("홍길동")ㅎㄱㄷ (한글 아닌 글자는 그대로)
matchInitialsKr("서울특별시 강남구", "ㄱㄴㄱ")true (초성과 글자를 섞어 부분 일치. 빈 질의는 true)
maskNameKr("홍길동")홍*동 (두 글자 홍*, 한 글자 *)
maskAccountNumberKr("110-123-456789")***-***-**6789 (마지막 visibleLast 자리(기본 4, 0–6)만 보임, 구분자는 그대로)
isPostalCodeKr("06236")true (숫자 5자리, 00000 은 거짓. 모양만 봅니다)
isCorpRegNoKr("130111-0006246")true

법인등록번호의 마지막 자리는 앞 12자리에 1·2 를 번갈아 곱해 더한 값을 10 으로 나눈 나머지를 10 에서 뺀 값(10 이면 0)입니다(나무위키 «법인등록번호» «오류검색번호»). 2025년 1월 31일 이후 부여한 번호는 이 자리가 일련번호의 일부라 검증식이 없고, isCorpRegNoKr 은 그런 번호에 false 를 줄 수 있습니다. 앞 6자리(등기소 코드)와 번호가 실제 있는지는 보지 않습니다.

따옴표

ts
import { quote, quotes } from "@plott/webkit-format";
declare const hotel: { name: string };
quote(hotel.name, "ja");quote("Hotel", "de");quote("hôtel", "fr");quote("안쪽", "ko", { level: 2 });quotes("zh-TW");

결과는 「호텔」, „Hotel“, « hôtel », ‘안쪽’, { open: "「", close: "」", openAlt: "『", closeAlt: "』" } 입니다. 찾는 순서는 지역(pt-PT)·표기 체계(zh-TW → 번체) → 언어 → 영어이고, 언어가 없는 태그(und)와 모르는 언어는 영어(“ ”)입니다. 글을 이스케이프하지 않고 감쌉니다. 프랑스어는 안쪽에 보통 공백 하나를 둡니다(space 로 켜고 끕니다). 틀린 태그와 level 은 RangeError 입니다. 글자표는 Unicode CLDR 49(delimiters.json, cldr-json cldr-misc-full 49.0.0)에서 옮긴 47 행입니다(원본과 맞추려면 같은 파일을 받아 src/quote.ts 의 표와 비교합니다).

금융기관 코드와 번호판

ts
import { bankNameKr, bankSuccessorKr, parsePlateKr } from "@plott/webkit-format/kr";
bankNameKr("004");bankNameKr("005");bankNameKr("005", { legacy: "exclude" });bankSuccessorKr("230");parsePlateKr("서울 12 가 3456");parsePlateKr("85배4567");
호출결과
bankNameKr("004")국민은행
bankNameKr("005")외환은행
bankNameKr("005", { legacy: "exclude" })null
bankSuccessorKr("230")"238"
parsePlateKr("서울 12 가 3456"){ normalized: "서울12가3456", kind: "private", usage: "private", vehicleType: "passenger", region: "서울" }
parsePlateKr("85배4567")kind: "commercial", usage: "delivery"
  • BANKS_KR 은 일부 목록입니다. 외국 은행은 054(HSBC)만 있고 카드사 코드는 없습니다. 이름은 기관의 현재 이름입니다. isBankCodeKr 가 false 인 것은 «이 목록에 없다» 이지 «없는 코드» 가 아닙니다. 계좌번호 자릿수는 검사하지 않습니다.
  • 코드와 이름의 출처: 위키백과 «대한민국의 금융기관 목록»(괄호 안 숫자를 금융결제원 금융기관 코드로 적은 표), 토스페이먼츠 기관 코드표(«금융결제원 공식 코드» 열), 토스페이 은행 코드. 금융결제원 고객센터 ARS 1369 안내 표는 은행·상호금융 코드 24개만 옛 이름(KEB하나은행·대구은행·K뱅크)으로 싣고 있고 코드는 이 목록과 모두 같습니다. 금융결제원의 금융회사 공동코드 원본 파일과는 대조하지 않았습니다. 토스페이먼츠 표에는 옛 이름이 남아 있습니다(227 KTB투자증권, 292 LIG투자증권, 294 펀드온라인코리아).
  • 합병으로 더는 쓰지 않는 코드(005 외환은행 → 081, 230 미래에셋증권 → 238)는 LEGACY_BANKS_KR 에 있고 bankNameKr 가 기본(legacy: "include")으로 찾습니다. 이름만 바뀌고 코드가 그대로인 기관은 BANKS_KR 에 현재 이름으로 있습니다. 선택 화면에는 BANKS_KR 만 씁니다. 후속 코드는 bankSuccessorKr 입니다.
  • 번호판은 모양과 종류만 봅니다. 현행 꼴(12가3456·123가4567)과 옛 지역명 꼴(서울12가3456)을 읽고, 종류는 한글 한 글자로 정합니다(자가용 가나다라마 거너더러머버서어저 고노도로모보소오조 구누두루무부수우주, 일반사업용 바사아자배, 대여사업용 하허호). 차종은 앞 숫자로 가릅니다: 승용 01–69·100–699, 승합 70–79·700–799, 화물 80–97·800–979, 특수 98–99·980–997, 긴급 998–999. 일반사업용 글자는 두 자리 분류기호만 쓰므로 세 자리와 함께 오면 null 입니다. 배 는 kind: "commercial" 이면서 usage: "delivery" 입니다. 외교용(외교·영사·준외·준영·국기·협정·대표)·이륜차 글자·군용·건설기계·임시번호판은 null 입니다. 등록된 번호인지는 모릅니다. 출처: 국토교통부고시 제2023-954호 «자동차 등록번호판 등의 기준에 관한 고시»(시행 2024-01-01) 제5조 표, 국가법령정보센터. 같은 고시의 제2025-676호 개정(시행 2026-11-28)은 제2조·제3조·제3조의2·제4조·제4조의2·제8조(재료·반사 성능·검사)를 바꾸고 제5조는 바꾸지 않으며, 개정 뒤 전문의 제5조 표도 위와 같습니다(한국자동차제작자협회 게시 개정문·전문).

기본으로 지키는 것

  • 로케일에 기본값이 없습니다. 인자 기본값에 숨은 ko 는 모든 언어 화면을 한국어로 그립니다. createFormat 은 로케일이 없으면 던집니다.
  • 시간대에도 기본값이 없습니다. 빠지면 실행 환경의 시간대로 떨어져 서버와 브라우저가 다른 시각을 그립니다(hydration 불일치). createFormat 은 timeZone 이 없거나 틀리면(EST, Asia/seoul) 던집니다. 이 패키지의 테스트는 Pacific/Kiritimati(UTC+14)에서 돌아 호스트 시간대로 새는 곳이 있으면 깨집니다.
  • 로케일은 BCP 47 태그입니다(zh-Hant). 앱의 로케일 코드(zh-hant)가 아니라 locales.tagOf(locale) 을 넘깁니다. 틀린 태그는 만들 때 RangeError 입니다.
  • 시각은 기본으로 24시간제입니다. Node(ICU)와 브라우저가 오전/오후 낱말을 다르게 그려 hydration 이 어긋납니다. hour12: false 대신 hourCycle: "h23" 을 씁니다(앞의 것은 일부 엔진에서 자정을 24:00 으로 냅니다). 로케일의 시간제를 쓰려면 createFormat 의 hourCycle: "locale" 로 직접 고릅니다.
  • React 에서 공급자 없이 useFormat 을 부르면 던집니다. 로케일·시간대에 기본값이 없는 createFormat 과 같은 규칙입니다. 숨은 기본값은 서버와 브라우저가 다른 글자를 그리게 합니다.
  • 금액의 최소 단위 변환은 정수로 셈합니다. 1.15 * 100 은 114.99999999999999 지만 toMinor(1.15, "USD") 는 115 입니다. 유한하지 않은 값, 통화 최소 단위보다 아래 자리가 있는 값(toMinor(0.5, "KRW")), 정수가 아니거나 안전 정수를 넘는 최소 단위(fromMinor(12.5, "USD"))는 RangeError 입니다.
  • dateTime 은 오프셋 없는 ISO 문자열을 받지 않습니다. 2026-11-03T14:30:00 은 기기 시간대로 읽혀 서버와 브라우저가 다른 순간을 그립니다. Z 나 +09:00 이 있는 문자열만 읽습니다(@plott/webkit-time 의 parseInstant 와 같은 규칙).
  • 전각·다른 문자 체계의 숫자를 숫자로 읽습니다. formatBizNoKr·isBizNoKr·formatRrnKr·maskRrnKr·isRrnShapeKr·isCorpRegNoKr·isPostalCodeKr·maskAccountNumberKr·maskCardNumber 는 123 을 123 으로 정규화합니다. 그러지 않으면 빈 값·false·전부 가림으로 떨어집니다.
  • 모든 출력의 특수 공백(U+00A0·U+202F·U+2009·U+200A)을 보통 공백으로 바꿉니다. ICU 판마다 다르게 넣는 공백이 서버와 브라우저 글자를 갈라 놓지 않게 합니다. 값이 줄 끝에서 끊길 수 있으니 끊기면 곤란한 자리는 감싸는 쪽에서 white-space: nowrap 을 줍니다.
  • 유한하지 않은 값(NaN·Infinity·잘못된 Date)은 모든 포매터가 빈 글자 "" 로 냅니다. NaN byte·∞ TB·NaN:NaN 을 화면에 두지 않습니다. production 이 아니면 함수마다 한 번 console.warn 으로 알립니다. 반올림해 0 이 되는 음수는 -0·-₩0 이 아니라 0·₩0 입니다(sign 을 직접 정하면 Intl 규칙).
  • relative 는 앞뒤가 대칭입니다. +90분 은 «2시간 뒤», -90분 은 «2시간 전». 기본 경계는 45초 미만 초(0 은 «지금») · 45분 미만 분 · 22시간 미만 시간 · 26일 미만 날(이 시간대의 달력) · 11개월 미만 달 · 그 위 해이고 relativeThresholds 로 바꿉니다. 주 단위는 쓰지 않습니다.
  • duration 은 가장 가까운 분(객체면 필드별 정수)으로 반올림합니다. 음수는 부호를 한 번만(-90 → -1 hr, 30 min). Intl.DurationFormat 이 없는 런타임(Node 22 이하)에서는 NumberFormat 의 단위와 ListFormat 으로 같은 낱말을 냅니다.
  • numberRange(5, 5) 는 5 (근사 기호 없이, 반올림해 같아져도). 앞이 더 크면 받은 순서대로 찍습니다.
  • fileSize 는 표시 자리로 반올림한 뒤 단위를 고릅니다. 1048575 바이트는 1 MB(기본 기준 1024, fileSizeBase: 1000 이면 1000). 음수는 부호를 붙여 그대로 씁니다.
  • dateRange 의 양 끝은 같은 style 의 date() 와 같은 글자이고 둘 사이만 CLDR 의 이음 글자입니다. 이음 글자는 Intl 의 범위 패턴에서 첫 범위 기호(공백·–·~·~·至) 이후만 쓰므로, 패턴에 딸린 앞 날짜의 끝 글자(ko 의 일, de 의 .)가 겹쳐 찍히지 않습니다. 범위 줄임(Nov 3 – 5, 2026)과 2자리 채움 풀기는 쓰지 않습니다.
  • weekday 는 ISO 1–7 의 정수만 받고 monthName 은 1–12 만 받습니다. 그 밖은 RangeError 입니다.
  • 달력 날짜는 시간대를 거치지 않습니다. 체크인 날짜가 기기 시간대 때문에 하루 밀리지 않습니다. 순간(결제 시각)은 포매터의 시간대로 찍습니다.
  • 낱말·어순·범위 기호는 CLDR(Intl)이 정합니다. 문자열을 이어 붙이지 않습니다. 시각 범위는 같은 화면의 time() 과 같은 모양입니다.
  • 금액은 서버가 계산한 값을 그대로 씁니다. 반올림해 다시 셈하지 않습니다(표시 자리수만 maxFractionDigits). 통화 기호는 narrowSymbol 이라 언어마다 KRW·₩·₩ 로 갈리지 않습니다. 통화가 없으면 currency() 가 던집니다.
  • 입력한 금액은 로케일의 자리 구분·소수점으로 읽습니다(parseAmount). 숫자 아닌 글자를 지워 읽으면 1,234.56 이 123456 이 됩니다. 모양이 틀리면 null 입니다. 포매터가 낸 글자는 다시 읽힙니다: 인도식 묶음(12,34,567), 아랍-인도·전각 숫자, de-CH 의 ’·', fr 의 좁은 NBSP. 음수는 allowNegative 일 때 -5·−5(U+2212)·(1,000); 5- 처럼 모호한 꼴은 null, .5 는 0.5(allowFraction: false 면 null), -0 은 0. parseAmountResult 는 { value } 또는 { error: "empty" | "format" | "negative" | "fraction" } 를 줍니다. 올바르지 않은 로케일 태그는 코드의 실수라 던집니다.
  • 전화번호는 E.164 로 저장하고 화면에서만 하이픈을 넣습니다. 02 는 두 자리 지역번호, 15xx 는 여덟 자리 대표번호(해외에서 걸 수 없어 toE164Kr 이 null), 0505 는 11자리면 4-3-4, 12자리면 4-4-4 입니다. +82·0082 만 한국 국가 번호로 읽고 다른 나라의 + 번호는 묶지 않고 +14155552671 로 돌려줍니다. + 없는 82 는 한국 번호 모양이 되는 순간(821012345678)에만 국가 번호로 읽습니다. 전각 숫자는 정규화하고 넘치는 자리는 버립니다.
  • 주민등록번호는 가려서 보입니다. 13자리가 아니면 전부 가립니다. visibleBackDigits 는 0–7 의 정수이고 아니면 입력 길이와 무관하게 던집니다. 검증식은 2020년 10월 이후 발급분에 없어 모양만 봅니다(isRrnShapeKr): 뒷자리 첫 숫자 1·2/5·6 은 1900년대, 3·4/7·8 은 2000년대, 9·0 은 1800년대이고 있을 수 없는 날짜는 거릅니다. 시계를 읽지 않으니 미래의 생일은 isRrnShapeKr(x, { today: "2026-11-03" }) 로 거릅니다.
  • 번호판·금융기관 코드는 모르면 null 입니다. 짐작해 채우지 않습니다. 번호판은 앞 숫자 0·뒤 0000·번호판에 쓰지 않는 한글(갸)·시·도가 아닌 지역명을 거릅니다.
  • 영문 낱말의 조사는 읽는 소리를 어림합니다. Line·Time·Phone(끝 e 는 소리 없음)·Mac·Book(모음 뒤 c·k)은 받침 있음, Park·Mark·Airbnb(자음 뒤 k·b)는 받침 없음, 약어(KT·LG)는 글자 이름으로 읽습니다. 어림이 틀리면 josa(word, pair, { pronunciation: "라인" }) 로 읽는 소리를 주거나 createJosa({ dictionary: { Slack: "슬랙" } }) 로 사전을 적습니다(키는 대소문자를 가리지 않습니다). 받침을 모르면 이(가) 입니다.

그 밖

  • 수를 넣은 문장(3박·3 nights)은 복수형이 언어마다 달라 메시지의 일입니다. @plott/webkit-i18n 의 count 를 씁니다.
  • 테스트는 pnpm test, 커버리지는 pnpm test:coverage 입니다.