gpdf vs wkhtmltopdf vs Chromium — 2026년 PDF 생성 비교
wkhtmltopdf은 아카이브됐다. Chromium은 요청당 170 MB. gpdf는 13 µs/페이지, 브라우저 없이. 2026년 솔직한 비교.
TL;DR
wkhtmltopdf은 2023년 1월에 아카이브됐다. 헤드리스 Chromium (Puppeteer / Playwright / chromedp / go-rod)은 동작하지만 약 170 MB의 브라우저 바이너리를 동봉해야 하고, 동시 요청마다 RSS 50–120 MB를 차지하며, 콜드 스타트 300–800 ms가 추가된다. gpdf는 13 µs/페이지로 PDF를 만든다. 의존성 0, 헤드리스 브라우저 불필요 — 단, 임의의 HTML+CSS는 렌더링하지 않는다.
이 글의 판단 기준: "디자이너가 Tailwind 페이지를 넘기고 pixel-perfect로 뽑아달라"는 요구라면 여전히 Chromium이 정답. "청구서, 명세서, 보고서, 인증서, 라벨"이라면 네이티브 경로는 비용 카테고리 자체가 다르다.
입장 공개: 우리는 gpdf를 만드는 쪽이다. 벤치마크 코드는 공개되어 있고, 트레이드오프 섹션에서는 무엇을 포기했는지 명시하며, 유스케이스 표에서도 gpdf가 모든 곳에서 이긴다고 주장하지 않는다.
세 가지 아키텍처를 나란히
| 접근 방식 | 대표 도구 | 렌더링 엔진 | 바이너리 크기 | 요청당 RSS | 콜드 스타트 | 라이선스 |
|---|---|---|---|---|---|---|
| wkhtmltopdf | wkhtmltopdf CLI | QtWebKit fork (~2014) | ~40 MB | ~30–80 MB | ~150 ms | LGPLv3 |
| Chromium 계열 | Puppeteer / Playwright / chromedp / go-rod | Blink + V8 (진짜 Chromium) | ~170 MB | ~50–120 MB | ~300–800 ms | BSD + 재배포 제약 |
| 네이티브 (gpdf) | gpdf / signintech/gopdf / gofpdf† | 순수 Go PDF Writer | 의존성 0 | ~2–10 MB | 0 ms | MIT |
† gofpdf와 go-pdf/fpdf는 모두 아카이브됨. Go 생태계 전반은 2026년 Go PDF 라이브러리 비교를 참조.
표를 설명하기 전에 세 가지를 짚고 가자.
첫째, wkhtmltopdf의 "작은 바이너리"는 오해를 부른다. 바이트 수가 작은 이유는 그 WebKit fork가 10년 넘게 업스트림을 추적하지 않았기 때문이다. CVE 백로그는 작지 않다.
둘째, Chromium은 PDF 라이브러리가 아니다. 우연히 인쇄도 되는 브라우저다. 그 열의 모든 비용은 브라우저 비용이다.
셋째, "0 ms vs 300 ms 콜드 스타트"의 간극은 한 시간에 한 번 PDF를 만드는 상주 서버에는 무의미하다. 그러나 서버리스 (Lambda / Cloud Run / Workers)와 "1,000개 PDF를 가장 빠르게" 같은 배치 작업에서는 사활이 걸린 문제다.
2026년의 wkhtmltopdf
이 섹션은 안 읽어도 될 수도 있다. 이미 wkhtmltopdf에서 빠져나왔다면 다음 섹션으로 가세요.
아직 안 그런 분들에게: wkhtmltopdf 개발은 사실상 2022년에 멈췄고, 저장소는 2023년 1월에 아카이브됐다. 메인테이너의 작별 노트는 대체재로 Chromium을 권한다고 명시했다. 이유는 인프라 차원이었다. wkhtmltopdf의 렌더러는 QtWebKit이라는 WebKit 포크인데, 대략 2014년부터 업스트림을 따라가지 않았다. Qt 자체도 2016년에 QtWebKit을 폐기하고 Chromium 기반 QtWebEngine으로 전환했다. wkhtmltopdf가 지금도 쓰고 있는 포크는 12년 된 브라우저 엔진이다.
구체적으로 말해, 모던 CSS — 완전한 flex 스펙, grid, 대규모의 CSS 커스텀 프로퍼티, aspect-ratio, :has(), container queries, flex의 gap, 모던 color 함수 — 는 잘못 렌더링되거나 아예 렌더링되지 않는다. @font-face 웹 폰트는 대부분 동작하지만 가변 축이 있는 웹 폰트는 안 된다. SVG 지원은 부분적. WOFF2 지원은 늦게 추가됐고 버그가 많다.
따라서 2026년에 "wkhtmltopdf을 쓴다"는 말은 두 가지 의미 중 하나이고, 둘 다 곤란하다.
패치되지 않은 WebKit 코드가 포함된 업스트림 버전을 쓰고 있다. 보안 팀이 결국 이를 지적할 것이며, "프로젝트가 아카이브됐다"는 완화 계획이 아니다. 마지막 릴리스는 2020년. 그 이후의 CVE 작업은 업스트림이 아니라 리눅스 배포판이 각자 백포트하고 있다.
사설 포크를 유지하고 있다. Qt와 WebKit 소스를 읽고 패치를 백포트하고 배포 대상 플랫폼마다 다시 빌드할 사람이 필요하다. 실제 사례를 본 적 있다. 비용은 "차라리 다른 일을 하고 싶은" 엔지니어 한 명의 풀타임이었다.
마이그레이션 질문은 Chromium (높은 충실도, 높은 비용)으로 갈지 네이티브 PDF 생성기 (낮은 비용, HTML/CSS 없음)로 갈지다. 이 글의 나머지 주제다.
Chromium 기반 PDF 생성의 실제 비용
헤드리스 Chromium은 정말로 브라우저가 필요할 때 옳은 도구다. 비용은 네 군데에서 발생한다.
바이너리. Chromium 자체가 ~170 MB. Playwright는 검증된 빌드를 번들로 동봉하고, Puppeteer는 설치 시 다운로드한다 (세 브라우저 합쳐 ~280 MB). 컨테이너 이미지에서는 가장 큰 레이어가 된다. 다른 레이어보다 한 자릿수 더 크다. Lambda zip의 250 MB 상한 안에서는 이 항목만으로 거의 가득 찬다.
프로세스당 메모리. 갓 시작한 Chromium 프로세스의 RSS는 ~50 MB. 사소하지 않은 페이지 (실제 CSS, 웹 폰트, 이미지 몇 장)를 로드하면 80–120 MB까지 올라간다. 페이지에 따라 변동하지만, 하한선은 변하지 않는다.
콜드 스타트. Chromium을 띄우고 about:blank로 가는 것만으로 따뜻한 머신에서 ~300 ms. await page.goto(url) + 실제 페이지 로드 + 폰트 패치 + await page.pdf()를 추가하면 첫 요청에서 보통 500 ms ~ 2초. 풀로 따뜻하게 유지하면 도움이 되지만, 서버리스에서는 의미가 없다. 스케일업 이벤트마다 콜드 스타트를 지불한다.
운영 표면. 브라우저는 본래 하기로 결정하지 않았던 결정들의 대륙이다. CSP를 어떻게 다룰지, networkidle을 기다릴지 load를 기다릴지 domcontentloaded를 기다릴지, JS를 비활성화할지, Docker에서 --disable-dev-shm-usage를 어떻게 설정할지, 브라우저 프로세스가 누수되면 어떻게 할지. 각각은 어렵지 않다. 하지만 전부 합치면, 차라리 하고 싶지 않은 디버깅이다.
솔직한 반론도 있다: 충실도가 필요할 때는 정말로 필요하다. 디자이너가 Figma 익스포트와 Tailwind 페이지를 넘기면서, 커스텀 폰트와 그라데이션과 SVG 아이콘이 그대로 출력돼야 한다고 한다 — 이건 Chromium의 일이다. 선언적 문서 API로 우격다짐하면 일주일을 태우고 첫 리뷰에서 디자이너에게 반려된다.
그래서 질문은 "Chromium을 쓸까 말까"가 아니다. "내가 렌더링하는 게 정말 웹페이지인가?"이다.
gpdf: 브라우저 없는 네이티브 렌더링
gpdf는 세 번째 카테고리 — 순수 Go PDF Writer다. HTML도, CSS도, 헤드리스 브라우저도 없다. 문서를 Go (또는 JSON, 또는 Go 템플릿)로 기술하면 PDF 바이트가 바로 나온다.
package main
import (
"os"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
doc := gpdf.NewDocument(
gpdf.WithPaperSize(document.A4),
gpdf.WithMargin(document.Mm(20)),
)
doc.AddPage(func(p *template.PageBuilder) {
p.Row(document.Mm(12), func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("청구서", template.FontSize(24), template.Bold())
})
})
p.Row(document.Mm(8), func(r *template.RowBuilder) {
r.Col(6, func(c *template.ColBuilder) {
c.Text("Acme 주식회사", template.FontSize(11))
})
r.Col(6, func(c *template.ColBuilder) {
c.Text("INV-2026-0517", template.FontSize(11), template.AlignRight())
})
})
// 이하 명세 행 + 합계
})
out, _ := os.Create("invoice.pdf")
defer out.Close()
doc.Write(out)
}
스택 전부 이것뿐이다. 컨테이너에 Chromium 바이너리 없음, npm install puppeteer 없음, page.goto 없음. Write 호출이 PDF를 writer에 곧장 쓴다. 1페이지 청구서면 CPU 시간 ~13 µs.
그러기 위해 포기한 것: 렌더러는 display: flex가 무슨 뜻인지 모른다. 아는 건 행, 열 (12 컬럼 그리드), 텍스트 런, 이미지, 표, 바코드뿐. 대규모로 생성되는 대부분의 문서 — 청구서, 명세서, 영수증, 보고서, 인증서, 라벨, 배송표 — 는 이 어휘로 충분하다. 나머지 (마케팅 PDF, 디자이너 주도의 브로셔, 원래 웹페이지였던 것)는 그렇지 않다.
성능 비교
세 카테고리를 함께 벤치마킹하는 건 방법론적으로 골치 아프다. 각자 조금씩 다른 문제를 푸니까. 그래도 한다. 공정한 비교는 "같은 최종 산출물, 세 가지 구현": 헤더 + 4×10 명세표 + 합계가 있는 한 페이지 청구서.
| 작업 부하 | gpdf | wkhtmltopdf (CLI) | Chromium (Playwright page.pdf()) |
|---|---|---|---|
| 1페이지 청구서 | 13 µs | ~140 ms | ~280 ms (warm) / ~1.2 s (cold) |
| 100페이지 분쇄 보고서 | 683 µs | ~3.4 s | ~6.1 s (warm) |
| 단일 요청 중 최대 RSS | ~5 MB | ~70 MB | ~120 MB |
| 컨테이너 이미지 크기 영향 | 0 | +40 MB | +170 MB |
Apple M1, Go 1.25 (gpdf 측), wkhtmltopdf 0.12.6 바이너리, Playwright 1.42 + 번들 Chromium. gpdf 벤치마크 코드는 _benchmark/ — 클론해서 본인 하드웨어에서 재현 가능.
곱씹어볼 숫자가 두 개 있다.
1페이지 청구서의 차이는 약 22,000배. 대부분은 렌더링 자체가 아니라 요청마다 브라우저 프로세스를 시작하고 종료하는 비용이다. Playwright 풀을 따뜻하게 유지하면 ~4배로 줄어든다. 그래도 4자릿수 차이.
100페이지 보고서의 차이는 약 9,000배. 여기서는 렌더링 비용이 지배적이 되고, "브라우저 시작" 같은 고정 비용은 분산된다. 분산된 뒤에도 Chromium은 요소마다 레이아웃 비용을 낸다. 네이티브 PDF Writer는 그걸 건너뛴다.
프로덕션에서 진짜로 무는 건 최대 RSS 행이다. Chromium 프로세스 하나가 6초 작업 동안 ~120 MB를 잡고 있다 = 4 GB 컨테이너가 동시 보고서 약 30개. 같은 컨테이너에서 gpdf를 돌리면 동시 수천 개.
각 접근 방식이 이기는 곳
"gpdf가 모든 것을 이긴다"는 표가 아니다. 그런 표가 아니어야 한다. 실제 아키텍처 결정은 이렇게 생겼다.
| 유스케이스 | 올바른 도구 | 이유 |
|---|---|---|
| Figma + Tailwind 기반 마케팅 PDF | Chromium (Playwright) | 디자이너 의도에 대한 충실도가 비용보다 중요. |
| 월말 50,000건의 월간 명세서 | gpdf | 건당 비용 × 수량 = 실제 돈. CSS 불필요. |
| 일회성 "디자이너가 브로셔 요청" | Chromium (or InDesign) | 수량 적고 CSS 많음. 한 번이면 옳은 도구 쓰기. |
| SaaS 과금 시스템의 청구서 | gpdf | 매출에 따라 수량 증가. 콜드 스타트 중요. 레이아웃이 구조적. |
| 세무 양식 / 규제 신고 (PDF/A) | gpdf (or unidoc) | PDF/A 적합, 서명, 감사 추적. 브라우저가 다루지 않음. |
| 차트 스크린샷이 있는 BI 대시보드 보고서 | Chromium | 차트가 핵심. PDF는 내보내기 수단. |
| "Markdown을 인쇄" / 문서 PDF | gpdf 또는 Chromium | 둘 다 가능. 비용과 충실도의 거래. |
| 레거시 wkhtmltopdf 마이그레이션 | HTML이 단순하면 gpdf, CSS가 진짜면 Chromium | 템플릿을 먼저 감사. |
패턴: 수량 × 요청당 비용 vs 디자인 충실도. 전자가 지배적이면 네이티브가 이긴다. 후자가 지배적이면 Chromium이 이긴다. wkhtmltopdf은 2026년의 이 매트릭스 어디에도 자리가 없다.
모른 척하지 않는 트레이드오프
이 글 내내 깔아뒀지만, 절을 따로 두고 말한다.
gpdf는 HTML이나 CSS를 렌더링하지 않는다. 기존 시스템이 "HTML 메일 템플릿을 PDF로도 인쇄한다"라면, gpdf로 옮기는 건 그 템플릿을 gpdf builder API로 다시 쓰는 일이다. 템플릿 하나면 오후 한나절이면 된다. 디자이너가 유지보수하는 마케팅 템플릿 30개 라이브러리라면, 그건 프로젝트다.
@font-face 웹 폰트도 렌더링하지 않는다. TTF/OTF 파일을 문서 생성 시점에 gpdf에 건넨다. CJK 폰트는 first-class다 — CGO 없이 CJK를 렌더링하는 이유를 따로 적었다 — 하지만 폰트 파일 배포는 개발자 책임이다.
타협하지 않는 것: 속도, 메모리, 배포 용이성, 의존성 풋프린트. 트레이드오프는 기능 표면적으로 지불한다. 프로덕션 비용으로 지불하지 않는다. 우리 판단으로는, 대규모 구조화 문서를 생성하는 많은 팀이 필요 없는 브라우저에 돈을 내고 있다. 그런 팀에게는 네이티브 경로가 정답이다. 모든 팀에게 gpdf가 정답이라고는 생각하지 않는다.
코드: 같은 청구서, 세 가지 방식
API 차이를 직접 느끼고 싶다면, 세 구현을 나란히 둔다.
Chromium (Playwright, Node):
const { chromium } = require('playwright');
const fs = require('fs');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const html = fs.readFileSync('invoice.html', 'utf8');
await page.setContent(html, { waitUntil: 'networkidle' });
await page.pdf({
path: 'invoice.pdf',
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '20mm', right: '20mm' },
});
await browser.close();
})();
여기에 직접 유지보수하는 invoice.html, 번들된 Chromium 바이너리 (~170 MB), 폰트 로딩 방법 (웹 폰트? base64 내장? --font-render-hinting?). Tailwind 템플릿에서는 아름답게 동작한다. 유지보수 대상은 HTML이다.
wkhtmltopdf (shell):
wkhtmltopdf --enable-local-file-access \
--margin-top 20mm --margin-bottom 20mm --margin-left 20mm --margin-right 20mm \
invoice.html invoice.pdf
여기에 wkhtmltopdf 바이너리, QtWebKit-2014가 이해하지 못하는 CSS를 피한 HTML 템플릿 (실무상: grid 불가, flex 조심, :has() 불가, 커스텀 프로퍼티는 부분적). 그리고 바이너리가 감사에 걸렸을 때의 보안 대화.
gpdf (Go):
doc := gpdf.NewDocument(
gpdf.WithPaperSize(document.A4),
gpdf.WithMargin(document.Mm(20)),
)
doc.AddPage(func(p *template.PageBuilder) {
invoiceHeader(p, "INV-2026-0517", "Acme 주식회사")
invoiceTable(p, lineItems)
invoiceTotals(p, subtotal, tax, total)
})
out, _ := os.Create("invoice.pdf")
defer out.Close()
doc.Write(out)
여기에 builder API에 대해 직접 쓴 Go 함수 세 개. 템플릿 파일 없음, 바이너리 의존성 없음, 별도 렌더 단계 없음. 단일 Go 바이너리로 FROM scratch 컨테이너에 배포 가능.
올바른 독해는 "어느 게 가장 짧은가"가 아니다. "어떤 표면적을 유지보수하고 싶은가"다. Chromium의 표면적은 HTML + CSS + 브라우저. wkhtmltopdf의 표면적은 HTML + CSS + 10년 된 브라우저. gpdf의 표면적은 Go.
FAQ
2026년에 wkhtmltopdf은 정말 못 쓰나?
"못 쓴다"는 너무 세다. "권하지 않는다"가 정확하다. 여전히 동작하고, 단순한 템플릿에 대해서는 정확한 PDF를 만든다. 새 프로젝트에서 채택하지 말아야 할 이유: 프로젝트가 아카이브됐고, WebKit fork는 2014년 코드베이스이며, 보안 감사가 반드시 잡을 것이고, 공식 대체 가이드가 "Chromium을 쓰라"이기 때문. 이미 프로덕션에 있다면 마이그레이션할 시간은 있다. 새로 의존성을 추가할 시간은 없다.
Chromium 비용을 그냥 받아들이면 안 되나?
대부분의 워크로드에서는 그래도 된다. 위 의사결정 매트릭스도 마케팅 PDF와 디자이너 주도 문서는 명확히 Chromium 열에 둔다. 이 글이 존재하는 이유는 Chromium이 청구서, 명세서, 보고서에도 쓰이고 있기 때문이다 — 브라우저의 충실도가 필요 없는 워크로드. 그쪽에서 비용이 AWS 청구서로 드러난다.
Chromium 없이 HTML→PDF를 하는 도구 (html2pdf, jsPDF 같은) 는?
그건 브라우저 쪽 JS 라이브러리로, HTML을 canvas에 그린 다음 PDF로 만든다. 충실도는 Chromium보다 훨씬 나쁘고 (모던 CSS 대부분이 동작하지 않음), 성능도 네이티브보다 나쁘다 (두 번 렌더링: HTML → canvas → PDF). 자기만의 틈새 (서버 없이 브라우저에서 PDF를 만드는 경우)가 있지만, 이 비교 대상에는 포함되지 않는다.
gpdf은 PDF/A나 디지털 서명을 지원하나?
지원한다. gpdf.WithPDFA(...)로 PDF/A-1b 및 PDF/A-2b 적합, gpdf.SignDocument(...)로 PKCS#7 서명 (RFC 3161 타임스탬프 포함). 둘 다 MIT 코어 라이브러리에 포함 — 별도 패키지나 상용 라이선스 불필요.
gpdf은 다른 Go PDF 라이브러리 (브라우저 계열 아닌) 와 비교하면 어떤가?
다른 질문이다. 짧은 답변: gofpdf와 go-pdf/fpdf는 아카이브됨, signintech/gopdf는 유지보수되지만 저수준 (레이아웃 그리드 없음), Maroto v2는 유지보수되지만 아카이브된 gofpdf 위에 있음, unidoc은 상용. 전체 비교는 2026년 Go PDF 라이브러리 비교에서.
gpdf 시작하기
gpdf은 Go 용 PDF 라이브러리다. MIT, 의존성 0, 네이티브 CJK.
go get github.com/gpdf-dev/gpdf
이어 읽기
- 왜 gpdf은 다른 Go PDF 라이브러리보다 10–30배 빠른가 — 이 글 숫자 뒤의 아키텍처
- 2026년 Go PDF 라이브러리 비교 — Go 네이티브 라이브러리 간 비교
- gofpdf에서 gpdf로 마이그레이션 — 아카이브된 라이브러리에서 벗어나려면