전체 글

gpdf의 Col 안에 Row를 중첩하려면?

할 수 없습니다. ColBuilder에 Row 메서드가 없고 gpdf의 12-컬럼 그리드는 의도적으로 평면입니다. 대체할 세 가지 관용 패턴을 소개합니다.

질문을 다르게 표현하면

Bootstrap이나 Tailwind에서는 .row 안에 .col, 그 안에 다시 .row를 자유롭게 중첩할 수 있습니다. gpdf에서도 같은 r.Col(span, fn) 패턴이 보이니, 컬럼 콜백 안에서 c.Row(...)를 찾게 됩니다. 자동완성이 뜨지 않습니다. 빠진 건가요?

짧은 답

아닙니다. gpdf의 12-컬럼 그리드는 의도적으로 평면입니다. ColBuilder는 콘텐츠만 받습니다 — Text / Image / Table / Box / List / SpacerRow / AutoRowPageBuilder 쪽에만 있습니다. 문법을 찾으러 왔다면, 그 문법은 없습니다. 아래에서 대체 관용 패턴 세 가지를 설명합니다.

API의 실제 모양

ColBuilder의 메서드 집합 (gpdf/template/grid.go 출처):

func (c *ColBuilder) Text(text string, opts ...TextOption)
func (c *ColBuilder) Image(src []byte, opts ...ImageOption)
func (c *ColBuilder) Box(fn func(c *ColBuilder), opts ...BoxOption)
func (c *ColBuilder) Table(header []string, rows [][]string, opts ...TableOption)
func (c *ColBuilder) Line(opts ...LineOption)
func (c *ColBuilder) List(items []string, opts ...ListOption)
func (c *ColBuilder) Spacer(height document.Value)
// …PageNumber, TotalPages, RichText, QRCode, Barcode

Row도, AutoRow도, Col도 없습니다. Col → Row로 가는 메서드 경로 자체가 없고, 가장 가까운 건 c.Box(fn, ...)인데 이것도 또 다른 *ColBuilder를 받지 Row를 받지 않습니다. 컬럼을 컬럼 안에(Box로 흉내내서) 넣을 수는 있지만, 컬럼 안에 새로운 가로 행을 열 수는 없습니다. 이게 제약입니다.

관용 패턴 1 — 페이지 레벨의 형제 Row

"중첩 Row"라고 쓰고 싶어지는 경우의 90%가 사실 이걸 원합니다.

package main

import (
    "log"
    "os"

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

func main() {
    doc := gpdf.NewDocument(
        gpdf.WithPageSize(document.A4),
        gpdf.WithMargins(document.UniformEdges(document.Mm(15))),
    )
    page := doc.AddPage()

    // 쓰고 싶지만 쓸 수 없는 코드:
    //
    //   page.AutoRow(func(r *template.RowBuilder) {
    //       r.Col(8, func(c *template.ColBuilder) {
    //           c.Row(...) ❌ 존재하지 않음
    //       })
    //   })

    // 실제로 쓰는 코드:
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(8, func(c *template.ColBuilder) {
            c.Text("기사 제목", template.FontSize(18), template.Bold())
        })
        r.Col(4, func(c *template.ColBuilder) {
            c.Text("2026-05-16")
        })
    })
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(8, func(c *template.ColBuilder) {
            c.Text("리드 문단은 같은 8 폭 컬럼을 사용합니다.")
        })
        r.Col(4, func(c *template.ColBuilder) {
            c.Text("저자: Taiki Noda")
        })
    })

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

AutoRow가 같은 8+4 스팬을 공유하므로 시각적으로 컬럼이 정렬됩니다. 서브 그리드가 아니라, 같은 컬럼 분할을 쓰는 평면 Row 두 개입니다. CSS에서 .col-8 안에 .row를 중첩한 출력과 동일합니다 — 중첩이 사주는 건 결국 문법적 지역성뿐이고, gpdf는 그 예산을 "컬럼 폭 일관성"에 다시 씁니다.

관용 패턴 2 — 시각적 그룹화는 c.Box

진짜 동기가 "이 컬럼 안에 테두리 있는 카드를 그리고 안에 두 줄을 쌓고 싶다"였다면 원하는 건 Box이지 서브 Row가 아닙니다:

