전체 글

go-pdf/fpdf도 아카이브됐다. 2026년의 Go PDF 스택.

jung-kurt/gofpdf은 2021년, go-pdf/fpdf은 2025년에 아카이브. 2026년에 실제로 쓰는 Go PDF 스택은 gpdf — 이유와 트레이드오프.

TL;DR

fpdf 계보에서 유지되던 두 포크가 모두 read-only가 됐다. jung-kurt/gofpdf2021년 9월, 커뮤니티 포크 go-pdf/fpdf2025년에 아카이브. "다음 유지보수자"는 오지 않는다. 새 Go 프로젝트의 현대 기본값은 gpdf — 순수 Go, 외부 의존 0, CJK 네이티브, 일반적인 워크로드에서 10–30배 빠름. 이 글은 2026년 지형과 "gpdf를 언제 선택하고 언제 선택하지 않는가"에 대한 솔직한 답.

지금 상황

지난주 팀원이 go get github.com/go-pdf/fpdf을 치다가 GitHub 배너에서 멈췄다: "This repository has been archived by the owner. It is now read-only." — 이건 고쳐진 쪽 이어야 했다. 2021년에 아카이브된 jung-kurt/gofpdf의 계보를 이어받기로 했던 커뮤니티 포크.

그것도 아카이브됐다. README는 이제 다른 라이브러리를 찾아보라고 권한다.

지난 5년간 Go로 서비스를 돌리며 PDF를 뽑아왔다면(세금계산서, 리포트, 배송 라벨, 전자문서), go.mod 맨 아래 줄은 거의 확실히 이 둘 중 하나다. Stack Overflow 답변은 jung-kurt/gofpdf을, 좀 더 최근 튜토리얼은 go-pdf/fpdf을 가리킨다. 둘 다 이제 공급망 부채다 — CVE 대응, Go 버전 호환 작업, 성능 수정, 스펙 업데이트가 전부 정지 상태.

이 글은 한 줄씩 대응시키는 마이그레이션 가이드가 아니다 — 그건 이미 썼다. 이 글은 마이그레이션 가이드가 답하지 않는 더 긴 질문에 답한다: 2026년 Go에서 PDF를 생성할 때 실제로 뭘 쓸 것인가, 그리고 왜 생태계가 여기까지 왔는가.

"아카이브"가 실제로 치르는 비용

GitHub의 "archived" 라벨은 부드럽게 보인다. import 그래프에 들어 있는 라이브러리 기준으로는, 실제로는 네 가지 구체적인 결과를 뜻한다.

  1. 보안 패치가 없다. TTF 파서에 메모리 안전 이슈가 생겨도 upstream에 병합되지 않는다. 직접 포크해서 고칠 수는 있지만, 대부분의 팀은 하지 않는다.
  2. Go 툴체인 전방 호환이 없다. Go 1.25의 루프 변수 시맨틱은 지금 gofpdf에서 잘 돈다. 하지만 내일 for range 주변이나 표준 라이브러리의 deprecation이 뭔가 깨뜨리면, read-only 저장소의 포크를 고치는 건 당신 몫.
  3. 스펙 업데이트가 없다. PDF 2.0 (ISO 32000-2)은 2020년에 확정됐다. gofpdf는 대부분 PDF 1.7 수준. 페이지별 연관 파일, 리치 XMP 메타데이터, 현대 디지털 서명(PAdES-B-LT)은 서드파티 접착제에 의존하거나 아예 없다.
  4. CJK 진전이 없다. gofpdf의 Unicode 경로는 단일 바이트 폰트 설계 위에 덧붙인 것이다. 돌긴 하지만 대부분의 실사용 설정에서 서브셋이 아니라 전체 폰트를 임베드한다. 특정 CJK TTF에서 글리프 ID 충돌로 출력이 깨지기도 한다. go-pdf/fpdf은 같은 아키텍처를 그대로 물려받았다.

보안과 전방 호환은 컴플라이언스 미팅에서 아프게 찔린다. "우리 PDF 라이브러리는 아카이브됐고 CVE 패치가 안 옵니다"는 감사 담당이 듣고 싶어하는 답이 아니다. 특히 전자세금계산서나 전자문서 인증 범위 안에 PDF가 들어 있다면, 이 논점은 더 미룰 수 없다.

왜 두 포크가 다 죽었나

