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 / Spacer — Row / AutoRow는 PageBuilder 쪽에만 있습니다. 문법을 찾으러 왔다면, 그 문법은 없습니다. 아래에서 대체 관용 패턴 세 가지를 설명합니다.
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:246의 c.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의 12-컬럼 그리드는 어떻게 동작하는가? — 그리드 자체의 자세한 설명
- Go로 50줄 안에 청구서 PDF 만들기 — 평면 그리드로 문서 한 장 구성
- Layout guide — Row / Col / Box 레퍼런스
gpdf 써보기
gpdf는 Go용 PDF 생성 라이브러리입니다. MIT 라이선스, 외부 의존성 없음, CJK 네이티브 지원.
go get github.com/gpdf-dev/gpdf