본문으로 건너뛰기

Invoice Builder로 원화 인보이스를 만들어 봤다 — PDF 한글 깨짐, 포크해서 고쳤다

오프라인 우선, 구독 없음, 데이터는 내 DB 파일에. PDF에서 한글이 전부 깨져서 한글 폰트를 넣어 포크로 고쳤다. 코드 6곳이면 됐다. 전자세금계산서는 여전히 별도 경로다.

프리랜서나 1인 기업이 인보이스 SaaS에 월 구독료를 내는 게 아깝다는 얘기를 자주 듣는다. 견적서 몇 장 보내려고 계정을 만들고 데이터를 남의 서버에 둔다.

Invoice Builder는 그 반대편이다. 계정 없음, 클라우드 없음, 구독 없음. 데이터는 내 DB 파일에 들어간다. MIT 라이선스, 스타 1,176개, 최신 릴리스는 2026년 9월 16일 v2.9.0이다.

직접 띄우고 원화 인보이스를 만들어 봤다. 결론부터 쓰면, 설계는 좋고 한국에서는 두 군데가 막힌다. 하나는 PDF 한글이고, 이건 포크해서 직접 고쳤다. 다른 하나는 전자세금계산서이고, 이건 도구 밖에서 풀어야 한다.

실행: 애플 실리콘에서는 공식 이미지가 안 돈다

먼저 걸린 것부터. 공식 도커 이미지를 받으려 하면 이렇게 끝난다.

no matching manifest for linux/arm64/v8 in the manifest list entries

이미지가 amd64 단일 아키텍처로만 올라가 있다. 애플 실리콘 맥에서는 공식 이미지를 그대로 못 쓴다. 에뮬레이션으로 돌리거나 소스에서 빌드해야 한다.

소스 빌드로 갔다. 빌드에 211초, 컨테이너가 정상 상태가 되는 데 7초. 접속은 localhost:3001이다. 데스크톱 앱(윈도우, 리눅스 AppImage, macOS DMG)도 있는데 macOS 앱은 서명이 안 돼 있어 검역 속성을 손으로 풀어야 한다.

DB는 둘 중에 고른다. 혼자 쓰면 SQLite 파일, 여러 명이 쓰면 PostgreSQL 서버다.

한국 시나리오로 만들어 봤다

원화 통화를 직접 추가했다. 기본 제공 통화 10개(USD, EUR, JPY 등)에 KRW가 없다. 추가 자체는 정상 동작한다. 기호, 표기 형식, 소수 단위를 직접 정한다.

사업자와 고객, 품목, 단위, 은행을 넣고 부가세 10% 인보이스를 만들었다.

Invoice Builder 인보이스 목록 — 한글 고객명과 원화 금액이 정상 표시
앱 화면에서는 한글이 정상이다. 고객명 주식회사 예시, 금액 ₩1,800,000.00. 2026-09-18, v2.9.0.

여기까지는 문제가 없었다. PDF를 뽑기 전까지는.

PDF에서 한글이 전부 깨진다

PDF 미리보기 — 한글이 전부 깨진 문자로 표시되고 원화 기호와 숫자만 정상
같은 문서의 PDF 미리보기. 김정우 컨설팅이 @$°, 주식회사 예시가 üŴŒ¬로 나온다. 원화 기호와 숫자는 정상이다.

원인을 폰트까지 확인했다. 고를 수 있는 PDF 폰트는 다섯 개다. Helvetica, Times-Roman, Courier, Roboto, Inter. 앞의 세 개는 PDF 표준 폰트라 한글이 없다. 뒤의 두 개는 번들된 TTF를 열어 문자 목록을 확인했더니 한글 음절 글리프가 0개였다. 파일 크기가 각각 340KB 남짓인 라틴 전용 서브셋이다.

재미있는 건 원화 기호는 된다는 점이다. ₩는 두 폰트 모두에 들어 있어서 ₩1,500,000.00이 정상으로 찍힌다. 기호는 되고 글자는 안 되는 비대칭이다.

고치는 방법은 하나다. 한글 폰트를 앱에 번들하고 PDF 폰트로 등록하는 것. 폰트 목록이 코드에 상수로 박혀 있어서 포크해서 소스를 고쳐야 한다. 그래서 해 봤다.

한글 폰트를 넣어 포크로 고쳤다

v2.9.0 태그에서 브랜치를 따고 한글 폰트를 넣었다. 폰트는 Pretendard 1.3.9(SIL OFL 1.1)를 공식 릴리스에서 받았다. 한글 음절 11,172자가 전부 있고 ₩도 있다. Regular와 Bold 두 파일, 합쳐서 약 5.4MB다.