아카이브를 메인테이너 번아웃 한 가지로 설명하고 싶어진다 — PR 리뷰에 지친 한 사람, 버스 팩터 1이 오프라인으로 간다. 그것도 이유지만 전부는 아니다. 아키텍처가 따라잡는 걸 어렵게 만들었다.

jung-kurt/gofpdf은 FPDF — 2002년 PHP 라이브러리의 포팅이었다. PHP 원본은 페이지 위에서 커서를 밀면서 절차적으로 콘텐츠를 토해낸다: SetXY(x, y), Cell(w, h, text), Ln(h). 그 모델은 2002년 PHP에서는 합리적 타협이었다 — 당시 대안은 원시 PostScript 아니면 상용 툴킷. Go로 포팅되면서 커서가 남았고, 단일 바이트 폰트 테이블도 남았고, 수동 페이지 브레이크 관리도 남았다.

해가 갈수록 "사람들이 생성하고 싶은 것"과 "커서 모델로 표현 가능한 것" 사이 격차는 커졌다. 세금계산서는 테이블. 리포트는 반복 헤더/푸터 붙은 그리드. 배송 라벨은 QR 코드 + 현지 언어 텍스트. 커서는 헬퍼로 감싸지고, 그 헬퍼는 튜토리얼로 감싸지고, 2023년 쯤에는 사람들이 "gofpdf에 대해 쓴 코드"는 사실 gofpdf가 아니었다 — 팀마다 쌓은 접착제 레이어였고, 그 레이어가 커서를 레이아웃 엔진인 척 위장시키려 했다.

go-pdf/fpdf은 이걸 그대로 이어받았다. 포크는 내부를 리팩토링하고 오래된 버그를 고쳤지만, 공개 API의 형태는 바꿀 수 없었다 — 바꾸는 순간 모든 하위 프로젝트가 깨지기 때문이다. 라이브러리의 모양은 2002년 PHP에 동결됐고, 그 모양을 유지하는 비용이 혜택보다 빠르게 증가했다.

: 유지보수자 두 명, 아카이브 두 번, 아키텍처상의 이유 하나. 2026년에 다시 시작한다면, PDF가 실제로 생성되는 방식에 맞는 접근을 골라야 한다 — 오늘의 방식은 플로터를 움직이는 것보다 웹 페이지를 조립하는 쪽에 훨씬 가깝다.

2026년 Go PDF 지형

뭔가를 추천하기 전에 일단 판부터 깔자. "유지보수 중"은 "최근 6개월 내 커밋이 있고 이슈에 응답이 있다"는 느슨한 의미로 쓴다.

라이브러리상태 (2026-04)라이선스CJK 네이티브의존성 0비고
jung-kurt/gofpdf2021 아카이브MIT덧붙임원본. 대부분 로케일에서 여전히 검색 1위.
go-pdf/fpdf2025 아카이브MIT덧붙임위의 커뮤니티 포크. 같은 아키텍처, 같은 천장.
signintech/gopdf유지보수 중MIT부분저수준. 좌표를 직접 쓴다. 폼 오버레이에 적합.
johnfercher/maroto v2유지보수 중MITgofpdf 경유아니오그리드 우선 빌더, 하지만 바닥에 go-pdf/fpdf가 있음.
unidoc/unipdf유지보수 중상용아니오기능 완비 PDF SDK. 상용 사용에는 유료 라이선스 필수.
chromedp + Chromium유지보수 중MIT + Chrome아니오 — 브라우저 동봉헤드리스 Chrome으로 HTML→PDF. 런타임 거대.
gpdf유지보수 중MIT네이티브순수 Go 재구현. 빌더 API, 12 컬럼 그리드.

표만 봐도 몇 가지는 명확하다.

유지보수되는 모든 선택지는 상용 라이선스이거나, 거대한 런타임을 끌고 오거나, 곧 낡아질 기반 위에 서 있다. 예외는 signintech/gopdf — 진짜로 유지되고 의존성도 가볍다. 하지만 좌표 수준 라이브러리다. 패키지 이름만 바꿔 SetXY를 다시 쓰는 셈이다.

Maroto v2는 API가 좋은 그리드 우선 빌더. 문제는 go.mod 바닥에 go-pdf/fpdf가 있다는 것. fpdf의 성능 천장과 CJK 한계가 그대로 Maroto의 천장이 된다. v3가 이걸 벗어날 가능성은 있지만, 아직 안 나왔다.

