Print CSS와 PDF 생성: 웹 페이지를 완벽한 인쇄물로

디자인

Print CSSPDF 생성Puppeteer인쇄 최적화웹 디자인

이 글은 누구를 위한 것인가

  • 견적서, 인보이스, 보고서를 PDF로 제공해야 하는 팀
  • @media print로 화면과 인쇄물을 분리하려는 개발자
  • Puppeteer/Playwright로 서버사이드 PDF를 생성하려는 팀

들어가며

"인쇄하면 메뉴바가 그대로 나와요" — Print CSS 없이는 화면 UI가 그대로 인쇄된다. @media print로 내비게이션을 숨기고, page-break-*로 표가 페이지 중간에 잘리지 않게 한다. 서버사이드 PDF는 Puppeteer가 헤드리스 Chrome으로 렌더링한다.

이 글은 bluefoxdev.kr의 Print CSS PDF 생성 가이드 를 참고하여 작성했습니다.


1. Print CSS 핵심 패턴

[인쇄 시 숨길 요소]
  nav, header, footer (UI 내비게이션)
  .no-print 클래스
  fixed/sticky 포지션 요소
  배경 이미지/색상 (잉크 절약)
  버튼, 폼 컨트롤

[표시할 요소]
  href URL 표시: a[href]::after { content: " (" attr(href) ")" }
  인쇄용 추가 정보: .print-only { display: block; }

[페이지 나누기]
  page-break-before: always (강제 페이지 나누기)
  page-break-after: avoid (이후 나누기 방지)
  page-break-inside: avoid (내부 나누기 방지)
  orphans: 3 (페이지 하단 최소 줄 수)
  widows: 3 (페이지 상단 최소 줄 수)

[페이지 설정]
  @page { size: A4; margin: 20mm; }
  @page :first { margin-top: 30mm; }
  @page :left { margin-left: 25mm; }
  @page :right { margin-right: 25mm; }

2. Print CSS와 Puppeteer PDF 구현

/* 인쇄 미디어 쿼리 */
@media print {
  /* UI 요소 숨기기 */
  nav, .sidebar, .header-nav, .footer-nav,
  button, .no-print {
    display: none !important;
  }

  /* 페이지 설정 */
  @page {
    size: A4 portrait;
    margin: 20mm 15mm;
  }

  /* 배경 인쇄 허용 */
  * {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  /* 표 페이지 나누기 방지 */
  table { page-break-inside: avoid; }
  tr { page-break-inside: avoid; }
  thead { display: table-header-group; } /* 모든 페이지에 표 헤더 반복 */

  /* 제목은 다음 페이지로 분리 방지 */
  h1, h2, h3 { page-break-after: avoid; }

  /* 새 섹션 강제 페이지 나누기 */
  .page-break { page-break-before: always; }

  /* 링크 URL 표시 */
  a[href]:not([href^="#"])::after {
    content: " (" attr(href) ")";
    font-size: 0.8em;
    color: #666;
  }

  /* 인쇄 전용 표시 */
  .print-only { display: block !important; }

  /* 폰트 크기 조정 */
  body { font-size: 12pt; line-height: 1.5; }
  h1 { font-size: 20pt; }
  h2 { font-size: 16pt; }
}
// Puppeteer 서버사이드 PDF 생성
import puppeteer from 'puppeteer';

async function generateInvoicePDF(invoiceId: string): Promise<Buffer> {
  const browser = await puppeteer.launch({ headless: 'new' });
  const page = await browser.newPage();

  // HTML 렌더링 (내부 URL 또는 HTML 직접 전달)
  const invoiceHtml = await renderInvoiceHtml(invoiceId);
  await page.setContent(invoiceHtml, { waitUntil: 'networkidle0' });

  // PDF 생성
  const pdf = await page.pdf({
    format: 'A4',
    margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' },
    printBackground: true, // 배경색/이미지 포함
    displayHeaderFooter: true,
    headerTemplate: `
      <div style="font-size: 10px; width: 100%; text-align: right; padding-right: 15mm; color: #666;">
        <span>인보이스 #${invoiceId}</span>
      </div>
    `,
    footerTemplate: `
      <div style="font-size: 10px; width: 100%; text-align: center; color: #666;">
        <span class="pageNumber"></span> / <span class="totalPages"></span>
      </div>
    `,
  });

  await browser.close();
  return Buffer.from(pdf);
}

// Next.js API Route
export async function GET(req: Request, { params }: { params: { id: string } }) {
  const pdf = await generateInvoicePDF(params.id);
  return new Response(pdf, {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': `attachment; filename="invoice-${params.id}.pdf"`,
    },
  });
}

async function renderInvoiceHtml(id: string): Promise<string> {
  return `<!DOCTYPE html><html><body>Invoice ${id}</body></html>`;
}

마무리

Print CSS는 @media print 블록 하나로 인쇄 전용 스타일을 완전히 분리한다. page-break-inside: avoid로 표와 카드가 페이지 중간에 잘리지 않게 하고, thead { display: table-header-group }으로 긴 표의 헤더를 모든 페이지에 반복한다. 서버사이드 PDF는 Puppeteer/Playwright가 헤드리스 Chrome으로 렌더링해서 CSS가 완벽하게 적용된 PDF를 생성한다.