우리는 청구서를 PDF로 보내야 했습니다. 해결 방법은 다음과 같습니다.

발행: (2026년 1월 16일 오전 12:57 GMT+9)
12 min read
원문: Dev.to

I’m happy to translate the article for you, but I’ll need the text you’d like translated. Could you please paste the content (excluding the source line you already provided) here? Once I have the article text, I’ll keep the source link at the top unchanged and translate the rest into Korean while preserving the original formatting and technical terms.

몇 달 전, 우리 재무팀이 매우 합리적인 요청을 가지고 찾아왔습니다.

그들은 Google Docs에서 수동으로 인보이스를 만들고, PDF로 내보낸 뒤 매주 고객에게 이메일을 보내고 있었습니다. 매주 월요일마다 약 3시간이 걸렸고, 이를 자동화하고 싶어했습니다.

I remember thinking:

“This is just PDF generation. I’ve built far more complex things than this.”

그 자신감은 약 5분 정도 지속됐습니다. PDF를 한 번도 만들어 본 적이 없다면, 이 글이 도움이 될 것입니다. If you have built PDFs before, you already know where this is going — brace yourself, it’s going to hurt a little. 😅

당시에는 그 요청이 무해해 보였습니다. 인보이스 자동화. PDF 생성. 바로 배포.

우리가 깨닫지 못한 것은, 이 작은 작업이 브라우저, 폰트, 테이블, 페이지 매김, 그리고 우리의 과신과 싸우는 몇 주가 될 것이라는 점이었습니다.

시도 #1: “PDF 라이브러리를 그냥 사용하자” 🤡

대부분의 개발자와 마찬가지로, 나는 JSON으로 문서를 정의할 수 있는 PDF 라이브러리부터 시작했다.

const docDefinition = {
  content: [
    { text: "INVOICE", style: "header" },
    {
      table: {
        body: [
          ["Pro Plan", "$99"],
          ["Extra Users", "$75"],
        ],
      },
    },
  ],
};

pdfMake.createPdf(docDefinition).download("invoice.pdf");

이론적으로는 괜찮아 보였다. 실제로는 스프레드시트 안에 CSS를 쓰는 느낌이었다. 모든 것이 깊게 중첩돼 있었고, 스타일링은 어색했으며, 인보이스에서 가장 중요한 동적 테이블 관리가 엄청나게 고통스러웠다.

몇 일을 지나자, 이것이 확장되지 않을 것이라는 것이 명백해졌다.

혼란스러운 개발자

문제점이 즉시 드러났다:

  • Tailwind 디자인 시스템은 쓸모없었다 — 모든 스타일을 다시 작성해야 함
  • 간단한 레이아웃 변경이 깊게 중첩된 설정 객체로 변함
  • 동적 콘텐츠는 읽기 어려운 JSON 로직을 만들게 함
  • 디버깅이 고통스러웠다: 생성 → 다운로드 → 열기 → 눈을 가늘게 뜨기 → 반복

게다가 2009년에 설계된 것처럼 보였다.

Attempt #2: “What If We Just Print HTML?” 🖨️

다음으로 떠오른 명백한 아이디어: 이미 HTML이 있으니 window.print()를 사용하면 어떨까?

<div class="invoice">
  <h2>Invoice</h2>
  <p>Pro Plan $99</p>
  <button>Generate PDF</button>
</div>

처음엔 마법처럼 느껴졌다 ✨

기존 HTML을 재사용할 수 있었다. CSS도 작동했다. 화면이 대부분 정상적으로 보였다.

그 후 여러 브라우저에서 테스트해 보았다:

브라우저문제
Chrome잘 보임
Safari이상한 여백
Firefox표 헤더가 사라짐
Windows Chrome푸터가 사라짐

페이지 구분은 완전한 혼란이었다. 제품 이름은 한 페이지에, 가격은 다음 페이지에 나타났고, 이유 없이 페이지 하단에 로고 절반만 보였다.

내부 문서라면 허용될 수도 있지만, 고객에게 제공되는 인보이스에는 전혀 적합하지 않았다.

시도 #3: “알겠어, 헤드리스 크롬” 😐

브라우저 간 불일치를 해결하기 위해 Puppeteer—서버에서 실행되는 헤드리스 Chrome—로 전환했습니다.

import puppeteer from "puppeteer";

async function generateInvoice(html) {
  const browser = await puppeteer.launch({
    args: ["--no-sandbox", "--disable-setuid-sandbox"],
  });

  const page = await browser.newPage();
  await page.setContent(html, { waitUntil: "networkidle0" });

  await page.pdf({
    format: "A4",
    printBackground: true,
    margin: { top: "20mm", bottom: "20mm" },
  });

  await browser.close();
}

일관성은 개선됐지만 새로운 문제가 나타났습니다:

  • Linux 서버에서 커스텀 폰트가 누락됨
  • 이미지가 가끔 로드되고, 가끔 로드되지 않음
  • 사소한 CSS 변경이 페이지 매김을 예상치 못하게 깨뜨림
  • 페이지 구분에 대한 실질적인 제어가 없음

마치 조립 설명서 없이 IKEA 가구를 조립하는 느낌이었습니다. 기술적으로는 모두 맞물리지만, 전혀 견고하게 느껴지지 않았습니다.

모든 것이 괜찮아

실제 문제 (아무도 설명하지 않음)

결국, 그 패턴이 명확해졌다.

HTML은 스크롤링을 위해 설계되었습니다. PDF는 페이지를 위해 설계되었습니다.