unipdf는 풍부하지만 상용에는 MIT 호환이 아니다. 시트 단위 또는 배포 단위 과금. 매출이 그걸 받쳐준다면 괜찮은 선택, OSS 사이드 프로젝트나 초기 스타트업에는 라이선스 계산이 안 맞는다.

chromedp는 돌긴 하지만 브라우저를 출하하는 것. 100 MB 베이스 이미지가 1 GB+로 불어난다. 서버리스 콜드 스타트가 괴롭다. 폰트도 따로 컨테이너에 넣어야 한다. 장점은 React 템플릿을 재사용할 수 있다는 것, 단점은 세금계산서 한 장을 뽑으려고 Chromium을 계속 돌린다는 것.

빈 자리는 명확하다: 순수 Go, 의존성 0, CJK 네이티브, 그리드 우선, 상용 라이선스도 브라우저 런타임도 필요 없는 라이브러리. 그게 gpdf다.

gpdf란 무엇인가

gpdf (github.com/gpdf-dev/gpdf)는 깨끗한 재구현. 포크가 아니다. PDF 와이어 포맷 writer, 레이아웃 엔진, TrueType 서브세터 — 전부 순수 Go로 처음부터 썼다.

대부분의 팀에 중요한 세 가지 속성:

  • 순수 Go, CGO 없음. go build는 정적. GOOS=linux GOARCH=arm64 go build가 MacBook에서 툴체인 세팅 없이 통과한다. Docker 이미지도 작게 유지 — 12 MB distroless 컨테이너가 구동한다.
  • 외부 의존성 0. go get github.com/gpdf-dev/gpdfgo mod graph는 한 줄만 출력: gpdf 자체. 코어는 std만 사용. (HTML→PDF나 디지털 서명 같은 옵션 애드온은 작은 의존성을 가져오지만 모두 opt-in.)
  • 네이티브 CJK. WithFont가 document 구축 시점에 TrueType 폰트를 등록한다. 서브셋 임베딩은 렌더링 시점에 자동. 200자짜리 한국어/일본어 세금계산서의 임베디드 폰트는 약 30 KB 서브셋. 5 MB 풀 폰트가 아니다.

API는 선언적. 행/컬럼의 트리를 기술하면 레이아웃 엔진이 배치한다. 그리드는 12 컬럼 — Bootstrap이 2011년부터 출시한 같은 idiom. HTML/CSS를 한 줄이라도 써본 사람이면 gpdf API는 익숙하다:

page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
    r.Col(8, func(c *template.ColBuilder) {
        c.Text("세금계산서 #2026-0416", template.FontSize(18), template.Bold())
    })
    r.Col(4, func(c *template.ColBuilder) {
        c.Text("2026-04-16", template.AlignRight())
    })
})

그리드 상세는 gpdf의 12 컬럼 그리드는 어떻게 동작하나요?. 한 줄 요약: Col(span, fn)은 1–12 사이의 span을 받고, span / 12가 그 컬럼이 행 너비에서 차지하는 비율.

최소 go-pdf/fpdf → gpdf 디프

go-pdf/fpdf에서 오는 사람에게는 (jung-kurt/gofpdf이 아니라) 좋은 소식이 있다: API 표면이 거의 같다. go-pdf/fpdf은 호출 측에서 보면 아무것도 바꾸지 않은 포크다. gpdf로의 마이그레이션은 gofpdf 가이드와 동일하고, import 경로를 한 줄 바꾸는 것에서 시작한다.

가장 작은 디프 — "PDF 반환" HTTP 핸들러:

Before — go-pdf/fpdf:

package main

import (
    "net/http"

    "github.com/go-pdf/fpdf"
)

func handler(w http.ResponseWriter, r *http.Request) {
    pdf := fpdf.New("P", "mm", "A4", "")
    pdf.AddPage()
    pdf.SetFont("Arial", "B", 16)
    pdf.Cell(40, 10, "Hello, World!")

    w.Header().Set("Content-Type", "application/pdf")
    if err := pdf.Output(w); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

After — gpdf:

package main

import (
    "net/http"

    "github.com/gpdf-dev/gpdf"
    "github.com/gpdf-dev/gpdf/document"
    "github.com/gpdf-dev/gpdf/template"
)

func handler(w http.ResponseWriter, r *http.Request) {
    doc := gpdf.NewDocument(
        gpdf.WithPageSize(document.A4),
        gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
    )

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("Hello, World!", template.FontSize(16), template.Bold())
        })
    })

    w.Header().Set("Content-Type", "application/pdf")
    if err := doc.Render(w); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