PDF는 @react-pdf/renderer가 만든다. 폰트 등록 함수와 폰트 목록을 찾아 고친 곳은 여섯 군데다.

파일한 일
Preview/PDFDocument.tsx폰트 import와 Font.register
state/constant.ts폰트 목록에 Pretendard (한국어) 추가
enums/fontFamily.ts (2곳)화면·백엔드 enum에 값 추가
utils/typeGuardFunctions.ts저장 전 검증 목록에 추가
Preview/constant.ts글자 폭 계수 추가

라이선스 파일까지 합쳐 9개 파일, 폰트 파일을 빼면 코드 변경은 20줄이 안 된다. 기본 폰트는 Roboto로 그대로 두고 한글 폰트를 선택지로만 추가했다. DB의 폰트 컬럼은 제약 없는 텍스트라 마이그레이션도 필요 없었다.

핵심은 등록 네 줄이다.

Font.register({ family: 'Pretendard', src: PretendardRegular, fontWeight: 400 });
Font.register({ family: 'Pretendard', src: PretendardRegular, fontWeight: 400, fontStyle: 'italic' });
Font.register({ family: 'Pretendard', src: PretendardBold, fontWeight: 700 });
Font.register({ family: 'Pretendard', src: PretendardBold, fontWeight: 700, fontStyle: 'italic' });

이탤릭을 같은 파일로 한 번 더 등록한 게 요령이다. Pretendard에는 이탤릭이 없다. 그런데 PDF 레이아웃이 이탤릭을 17곳에서 쓰고, react-pdf 4.x는 같은 스타일의 폰트가 등록돼 있지 않으면 PDF 생성 자체를 실패시킨다. 정자체 파일을 이탤릭 이름으로도 등록해 두면 통과한다.

소스로 도커 이미지를 다시 빌드했다. 애플 실리콘에서 210초, 처음 빌드와 같은 수준이다. 설정의 폰트 목록에서 Pretendard (한국어)를 고르고 같은 인보이스를 다시 뽑았다.

포크한 빌드의 PDF 미리보기 — 사업자명, 고객명, 품목명, 부가가치세, 은행명까지 한글이 정상 표시
같은 문서를 포크한 빌드에서 Pretendard로 뽑았다. 김정우 컨설팅, 주식회사 예시, 품목 AX 교육 워크숍, 부가가치세(VAT) (10%)까지 정상이다. 이메일과 계좌번호는 예시값. 2026-09-19.

화면만 보고 끝내지 않고 PDF 파일을 열어 확인했다.

  • 폰트 임베딩: pdffonts로 보면 Pretendard-Regular, Pretendard-Bold 두 개가 서브셋(CID TrueType, Identity-H)으로 들어가 있다.
  • 텍스트 추출: pdftotext로 뽑으면 김정우 컨설팅, AX 교육 워크숍, 결제 기한 30일이 그대로 나온다. 받는 쪽에서 복사하고 검색할 수 있는 PDF다. 수정 전 PDF에서는 @°, üÝŒ¬ 같은 문자열이 나왔다.
  • 파일 크기: 같은 인보이스가 Roboto로 31,486바이트, Pretendard로 18,898바이트다. 5.4MB 폰트를 넣었는데 PDF는 40% 작아졌다. 쓰인 글자만 서브셋으로 넣기 때문이고, Roboto 쪽은 굵기별 서브셋이 네 개 들어가서 더 크다.

한계도 하나 남았다. 두 굵기만 넣어서 굵기 500을 쓰는 업체명 같은 자리가 Regular로 나온다. Roboto보다 살짝 얇아 보인다. Medium 파일(약 2.7MB)을 하나 더 등록하면 된다.

같이 확인한 것 두 가지

원화 소수점은 없앨 수 있다. 설정의 금액 형식을 en-US-no-decimal로 바꾸면 ₩1,980,000으로 찍힌다. 다만 통화별 설정이 아니라 전체 설정이라 달러 인보이스도 같이 소수점이 사라진다. 통화 설정의 소수 단위 값은 표기에 영향을 주지 않는다.

사업자등록번호는 값에 라벨을 넣어서 쓴다. 전용 필드는 없다. 업체의 Company code 필드가 PDF 머리에 라벨 없이 값만 찍히므로 사업자등록번호 123-45-67890처럼 라벨까지 적어 넣으면 PDF에 그대로 나온다.