웹 페이지PDF
무한 스크롤고정 페이지 크기
유연한 레이아웃정확한 레이아웃
페이지 규칙 없음엄격한 페이지 매김

브라우저는 이 격차를 메우려고 시도합니다. 하지만 대부분 실패합니다. 모든 팀이 결국 이 문제를 힘들게 다시 발견하게 됩니다.

마침내 성공한 방법 🎉

수 주간의 좌절 끝에 pdfn을 발견했고, PDF 생성 방식이 완전히 바뀌었습니다. 루프, props, 조건문을 활용한 React 컴포넌트 형태의 인보이스, 영수증, 계약서를 만들 수 있으며, 페이지네이션, 헤더·푸터, 스마트 페이지 나눔을 자동으로 처리합니다.

React Components
  (Invoice, Receipt)


 @pdfn/react
  - Pagination‑aware rendering
  - Smart page breaks
  - Headers & Footers


 @pdfn/serve
  - Headless Chromium / Puppeteer


 Perfect PDF Output
import { Document, Page } from "@pdfn/react";

export default function Invoice() {
  return (
    <Document>
      <Page>
        {/* Invoice content goes here */}
      </Page>
    </Document>
  );
}

pdfn을 사용하면서 최종적으로 얻은 것은:

  • 일관된 스타일링 (Tailwind가 기대대로 동작)
  • 신뢰할 수 있는 페이지네이션 (고아 행이나 잘린 로고가 없음)
  • 서버‑사이드 렌더링 (폰트와 이미지가 항상 포함)
  • 쉬운 유지보수 (평범한 React 코드, 신비한 JSON 없음)

이제 재무팀은 매주 월요일에 버튼 하나만 클릭하면 완벽하게 포맷된 PDF를 받아볼 수 있게 되었고, 우리는 몇 주에 걸친 개발 시간을 되찾았습니다. 🎉

"Inter",
{ family: "Roboto Mono", weights: [400, 700] },
// Local fonts (embedded as base64)
{ family: "CustomFont", src: "./fonts/custom.woff2", weight: 400 },
]}
>
  
  
    
  

);
}

전체 화면 모드 진입

전체 화면 모드 종료

이 아키텍처는 PDF 템플릿을 유지 보수하고 디버깅하기 쉽게 만듭니다

pdfn이 다르게 느껴진 이유

  • React를 사용하고, JSON은 사용하지 않습니다
  • Tailwind를 사용하고, 맞춤 스타일링 시스템은 사용하지 않습니다
  • 페이지네이션이 기본 제공되며, CSS로 해킹하지 않습니다
  • 헤더와 푸터가 기본적으로 작동합니다
  • 스마트 페이지 나눔(행 중간이나 문단 중간에서 분할되지 않음)
  • 동적 페이지 번호(예: 페이지 1 of 5가 자동으로 작동합니다)
  • 실시간 미리보기 및 핫 리로드
  • 여백, 그리드, 페이지 나눔을 시각화하는 디버그 오버레이

코드는 읽기 쉬웠고, 출력은 예측 가능했습니다.

pdfn은 오픈 소스(MIT 라이선스)이며, 따라서 향후 공급업체 종속이나 예상치 못한 비용이 없습니다. 코드를 검토하고, 자체 호스팅하며, 필요에 맞게 조정하세요.

현재 상황

오늘, 우리의 전체 PDF 워크플로우가 완전히 자동화되었습니다. 청구서, 영수증, 계약서 및 온보딩 양식이 자동으로 생성됩니다. 재무팀은 더 이상 Google Docs를 열어 서식을 수정하고, 다운로드하거나 PDF를 수동으로 이메일로 보내지 않습니다.

TL;DR (피곤한 개발자를 위해)

  • PDFs는 겉보기와 달리 어렵다
  • HTML‑to‑PDF는 쉬워 보이지만 (그렇지 않다)
  • 브라우저 인쇄는 당신을 배신한다
  • 페이지 인식을 하는 도구가 중요하다

PDF 생성을 시작하려는 경우, 제 실수에서 교훈을 얻으세요.

PDF는 어렵습니다. 당신이 일을 못하는 것이 아닙니다.

우리가 이것을 공유하는 이유

pdfn은 비교적 새롭고 작지만 반응이 빠른 팀이 있습니다. 유지보수자들이 우리의 이슈에 답변했습니다. 이 라이브러리는 활발히 유지 관리되고 있습니다. MIT 라이선스이므로 종속되지 않습니다.

더 많은 개발자들이 존재한다는 것을 알아야 합니다.

이 도움이 되었다면:

  • ⭐ 저장소에 스타를 달아 주세요
  • 📝 사용해 보셨다면 사용 사례를 공유해 주세요
  • 🐛 문제를 보고하여 개선에 도움을 주세요
  • 💬 PDF 지옥에 빠진 팀과 이 글을 공유하세요

오픈 소스는 우리가 잘 작동하는 것을 공유할 때만 의미가 있습니다.

Community

Back to Blog

관련 글

더 보기 »

의존성 롤러코스터: NPM 테마 파크 탐색

모든 것을 시작하게 만든 “아하!” 순간 새로운 기능을 구현하고 있었는데, 마치 코드 마법사 🧙‍♂️가 된 기분이었다. PR을 제출했더니, 내 TL이 댓글을 달았다.

튜토리얼 정복

번역할 텍스트를 제공해 주시면 한국어로 번역해 드리겠습니다.

Go의 비밀스러운 삶: 테스트

13장: 진리의 테이블 수요일 비가 아카이브 창에 일정한 리듬으로 두드리며 맨해튼 스카이라인을 회색과 슬랫 같은 흐림으로 만들었다.