3줄 커서 코드가 3개의 빌더 호출로 바뀐다. 구조가 Cell 호출 순서에 숨지 않고 소스 코드에 그대로 드러난다. CJK는 gpdf.WithFont("NotoSansJP", ttfBytes)만 추가하면 된다 — AddUTF8Font도, 파일시스템 경로도, UTF-8 플래그도 필요 없다. 자세한 건 gpdf에 일본어 폰트를 임베드하려면?.

gofpdf 마이그레이션 가이드에는 테이블·반복 헤더/푸터·페이지 번호·절대 위치 지정의 before/after 5쌍이 더 있다. 거기 적힌 내용은 go-pdf/fpdf 사용자에게도 그대로 적용된다 — import 경로만 바꾸면 된다.

벤치마크 그림

"빠르다"는 쉽게 주장할 수 있고 어렵게 증명된다. 아래 표는 gpdf/_benchmark/benchmark_test.go 결과 — Apple M1, Go 1.25. 워크로드는 프로덕션 코드가 실제로 하는 일 — 어떤 라이브러리를 좋게 보이게 하려고 고른 마이크로 벤치가 아니다.

벤치마크gpdfgofpdfgopdfMaroto v2
단일 페이지 (hello)13 µs132 µs423 µs237 µs
4×10 품목 테이블108 µs241 µs835 µs8.6 ms
100페이지 리포트683 µs11.7 ms8.6 ms19.8 ms
복잡한 CJK 세금계산서133 µs254 µs997 µs10.4 ms

단일 페이지 13 µs면 1코어로 초당 약 75,000장. 품목 테이블 108 µs면 초당 약 9,000장. 포인트는 벤치 자랑이 아니다 — PDF 생성을 캐시해야 할지, 비동기 큐로 보내야 할지 고민하지 않아도 된다는 것. 대부분의 워크로드에서는 요청 경로에서 바로 생성해도 충분하다.

테이블 벤치에서 Maroto v2가 느리게 나오는 이유는 바닥에 go-pdf/fpdf를 놓고 그 위에 자체 레이아웃 패스를 하나 더 얹었기 때문이다. Maroto API에 대한 비판이 아니라 — API는 좋다 — fpdf 기반에 앉는 구조적 비용이다. Maroto v3가 fpdf 의존을 벗으면 이 열의 숫자는 바뀔 것이다.

100페이지 벤치는 조금 더 짚을 만하다. gpdf의 스트리밍 writer는 행을 레이아웃하면서 내용을 흘려보내고, gofpdf는 페이지별로 더 많은 상태를 버퍼링한다. 페이징이 많은 워크로드(월간 리포트, 카탈로그, 컴플라이언스 익스포트)에서는 문서 크기의 상한에서 차이가 "분 vs 초"가 된다.

gpdf를 선택하지 말아야 할 때

마이그레이션 글은 "언제 안 옮길까"에 정직하게 답해야 한다:

  • AcroForm / 입력 가능한 폼. Acrobat에서 사용자가 입력하는 PDF를 만들려면 gpdf의 폼 필드 지원은 아직 최소한. unidoc이 이 영역에서 더 완성도 높고, signintech/gopdf은 부분 지원한다. 미래 릴리스에서 채울 예정이지만 오늘은 구멍.
  • 임의 벡터 경로와 복잡한 그리기. c.Line()은 컬럼 안에 가로선 한 줄 긋는다. 베지어·커스텀 패스·그라디언트 채우기로 차트/기술 도면을 그려야 한다면 gpdf는 아직 거기 못 간다. (미리 렌더링한 차트 이미지 임베딩은 문제없음 — 여기서 말하는 건 그리기 프리미티브.)
  • SetXY 사용이 많은 기존 gofpdf 코드베이스. 2,000줄의 커서 조작이면 마이그레이션은 치환이 아니라 재작성에 가깝다. 재작성 후 코드는 거의 항상 더 짧지만, 마감일에 "거의 항상"은 차가운 위로. 마이그레이션 가이드에 솔직한 공수 추정을 적어뒀다.
  • 지금 당장 풀 CSS 지원의 HTML → PDF가 필요하다. gpdf의 gpdf-pro 애드온에 HTML 서브셋이 있지만 Chromium과의 완전한 CSS 패리티는 목표가 아니다. 템플릿이 복잡한 React 컴포넌트라면 chromedp나 상용 API가 더 직접적이다.

