왜 당신의 PDF 파이프라인이 필요 이상으로 느린가
Source: Dev.to
모든 백엔드 엔지니어는 이런 경험을 해봤을 것이다. 클라이언트는 청구서, 보고서, 증명서, 명세서를 원한다. “그냥 PDF만 생성해 주세요,” 라고 마치 print() 문 하나처럼 말한다.
그래서 표준 툴체인을 잡는다: Jinja2로 HTML을 렌더링하고, 헤드리스 Chrome 인스턴스를 띄운 뒤 page.pdf()를 호출하고, 배치에서 500번째 문서에서 OOM이 발생하지 않기를 기도한다.
작동한다. 안 할 때까지.
헤드리스 브라우저 비용
헤드리스 브라우저를 통해 PDF를 생성할 때 실제로 일어나는 일은 다음과 같습니다:
- Chromium 프로세스를 생성(또는 풀에 연결)
- 새로운 페이지 컨텍스트 생성
- HTML + CSS + 자산 로드
- 폰트, 이미지, 레이아웃 로드 대기
- print‑to‑PDF API 호출
- PDF 바이트 직렬화
- 페이지 컨텍스트 정리
단일 청구서의 경우 2–5 seconds가 소요됩니다. 월간 10 000개의 명세서를 한 번에 처리하면 hours of compute, 기가바이트 단위의 RAM, 그리고 문서 인쇄만을 위해 전용 인프라가 필요한 배포 환경이 필요합니다.
가장 안 좋은 점은? Chromium이 전체 웹 페이지를 렌더링한다는 점입니다—JavaScript 엔진, DOM, CSSOM, 레이아웃 트리, 페인트, 합성—하지만 실제로 필요한 것은 “이 단어들을 이 위치에 배치하고 몇 개의 선을 그리기”뿐입니다.
결정론적 렌더링이 실제로 의미하는 바
결정론적 PDF 렌더러는 문서를 웹 페이지처럼 해석하지 않습니다. 구조화된 템플릿을 읽고, 레이아웃을 해결한 뒤 PDF 기본 요소를 직접 작성합니다. 브라우저 없음. JavaScript 엔진 없음. 중간 렌더링 단계도 없습니다.
비교
| 항목 | 헤드리스 브라우저 | 네이티브 렌더러 |
|---|---|---|
| 문서당 시간 | 2–5 seconds | 50–200 ms |
| 문서당 메모리 | 200–500 MB | 10–30 MB |
| 10,000개 배치 | 5–14 hours | 8–30 minutes |
| 결정성 | 아니오 (경쟁 조건) | 예 (SHA‑256 재현 가능) |
| 의존성 | Chrome/Puppeteer | 없음 |
그 마지막 행은 생각보다 더 중요합니다. “의존성 없음”은 PDF 생성이 Lambda 함수, Docker 컨테이너, CI 파이프라인, 혹은 아무 것도 설치되지 않은 베어 메탈 서버에서도 동작한다는 의미입니다. 관리할 Chrome 바이너리가 없습니다. 버전 불일치도 없습니다. 샌드박스 문제도 없습니다.
아무도 이야기하지 않는 재현성 문제
헤드리스 브라우저로 동일한 청구서를 두 번 생성하고 바이트를 비교해 보세요. 일치하지 않을 것입니다.
- 폰트가 실행마다 약간씩 다르게 렌더링됩니다.
- 타임스탬프가 메타데이터에 삽입됩니다.
- 이미지 압축이 비트 단위로 안정적이지 않습니다.
이는 해시를 비교하여 문서가 변조되지 않았는지 확인할 수 없으며, 적극적인 캐시를 사용할 수 없고, 문서 식별에 의존하는 감사 추적을 구축할 수 없다는 의미입니다.
결정론적 렌더러는 동일한 입력에 대해 항상 동일한 바이트를 생성합니다. 이는 학문적인 문제가 아니라 의료, 금융, 법률 문서 파이프라인에서의 규정 준수 요구 사항입니다. 규제 산업을 위한 문서를 생성한다면, 비결정론은 위험 요소가 됩니다.
실제 예시
저는 이 문제를 해결하기 위해 Fullbleed 를 만들었습니다. Rust 기반 PDF 렌더링 엔진에 Python 바인딩을 제공합니다.
단일 문서 설치 및 렌더링
pip install fullbleed
fullbleed render invoice.html --output invoice.pdf
배치 렌더링
from fullbleed import render_batch
documents = [
{"template": "invoice.html", "data": customer}
for customer in customers
]
# Rayon을 통해 사용 가능한 모든 코어에서 렌더링
results = render_batch(documents, workers="auto")
10 000개의 청구서. 시간 대신 분 단위로 처리됩니다. 출력이 결정적이며, Chrome은 전혀 필요 없습니다.
네이티브 렌더러를 사용해야 할 때(그리고 사용하면 안 될 때)
네이티브 렌더러를 사용해야 할 경우:
- 배치로 문서(청구서, 명세서, 보고서 등)를 생성할 때
- 컴플라이언스나 감사용으로 재현 가능한 출력이 필요할 때
- 파이프라인이 제한된 환경(Lambda, CI, 엣지)에서 실행될 때
- 성능이 중요할 때(문서당 1초 미만)
- 외부 의존성을 전혀 원하지 않을 때
헤드리스 Chrome을 사용해야 할 경우:
- 사용자가 제공한 임의의 HTML/CSS/JS를 렌더링할 때
- 픽셀 단위로 정확한 웹 페이지 스크린샷이 필요할 때
- 템플릿에 복잡한 JavaScript 인터랙션이 포함될 때
- 하루에 10개 미만의 문서만 생성하고 속도가 크게 중요하지 않을 때
대부분의 백엔드 엔지니어는 익숙하기 때문에 헤드리스 Chrome을 기본으로 선택합니다. 하지만 사용 사례가 구조화된 문서 생성(데이터가 들어간 템플릿)이라면, 필요 없는 기능 때문에 막대한 성능 및 복잡성 비용을 지불하게 됩니다.
요약
PDF 생성은 해결된 문제이지만 대부분의 팀이 제대로 해결하지 못한다. 그들이 무능해서가 아니라, 일반성을 성능보다 우선시하는 명백한 도구들(wkhtmltopdf, Puppeteer, Playwright) 때문이다. 실제 요구 사항이 “이 템플릿에 이 데이터를 채워 PDF를 만들어라”라면, 목적에 맞게 만든 렌더러가 10–100× 빠르고, 메모리를 극히 적게 사용하며, 결정론적인 출력을 만든다.
문서 파이프라인을 구축하고 이 접근 방식을 탐색하고 싶다면, Fullbleed은 오픈 소스입니다 (AGPLv3)이며 pip install fullbleed 로 설치할 수 있다. 상업적 사용을 위한 라이선스도 제공된다.