Go로 50줄 이하에서 인보이스 PDF 생성하기
실행 가능한 Go 인보이스 PDF를 50줄로. gpdf 하나로 의존성 제로, Chromium/CGO 불필요. 헤더, 테이블, 합계까지 모두 포함.
TL;DR
실행 가능한 Go 인보이스 PDF를 처음부터 끝까지 50줄로. main.go 하나, go get 한 번, Chromium 없음, CGO 없음, 템플릿 언어 없음, HTML 없음. 테이블, 줄무늬 행, 우측 정렬 합계 포함. 코드는 아래에 있고, 나머지는 각 블록의 역할과 이 패턴이 한계에 부딪히는 조건에 대한 이야기다.
먼저 코드만 보려면:
go get github.com/gpdf-dev/gpdf
그런 다음 다음 섹션의 main.go를 붙여넣는다.
왜 "50줄 이하"가 우리가 관심 두는 기준인가
솔직하게 말하면, "go 인보이스 pdf 생성"을 검색하면 (a) 헤드리스 Chromium을 띄우라거나, (b) 테이블 하나 그리려고 저수준 PDF 연산자 400줄을 보여주는 글이 대부분이다. 둘 다 기술적으로 틀리지 않았지만, 일의 형태와 맞지 않는다.
괜찮은 인보이스에 필요한 것:
- 발행자와 고객 정보가 담긴 헤더
- 인보이스 번호와 만기일
- 명세행 테이블
- 합계
네 가지. 그래서 코드도 네 블록이어야 한다. 한 화면에 안 들어가면 라이브러리 선택이 잘못된 것이다.
50줄은 일반 에디터에서 한 화면에 들어가는 한계점이며, 리뷰어가 테스트로 건너뛰지 않고 처음부터 끝까지 읽는 기준점이다. 이 선을 넘으면 결과물을 Slack 메시지 하나에 붙일 수 있고, 그 메시지만으로 누군가가 라이브러리를 익힐 수 있다.
아래 코드는 gofmt 적용, import 완전 전개, 모든 에러 경로 처리. 숨겨진 헬퍼 패키지도 꼼수도 없다. 보이는 그대로 컴파일된다.
50줄
package main
import (
"log"
"os"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/pdf"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
doc := gpdf.NewDocument(template.WithPageSize(document.A4))
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(6, func(c *template.ColBuilder) {
c.Text("ACME 주식회사", template.FontSize(22), template.Bold())
c.Text("서울특별시 강남구 테헤란로 100")
})
r.Col(6, func(c *template.ColBuilder) {
c.Text("인보이스 #INV-2026-001", template.Bold(), template.AlignRight())
c.Text("만기일: 2026-03-31", template.AlignRight())
})
})
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Spacer(document.Mm(6))
c.Table(
[]string{"항목", "수량", "단가", "금액"},
[][]string{
{"프런트엔드 개발", "40시간", "₩200,000", "₩8,000,000"},
{"백엔드 개발", "60시간", "₩200,000", "₩12,000,000"},
{"UI/UX 디자인", "20시간", "₩170,000", "₩3,400,000"},
},
template.ColumnWidths(40, 15, 20, 25),
template.TableHeaderStyle(template.Bold(), template.BgColor(pdf.RGBHex(0xF0F0F0))),
template.TableStripe(pdf.RGBHex(0xFAFAFA)),
)
c.Text("합계: ₩23,400,000", template.AlignRight(), template.Bold(), template.FontSize(14))
})
})
b, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("invoice.pdf", b, 0644); err != nil {
log.Fatal(err)
}
}
주의: 위 코드의 한글은 기본 폰트에서 두부 블록 (□)으로 표시된다. 한글 폰트 임베딩은 마지막 "한글 지원" 섹션에서 다룬다. 먼저 구조를 보자.
go run .으로 현재 디렉터리에 invoice.pdf가 생성된다. M1에서 프로그램 전체는 몇 밀리초, 실제 PDF 생성은 150 µs 미만이다.
각 블록이 하는 일
import
gpdf의 4개 패키지:
github.com/gpdf-dev/gpdf— 파사드.gpdf.NewDocument만 쓰며, 내부는template.New의 얇은 래퍼.github.com/gpdf-dev/gpdf/document— 단위 (Mm,Pt,Cm,In,Em,Pct), 용지 크기 (A4,Letter,Legal), 여백.github.com/gpdf-dev/gpdf/pdf— 색상 프리미티브 (RGBHex,Gray,pdf.White등 상수).github.com/gpdf-dev/gpdf/template— Builder API 본체.
외부 의존성 제로. go get github.com/gpdf-dev/gpdf 후 go.mod의 require는 한 줄.
문서 구성
doc := gpdf.NewDocument(template.WithPageSize(document.A4))
gpdf.NewDocument는 ...template.Option 가변 인자. 페이지 크기, 여백, 기본 폰트, 메타데이터, 커스텀 폰트 모두 WithXxx 옵션. 기본 여백은 20 mm.
헤더 행
page.AutoRow(func(r *template.RowBuilder) {
r.Col(6, func(c *template.ColBuilder) { ... })
r.Col(6, func(c *template.ColBuilder) { ... })
})
gpdf는 12컬럼 그리드를 채택했다. Bootstrap과 동일한 멘탈 모델. 한 행은 수평으로 12 단위, r.Col(6, ...)은 절반. Col(6) 두 개로 행이 꽉 찬다.
AutoRow는 행 높이를 가장 높은 컬럼 기준으로 결정한다. 컬럼 안에서 c.Text(...)를 세로로 쌓는다. 명시적 좌표 지정 없이 빌더가 커서를 관리한다.
우측 컬럼은 template.AlignRight()로 우측 정렬. 텍스트 옵션은 합성 가능해서 c.Text("인보이스", template.Bold(), template.AlignRight(), template.FontSize(20))처럼 한 호출에 여러 수정자를 겹칠 수 있다. 순서 무관.
명세 테이블
c.Table(
[]string{"항목", "수량", "단가", "금액"},
[][]string{ /* rows */ },
template.ColumnWidths(40, 15, 20, 25),
template.TableHeaderStyle(template.Bold(), template.BgColor(pdf.RGBHex(0xF0F0F0))),
template.TableStripe(pdf.RGBHex(0xFAFAFA)),
)
ColumnWidths는 백분율이며 절대 포인트가 아니다. 네 수의 합은 100이어야 한다. 합이 맞지 않아도 에러는 아니지만 우측이 넘칠 수 있다 — 유일한 함정.
TableHeaderStyle은 모든 텍스트 옵션을 받는다. TableStripe(color)는 얼룩말 줄. 테이블이 자동으로 행 높이를 측정하고 페이지를 분할한다 — 연속 페이지 상단에 헤더를 다시 그린다.
합계
c.Text("합계: ₩23,400,000", template.AlignRight(), template.Bold(), template.FontSize(14))
테이블 밖의 또 다른 Text 호출. 우측 정렬, 약간 더 큰 글씨.
생성과 쓰기
b, err := doc.Generate()
if err != nil { log.Fatal(err) }
if err := os.WriteFile("invoice.pdf", b, 0644); err != nil { log.Fatal(err) }
doc.Generate()는 ([]byte, error)를 반환하며 파일 시스템을 건드리지 않는다. 바이트 슬라이스 자체가 완전한 PDF로, 디스크 쓰기, S3 업로드, HTTP 응답 w.Write(b), 메일 첨부 모두 가능. 스트리밍이 필요하면 doc.Render(w io.Writer).
한글 지원
기본 폰트는 Latin만 지원하므로 한글은 두부로 렌더된다. Noto Sans KR TTF 하나로 해결:
ttf, err := os.ReadFile("NotoSansKR-Regular.ttf")
if err != nil { log.Fatal(err) }
doc := gpdf.NewDocument(
template.WithPageSize(document.A4),
template.WithFont("NotoSansKR", ttf),
template.WithDefaultFont("NotoSansKR", 10),
)
gpdf가 자동으로 폰트 서브셋화를 수행하여 실제 사용된 글리프만 PDF에 임베드된다. 3 MB TTF 전체가 들어가지 않는다.
50줄을 넘지 않으면서 보기 좋게
브랜드 색상. hex 값 (예: 네이비 0x1A237E)을 회사명과 테이블 헤더에 적용:
brand := pdf.RGBHex(0x1A237E)
c.Text("ACME 주식회사", template.FontSize(22), template.Bold(), template.TextColor(brand))
template.TableHeaderStyle(template.Bold(), template.TextColor(pdf.White), template.BgColor(brand)),
소계와 세금. 합계 위에 분리 표시:
c.Text("소계: ₩23,400,000", template.AlignRight())
c.Text("부가세 (10%): ₩2,340,000", template.AlignRight())
c.Text("합계: ₩25,740,000", template.AlignRight(), template.Bold(), template.FontSize(14))
전자세금계산서 대응. 발행자의 사업자등록번호, 공급받는 자의 상호와 등록번호는 c.Text 몇 줄 추가로 해결. 레이아웃 변경 없음.
실행
mkdir invoice-demo
cd invoice-demo
go mod init example.com/invoice-demo
go get github.com/gpdf-dev/gpdf
# main.go 붙여넣기
go run .
이 패턴이 깨지는 지점
- 명세가 데이터가 될 때. DB 쿼리나 JSON 페이로드에서 올 때 테이블 코드는 그대로.
[][]string구성 로직만 추가. - 레이아웃 재사용이 필요할 때. 루프로 여러 인보이스를 생성한다면 본문을
func renderInvoice(doc *template.Document, inv Invoice)로 분리. - 레이아웃에 분기가 생길 때. Builder API가 장황해지기 시작하면 JSON 스키마 엔트리나 Go 템플릿 엔트리가 적합.
- CJK 텍스트가 필요할 때. 위 "한글 지원" 섹션 참조.
모두 처음부터 다시 쓸 필요 없이 증분 확장.
FAQ
상용 인보이스에 써도 되나요? 됩니다. gpdf는 MIT 라이선스. 클로즈드 상용에도 무료로 임베드 가능, 표기 의무 없음.
바이트 슬라이스 없이 io.Writer에 직접 쓸 수 있나요?
네. doc.Render(w io.Writer) error.
실제로 얼마나 빠른가요? 위 50줄은 M1에서 약 100 µs에 PDF를 생성. 단일 페이지 hello world는 13 µs. 배치 워크로드에서 gpdf가 병목이 되는 경우는 드물다.
gpdf.Invoice 헬퍼는 없나요?
없습니다. 나라마다 인보이스 양식이 달라 어떤 단순화도 누군가를 배제한다. 50줄 출발점이 컨스트럭터 하나보다 유연.
PDF/A를 지원하나요?
PDF/A-2b 지원. 구성 시 gpdf.WithPDFA(pdfa.Level2B) 전달. 순수 Go로 PDF/A-2b 만들기 참조.
gpdf 사용해보기
gpdf는 Go용 PDF 생성 라이브러리. MIT, 의존성 제로, CJK 네이티브 지원, 벤치마크 워크로드에서 다른 라이브러리 대비 10–30배 빠름.
go get github.com/gpdf-dev/gpdf
다음 읽을거리
- 왜 gpdf가 다른 Go PDF 라이브러리보다 10–30배 빠른가 — 위 "수백 마이크로초" 주장의 벤치 수치.
- 2026 Go PDF 라이브러리 쇼다운 — 같은 인보이스를 gofpdf / gopdf / Maroto로 쓰면 몇 줄인가.
- gpdf의 12컬럼 그리드는 어떻게 동작하는가 — 위 헤더 행에서 사용한 레이아웃 모델의 깊은 설명.