@plott/webkit-format0.4.0
화면에 값을 쓰는 규칙 — 로케일(BCP 47)과 시간대를 명시하는 Intl 포매터(숫자·통화·면적·퍼센트·날짜·기간·시각·길이·목록·따옴표), 금액 최소 단위 변환, 글자 수·정렬·카드 번호 가림, 한국 형식(전화번호·사업자·법인·주민등록번호·조사·초성·금융기관 코드·번호판), React 공급자(/react)
설치
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-format | createFormat(로케일·시간대를 묶은 포매터), currencyDigits·toMinor·fromMinor(금액 최소 단위), parseAmount·parseAmountResult(입력한 금액 읽기), quote·quotes(따옴표), normalizeDigits(전각 등 숫자를 ASCII 로), graphemeCount(글자 수), collatorSort(로케일 정렬), maskCardNumber(카드 번호 가림) |
@plott/webkit-format/react | FormatProvider(앱에 로케일·시간대·통화를 한 번 정함), 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(법인등록번호 검증식) |
붙이기
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 이 범위 구분자를 주지 않을 때 쓰는 글자 |
fileSizeBase | 1024 | f.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).
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 에서 한 번 정하기
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)는 로케일이 없으면 던집니다.
한국 형식
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자리(등기소 코드)와 번호가 실제 있는지는 보지 않습니다.
따옴표
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 의 표와 비교합니다).
금융기관 코드와 번호판
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뱅크)으로 싣고 있고 코드는 이 목록과 모두 같습니다. 금융결제원의 금융회사 공동코드 원본 파일과는 대조하지 않았습니다. 토스페이먼츠 표에는 옛 이름이 남아 있습니다(
227KTB투자증권,292LIG투자증권,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입니다.