Node.js에서 HTML로부터 PDF 생성 (그리고 Puppeteer 사용을 중단한 이유)

발행: (2026년 3월 18일 PM 03:03 GMT+9)
7 분 소요
원문: Dev.to

Source: Dev.to

Puppeteer에 실제로 문제가 되는 점

  • Memory – Chromium은 전체 브라우저입니다. 각 인스턴스는 300–500 MB를 차지합니다. 렌더링이 충돌하면 브라우저 프로세스가 정리되지 않을 수 있어, 실제 트래픽 상황에서 서버 메모리가 결국 고갈됩니다.
  • Cold starts – Chromium 인스턴스를 시작하는 데 1–3 초가 걸리며, 매번 발생합니다. 제로 스케일링되는 서버리스 함수에서는 이 지연이 첫 번째 요청에 바로 영향을 줍니다.
  • Fonts and assets – Puppeteer는 샌드박스 환경에서 실행됩니다. 상대 경로나 file:// URL에서 로드되는 모든 것은 조용히 실패하거나 잘못 렌더링됩니다. 로컬에서는 정상인 PDF도 프로덕션에서는 깨져 보일 수 있습니다.
  • Server dependencies – Chromium은 libglib, libnss, libatk 등과 같은 라이브러리를 필요로 하는데, 이는 기본 Ubuntu 서버에는 포함되어 있지 않습니다. 새로운 환경마다 새 디버깅 세션이 필요하고, Docker 이미지 크기가 약 400 MB 정도 증가합니다.

이 모든 것이 Puppeteer의 잘못은 아닙니다; 설계 목적에 맞지 않는 작업을 수행하도록 요구받은 브라우저 자동화 도구이기 때문입니다.

사람들이 시도하는 다른 옵션

wkhtmltopdf

WebKit을 사용해 HTML을 렌더링합니다. 브라우저 프로세스를 관리할 필요가 없어 빠르고 가볍습니다.
단점: 2020년 이후로 유지보수가 중단됐으며, CSS 지원이 2013년 수준에 머물러 있습니다(플렉스박스, 그리드, CSS 변수 등 없음). 최신 레이아웃은 깨질 수 있습니다.

PDFKit / jsPDF

코드로 문서를 기술합니다—텍스트 배치, 선 그리기, 폰트 설정 등. 매우 정밀하며 고정 레이아웃 문서에 적합합니다.
단점: HTML 템플릿을 재사용할 수 없습니다. 디자인을 바꾸려면 코드를 수정해야 하고, 동적 테이블이 포함된 간단한 청구서조차도 코드가 장황해집니다.

API

HTML을 보내면 PDF를 반환받습니다. 렌더링 인프라가 제3자에게 맡겨지므로 Chromium을 관리할 필요도, 시스템 의존성도, 배포할 파일도 없습니다. 대부분의 팀이 다른 옵션을 모두 시도한 뒤 선택하는 방법입니다.

API를 실제로 사용하는 모습

Here’s a basic Node.js example using LightningPDF:

const response = await fetch("https://lightningpdf.dev/api/v1/pdf/generate", {
  method: "POST",
  headers: {
    "Authorization": "Bearer YOUR_API_KEY",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    html: `
      
        
        
          

Invoice #1042

Due: March 31, 2026

`,
options: { format: "A4" }

}) });

const { data } = await response.json(); const pdfBuffer = Buffer.from(data.pdf, “base64”);


그게 전부입니다. 기존 HTML 템플릿은 그대로 사용할 수 있고, Tailwind 클래스는 빌드 단계 없이도 렌더링되며, Puppeteer에서의 마이그레이션은 주로 브라우저 설정/해제 코드를 제거하는 정도입니다.

## 반복적으로 생성하는 문서 템플릿

청구서나 보고서와 같이 구조가 고정된 문서를 생성한다면, 시각 디자이너에서 템플릿을 한 번 만든 뒤 렌더링 시에 데이터를 전달하세요:

```js
body: JSON.stringify({
  template_id: "invoice-001",
  data: {
    company: "Acme Corp",
    invoice_number: "1042",
    items: [
      { name: "Web development", quantity: 10, price: 150 },
      { name: "Design review", quantity: 2, price: 200 }
    ]
  }
})

앱 코드에서 HTML 문자열을 직접 연결할 필요가 없습니다—템플릿은 별도로 존재하며 렌더링 시에 채워집니다.

배치 생성

대량 작업(월말 청구서, 보고서 실행 등)에는 async 엔드포인트를 사용하고 PDF가 준비되면 웹훅을 받으세요:

const response = await fetch("https://lightningpdf.dev/api/v1/pdf/async", {
  method: "POST",
  headers: {
    "Authorization": "Bearer YOUR_API_KEY",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    template_id: "monthly-statement",
    data: { user_id: "usr_123", month: "February" },
    webhook_url: "https://yourapp.com/webhooks/pdf-ready"
  })
});

대략적인 성능 수치

방법일반적인 렌더링 시간메모리 오버헤드
Puppeteer (self‑hosted)2–4 s인스턴스당 300–500 MB
wkhtmltopdf0.5–1 s낮음
API (simple docs)< 100 ms당신의 문제가 아님
API (complex CSS)1–3 s당신의 문제가 아님

간단한 문서의 속도 차이는 상당합니다. Go‑native 렌더러는 기본 인보이스를 100 ms 이하로 생성할 수 있지만, 복잡한 HTML/CSS의 경우에만 Chromium이 필요합니다.

작은 프로젝트에 이것이 가치가 있을까요?

아마도 그렇습니다. 주된 이유는 배포 복잡성 때문입니다. 한 달에 20개의 PDF만 생성하더라도 모든 서버에 Chromium을 설치하지 않아도 되는 것이 큰 장점입니다. 대부분의 PDF API는 저용량을 커버하는 무료 티어를 제공합니다.

실제 트래픽이 있거나 배치 생성이 필요한 경우, 이점이 더욱 명확해집니다—메모리 제한이나 프로세스 관리에 대한 고민이 사라집니다.

현재 PDF 생성을 위해 어떤 도구를 사용하고 계신가요? 자체 호스팅 Puppeteer를 프로덕션에서 잘 작동하도록 만든 방법이 있다면 알려 주세요.

0 조회
Back to Blog

관련 글

더 보기 »

Rob Pike의 5가지 프로그래밍 규칙

규칙 1: 프로그램이 어디에서 시간을 소비할지 알 수 없습니다. Bottlenecks는 예상치 못한 곳에서 발생하므로, 추측해서 speed hack을 넣으려 하지 마세요.