page.AutoRow(func(r *template.RowBuilder) {
    r.Col(6, func(c *template.ColBuilder) {
        c.Box(func(c *template.ColBuilder) {
            c.Text("청구지", template.Bold())
            c.Text("애크미 주식회사")
            c.Text("서울시 강남구")
        },
            template.WithBoxBorder(template.Border(
                template.BorderWidth(document.Pt(1)),
                template.BorderColor(pdf.RGBHex(0xBDBDBD)),
            )),
            template.WithBoxPadding(document.UniformEdges(document.Mm(4))),
        )
    })
    r.Col(6, func(c *template.ColBuilder) {
        c.Box(func(c *template.ColBuilder) {
            c.Text("배송지", template.Bold())
            c.Text("청구지와 동일")
        },
            template.WithBoxPadding(document.UniformEdges(document.Mm(4))),
        )
    })
})

Box가 받는 *ColBuilder 내부는 세로 스택입니다. Box를 가로로 쪼갤 수도 없습니다 — 가로가 필요하면 패턴 1로 돌아갑니다. 다만 "카드" 패턴을 만들고 싶어서 중첩 Row를 찾았다면, 정답은 이것입니다. grid.go:246c.Box가 그리드가 허용하는 유일한 중첩이며, 그건 의도적으로 1차원입니다.

관용 패턴 3 — 서브 그리드는 12 컬럼으로 직접 표현

"페이지 왼쪽 절반 안에서 2 컬럼이 필요해 — 썸네일과 캡션을 왼쪽에 나란히, 본문을 오른쪽에" 같은 경우. 직관은 Col(6) > Row > Col(6) + Col(6)이지만, 평면 등가물은 그냥 Col(3) + Col(3) + Col(6)입니다:

page.AutoRow(func(r *template.RowBuilder) {
    r.Col(3, func(c *template.ColBuilder) {
        c.Image(thumbBytes)
    })
    r.Col(3, func(c *template.ColBuilder) {
        c.Text("Photo by Ansel Adams", template.Italic())
        c.Text("1942")
    })
    r.Col(6, func(c *template.ColBuilder) {
        c.Text("본문 문단이 페이지의 오른쪽 절반을 차지합니다.")
    })
})

3 + 3은 합쳐서 6이므로, 썸네일 + 캡션 쌍이 왼쪽 절반에 정확히 자리잡습니다. 12는 2, 3, 4, 6으로 나누어떨어지므로 중첩 그리드는 거의 항상 평면으로 깔끔히 풀립니다. 중첩이 Col(8) > Row > Col(7) + Col(5)였다면 풀리지 않지만 — 그 숫자들은 실제 종이 위에서도 의미가 없습니다. 풀 수 있는 평면 버전을 고르세요.

왜 중첩이 없는가

평면 그리드는 폭을 한 번에 해결합니다. Row는 (페이지 폭 − 마진)의 백분율, 각 Col(span)은 그 span / 12. 끝. 재귀 없음, 폭-속-폭-속-폭 없음, 레이아웃 엔진에 부모 컨텍스트를 끌고 다니지 않습니다. grid.go에서 컬럼 폭을 계산하는 줄은 말 그대로 한 줄입니다:

Width: document.Pct(float64(col.span) / float64(gridColumns) * 100),

중첩을 허용하면 이 줄이 트리 탐색이 됩니다. Col(12) 안의 Col(8) 안의 Col(6)에서 그 6이 부모 컬럼의 50%인지, Row의 50%인지, 페이지의 50%인지 결정해야 합니다. Bootstrap은 "부모의 50%"를 골랐고, 그걸 견딜 만하게 만들기 위해 브레이크포인트와 거터(gutter)를 더했습니다. PDF에는 브레이크포인트가 없습니다. PDF에는 유동 컨테이너도 없습니다. 중첩 문법을 빌려오면 풀 필요 없는 문제 셋을 들여오면서, 안 필요한 문법 설탕을 받는 셈입니다.

"그래도 문법적 지역성이 필요해요"

이해합니다. 평면화의 단점은 개념적으로 한 묶음인 두 AutoRow가 편집을 거치며 소스에서 멀어진다는 점입니다. 작은 헬퍼로 메울 수 있습니다:

func card(page *template.PageBuilder, title, body string) {
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text(title, template.Bold())
        })
    })
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text(body)
        })
    })
}

지역성은 당신의 함수 안에 있지, API 안에 있지 않습니다. gpdf가 card를 내장하지 않는 이유는, 세 줄이면 되고 당신이 직접 쓴 버전이 우리가 준비한 버전보다 당신의 문서에 더 잘 맞을 것이기 때문입니다.

관련 레시피

gpdf 써보기

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

go get github.com/gpdf-dev/gpdf

⭐ GitHub에서 Star · 문서 읽기