전체 글

테이블을 여러 페이지에 걸쳐 출력하려면?

아무것도 하지 않아도 된다. 한 페이지에 안 들어가는 행 수의 테이블을 넘기면 gpdf가 본문을 자동으로 페이지 분할하고 각 페이지 맨 위에 헤더를 반복한다.

질문을 바꿔 말하면

리포트가 있다 — 청구 명세, 거래 로그, 300행짜리 내보내기 — 누가 봐도 A4 한 장에 안 들어간다. Go의 PDF 라이브러리에서 이 테이블을 2페이지, 3페이지로 흘려보내고 각 페이지 맨 위에 헤더가 다시 나오게 하려면 뭘 해야 하나? gpdf에서는 답이 짧다.

결론

아무것도 안 해도 된다. Table을 한 번 호출하고 모든 행을 넘기면 gpdf가 페이지를 나눈다:

c.Table(header, rows) // rows가 300행 — gpdf가 여러 페이지로 나눈다

본문은 필요한 페이지 수만큼 한 행씩 분할된다. header 슬라이스는 이어지는 페이지 맨 위에 자동으로 다시 렌더링된다 — 열 너비도 스타일도 동일하다. PageBreak() 메서드도, MaxRowsPerPage 옵션도, 행을 세는 루프도 없다. 오버플로 처리는 레이아웃 엔진의 일이지 당신의 일이 아니다.

동작하는 코드

여러 페이지 테이블을 출력하는 완전한 프로그램. main.go로 저장하고 go run ., report.pdf가 나온다.

package main

import (
    "fmt"
    "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(
        gpdf.WithPageSize(gpdf.A4),
        gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
    )

    brand := pdf.RGBHex(0x1A237E)

    header := []string{"Date", "Invoice #", "Customer", "Amount"}
    rows := make([][]string, 0, 200)
    for i := 1; i <= 200; i++ {
        rows = append(rows, []string{
            fmt.Sprintf("2026-%02d-%02d", (i%6)+1, (i%28)+1),
            fmt.Sprintf("INV-%05d", 10000+i),
            fmt.Sprintf("Customer #%d", i),
            fmt.Sprintf("$%d.00", 100+i*7),
        })
    }

    page := doc.AddPage()
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text("2026 Invoice Ledger", template.FontSize(18), template.Bold())
            c.Spacer(document.Mm(4))

            c.Table(header, rows,
                template.ColumnWidths(20, 20, 40, 20),
                template.TableHeaderStyle(
                    template.TextColor(pdf.White),
                    template.BgColor(brand),
                ),
            )
        })
    })

    data, err := doc.Generate()
    if err != nil {
        log.Fatal(err)
    }
    if err := os.WriteFile("report.pdf", data, 0o644); err != nil {
        log.Fatal(err)
    }
}

200행을 A4에 흘리면 약 8페이지가 된다. 그 모든 페이지 맨 위에 짙은 파란색 헤더가 앉아 있고, 본문은 이전 페이지가 멈춘 자리에서 정확히 이어진다. 이 코드에서 '여러 페이지'를 암시하는 건 루프 상한의 200뿐이다.

작동 원리

신뢰하기 위해 알아둘 가치가 있다. 레이아웃 엔진은 테이블을 배치할 때 본문 행을 순서대로 측정하면서, 다음 행이 가용 높이를 초과하기 직전까지 현재 페이지에 더한다. 안 들어간 행은 오버플로 테이블이 된다 — 같은 Header, 같은 Footer, 남은 본문 행을 가진 새 *document.Table이다. gpdf는 배치된 부분을 페이지에 흘려보내고, 다음 페이지를 열고, 새 페이지의 높이로 오버플로 테이블을 다시 레이아웃 엔진에 넘긴다. 남은 게 없을 때까지 반복한다.

여기서 두 가지가 따라 나온다:

  • 헤더가 반복되는 건 그것이 당신의 루프가 아니라 tbl.Header에 있기 때문이다. 오버플로 테이블은 같은 슬라이스를 재사용하므로 매 페이지에서 동일하게 다시 렌더링된다. 공짜로 얻는다.
  • '헤더가 안 들어감' 같은 경계 사례가 없다. 엔진은 본문이 몇 행 들어가는지 측정하기 전에 헤더 분량의 높이를 확보한다. 헤더 + 최소 한 행의 본문이 페이지에 안 들어가면, 어색하게 쪼개지 않고 테이블 전체를 다음 페이지로 보낸다.