전자세금계산서 연동은 없다

코드 전체에 국세청이나 홈택스 관련 구현이 없다. 있는 전자 인보이스 표준은 UBL 2.1, Peppol BIS Billing 3.0, XRechnung이다. 전부 유럽용이고 한국 체계와 호환되지 않는다.

한국에서는 직전연도 사업장별 공급가액 합계가 8천만원 이상인 개인사업자가 전자세금계산서 의무발급 대상이다. 이 도구는 세금계산서 발급 수단이 될 수 없다. 홈택스나 ERP·ASP 대행을 반드시 병행해야 한다.

그래서 현실적인 용도는 이렇다. 견적서, 거래명세서, 해외 고객용 인보이스. 세금계산서는 별도 경로로 발급한다.

오픈소스 인보이스 도구의 표준 지원이 EU 규제를 따라 움직인다는 방증이기도 하다. Peppol은 완비하고 한국 체계는 언급조차 없다.

부가세 처리는 충분한데 주의가 필요하다

기능 자체는 한국 VAT 구조를 표현할 수 있다. 별도(exclusive), 포함(inclusive), 차감(deducted)을 지원하고 품목별과 합계 기준을 고를 수 있다.

다만 처음 테스트에서 품목 세율 10%와 인보이스 세율 10%를 둘 다 넣었더니 합계란이 공급가의 20%로 합산 표기됐다. 두 번째 테스트에서 인보이스 단위 10%만 넣고 품목 세율을 0으로 두니 ₩1,800,000에 부가세 ₩180,000으로 정확히 나왔다. 한쪽 레벨만 쓰면 된다.

세금계산서 필수 기재사항인 사업자등록번호 전용 필드가 없다. 위에 적은 대로 Company code에 라벨까지 넣어 쓰는 게 현실적인 방법이다.

그래도 배울 게 있는 설계

스냅샷 데이터 모델이 인상적이다. 인보이스를 만들 때 사업자, 고객, 품목, 통화, 레이아웃을 전부 복사해 함께 저장한다. 그래서 나중에 마스터 데이터를 고쳐도 과거 문서는 그대로 남는다. DB를 열어 보니 스냅샷 테이블이 아홉 개 있었다.

회계 감사 추적성을 오픈소스 앱이 스키마 수준에서 구현한 예다. 인보이스와 견적은 내보내기만 되고 가져오기가 안 되는데, 그것도 같은 이유로 보인다. 과거 문서를 손대지 못하게 막는 것이다.

PDF 쪽 기능도 꽤 갖춰져 있다. 드래그앤드롭 레이아웃 빌더, 워터마크, 서명(업로드와 손그림), 결제 QR코드, 80mm 감열 영수증 출력까지 있다. UI 언어는 영어, 프랑스어, 독일어, 리투아니아어, 포르투갈어 다섯 개다. 한국어는 없다. i18n 파일 추가로 기여할 수 있는 부분이다.

정리

해외 고객에게 인보이스를 보내는 프리랜서라면 지금 그대로 쓸 만하다. 데이터가 내 파일에 남는다는 점도 분명한 장점이다.

국내 거래가 중심이면 두 가지를 먼저 해결해야 한다. PDF 한글은 폰트를 넣는 포크 작업으로 풀린다. 직접 해 보니 코드 여섯 군데, 반나절 거리다. 세금계산서는 도구가 풀 문제가 아니라 별도 경로로 발급하는 운영 방식으로 가야 한다. 포크를 감당할 수 있다면 견적서·거래명세서 도구로는 충분하다.

오픈소스를 도입할 때 "한국어가 안 된다"로 끝내는 경우가 많다. 이번처럼 원인이 폰트 등록 여섯 줄이면 고쳐 쓰는 쪽이 빠르다. 원인이 어디 있는지 확인하는 게 먼저다.

저장소: piratuks/invoice-builder · 2026-09-18에 v2.9.0을 소스 빌드해 확인했고, 2026-09-19에 한글 폰트 포크를 빌드해 PDF를 다시 뽑았다. 공식 amd64 이미지는 애플 실리콘에서 실행되지 않는다.

15초 요약

인스타그램 릴스로도 올린 영상입니다. HeyGen HyperFrames 로 이 글에서 바로 렌더했습니다.

CONTACT

컨설팅 · 강의 문의

회사 메일로 남겨 주시면 구성과 비용을 안내드립니다.

gmail·naver 같은 개인 메일은 받지 않습니다.