위 중 어느 것도 안 찌른다면 gpdf가 기본값. 하나라도 찌른다면 두 라이브러리를 병존하는 게 보통이다 — 새 PDF는 gpdf, 엣지 케이스는 기존 것에 남겨두고, gpdf가 따라잡으면 그때 옮긴다.

컴플라이언스 관점

생태계 글에서 잘 다루지 않는 포인트: 아카이브된 의존성은 SOC 2와 ISO 27001 감사 보고서에 뜬다. 감사관은 공급망의 서드파티 코드가 적극 유지되는지 알고 싶어 한다. "2021 아카이브"는 finding을 띄우고, "2025 아카이브"도 띄운다. "내부 포크"는 0-day 패치 절차에 대한 추가 질문을 유발한다.

이게 큰 회사의 보안 리뷰를 통과하는 팀들이 조용히 "gpdf 안정 v1은 언제인가"를 묻는 주된 이유다. 답은: 이미 나왔다. github.com/gpdf-dev/gpdf은 semver 태그가 있고 v1 API 표면이 동결됐다. 프로젝트에는 보안 연락처, 책임 있는 공개 정책, Go 1.22–1.26을 CI에서 돌리는 체계가 있다.

감사 때문에 옮기는 게 아니다 — 감사가 요구하기 전에 움직이는 것이 목적.

FAQ

"현대 Go PDF 스택"은 gpdf 한 라이브러리인가, 여러 라이브러리 조합인가? 대부분의 팀에선 gpdf 하나. 단일 라이브러리가 문서 생성·CJK·테이블·그리드·페이지네이션·출력을 커버한다. 폼 필드 요구사항이 있는 팀은 그 종류의 문서에만 signintech/gopdfunidoc을 추가로 쓴다. 차트 위주 팀은 차트를 PNG로 미리 렌더링해서 임베드한다. 여기서 "스택"은 층상 아키텍처가 아니라 짧은 리스트의 의미.

마이그레이션 중 gpdf와 go-pdf/fpdf를 병존할 수 있나? 할 수 있다. import 경로도 타입도 다르다. 새 엔드포인트는 gpdf, 오래된 것은 시간이 될 때까지 go-pdf/fpdf에 남겨두면 된다. 런타임 충돌은 없다.

go-pdf/fpdf v3나 새 포크가 나올까? 혹시 모른다. gpdf의 베팅은 "그 포크가 영원히 아카이브로 남는다"가 아니라 — 아키텍처가 오늘 만드는 것에 스케일하지 않는다 는 쪽. 새 포크가 레이아웃 모델을 안 고치면 같은 제약을 물려받는다. 고치면 그건 fpdf보다 gpdf에 가깝다.

현대 대안으로 signintech/gopdf은 어떤가? 진짜로 유지되고 진짜로 의존성 0. API는 좌표 수준 — SetX, SetY, CellWithOption — 폼 오버레이와 고정 템플릿에 잘 맞는다. 테이블과 반복 헤더/푸터가 있는 세금계산서류 문서에서는 결국 위에 레이아웃 헬퍼를 쓰게 되고, gofpdf 사용자가 빠진 같은 함정으로 돌아간다. gpdf와 gopdf는 사실 경쟁 관계가 아니다 — 인접한 문제를 푼다.

gpdf에 상용/호스티드 버전이 있나?gpdf-api 준비 중 — JSON 템플릿을 POST하면 PDF를 돌려주는 호스티드 API. 아직 공개하지 않았다. 출시할 때 이 블로그에 글이 올라온다. OSS 라이브러리는 계속 MIT, 의존성 0, 독립적으로 유용한 상태로 유지된다.

로드맵 우선순위는? 2026-04 시점의 공개 로드맵: (1) AcroForm 폼 필드, (2) 완전한 PDF/A-3 준수, (3) gpdf-pro의 HTML→PDF 커버리지 확장, (4) RTL 텍스트 지원(아랍어, 히브리어). 우선순위 피드백은 GitHub 이슈에서 환영.

gpdf 써보기

gpdf는 Go PDF 생성 라이브러리입니다. MIT 라이선스, 외부 의존성 0, 네이티브 CJK 지원.

go get github.com/gpdf-dev/gpdf

⭐ GitHub에서 별 누르기 · 문서 읽기

다음 읽기