푸터도 반복된다

합계 행(또는 '페이지 소계')을 페이지 하단에도 내고 싶으면 그건 document.Table.Footer — builder 경유가 아니라 document 레이어에서 테이블을 구성할 때 쓸 수 있다:

import "github.com/gpdf-dev/gpdf/document"

tbl := &document.Table{
    Columns: []document.TableColumn{
        {Width: document.Pct(20)}, {Width: document.Pct(20)},
        {Width: document.Auto},    {Width: document.Pct(20)},
    },
    Header: headerRows, // []document.TableRow
    Body:   bodyRows,
    Footer: []document.TableRow{footerRow},
}

Footer 슬라이스는 이어지는 페이지마다 반복되며, 메커니즘은 헤더와 같다. builder의 c.Table(...)이 푸터를 노출하지 않는 건 짧은 테이블 대부분에 필요 없기 때문이다 — 필요해진 순간 당신은 이미 '일반적인 경우' 구역 밖이다. 테이블 심층 설명이 document 레이어를 짚어준다.

테이블을 새 페이지에서 시작하기

테이블 단위의 '새 페이지에서 시작' 옵션은 없다. 페이지 레벨에서 한다 — 테이블을 담은 행 앞에 페이지를 추가한다:

doc.AddPage() // 아래 테이블은 이 페이지 맨 위에서 시작한다
page2 := doc.AddPage()
page2.AutoRow(func(r *template.RowBuilder) {
    r.Col(12, func(c *template.ColBuilder) {
        c.Table(header, rows /* , opts... */)
    })
})

테이블에 필요한 '페이지 나누기' 제어는 이 하나뿐이다. 테이블 내부의 분할은 알아서 처리되고, 외부의 분할은 그냥 '이 블록이 어디서 시작하는가'일 뿐이기 때문이다.

안 되는 것

  • '이 행들은 같이 둬라'. 모든 본문 행은 분할 대상이다. '4–7행 그룹은 한 페이지에 유지하라' 같은 주석은 없다. 알려진 빈틈이다. 청구 명세 한 줄과 그 하위 행이 절대 페이지를 가로질러 찢기면 안 된다면, 우회책은 그 그룹 앞에서 새 페이지를 시작하거나 document 레이어에서 테이블을 구성해 자체 분할 힌트를 넣는 것이다.
  • 마지막 페이지에만 있는 푸터. document.Table.Footer는 설계상 모든 페이지에서 반복된다(페이지별 열 합계가 일반적인 경우다). 문서 끝에 한 번만 나오는 총계가 필요하면, 테이블 이 아니라 에 별도 블록으로 추가한다.
  • 테이블 안의 페이지 번호. '3 / 8 페이지'는 테이블이 아니라 문서 푸터에 속한다. 그게 어디 놓이는지는 페이지 번호, 헤더, 푸터를 참고.

10분 날리는 실수

  • PageBreak 옵션을 찾는 것. 없고, 원하지도 않을 것이다 — 그걸 수동으로 호출하는 시점에 이미 졌다. 모든 행을 넘기면 된다.
  • 데이터를 직접 페이지 단위로 자르는 것. 1페이지에 rows[0:40], 2페이지에 rows[40:80]… 하지 마라. 어딘가에서 행 계산을 틀리고, 마지막 페이지가 짧아지고, 헤더 스타일이 어긋난다. 슬라이스 전체를 gpdf에 넘겨라.
  • 헤더가 1페이지에만 있을 거라고 생각하는 것. 그런 라이브러리도 있다. gpdf는 모든 페이지에서 반복하며, 이건 인쇄해서 넘겨 보는 리포트에 필요한 동작이다.
  • 150페이지 테이블에 6 MB CJK 폰트. 폰트는 실제 사용된 글리프로 서브셋되므로 괜찮다 — 출력은 작게 유지된다. 다만 어떤 이유로 서브셋을 꺼뒀다면, 긴 테이블이 바로 그게 무는 곳이다. 서브셋은 켜둔 채(기본값)로 둬라.

관련 레시피

gpdf 써보기

gpdf는 Go의 PDF 생성 라이브러리다. MIT 라이선스, 외부 의존성 제로, CJK 기본 지원.

go get github.com/gpdf-dev/gpdf

⭐ Star on GitHub · Read the docs