이 글은 누구를 위한 것인가
- 견적서, 인보이스, 보고서를 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를 생성한다.