Next.js에서 pdf다운하기
2025-01-16그동안 PDF다운로드 기능은 html2canvas와 jspdf를 사용해서 클라이언트측에서 구현했지만, 이번에 사이드 프로젝트를 하면서 서버에서 구현을 하고 싶어 ChatGPT도움을 받아 로직을 구성했습니다.
기본적인 흐름은 다음과 같은데요
- 프론트에서 HTML생성
- 생성된 HTML을 서버로 전송
- 서버에서 PDF파일 구성 후 프론트로 다시 전달
위와 같은 흐름을 생각하고 GPT에게 요청하니 puppeteer 라이브러리를 추천해주었습니다.
puppeteer란?
puppeteer는 Headless Chrome 또는 Chromium 브라우저를 제어하기 위한 Node.js 라이브러리로 구글이 만들었습니다. 주로 웹 스크래핑 혹은 테스트에 사용한다고 하는데 pdf생성해도 딱 맞아서 이번에 사용해보기로 결정했습니다.
클라이언트 로직 구성
클라이언트 측 로직은 간단한데요, pdf로 만들기 원하는 HTML을 서버로 전송 후, response를 a태그를 생성해서 다운로르 하면 끝입니다.
const handleDownloadPDF = async () => {
const input = document.getElementById("pdf-content");
if (input) {
const htmlContent = input.outerHTML;
const response = await fetch("/api/pdf", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ htmlContent }),
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "download.pdf";
document.body.appendChild(a);
a.click();
a.remove();
} else {
console.error("Error generating PDF");
}
}
};
서버 로직 구성
서버는 조금 복잡합니다. (사실 next.js 15버전으로 처음 구현하느라 공식문서를 참고하는 시간이 더 길었네요)
전체코드는 아래와 같습니다.
import { NextRequest, NextResponse } from "next/server";
import puppeteer from "puppeteer";
// 응답을 캐싱하지 않고 매번 재응답
export const dynamic = "force-static";
export async function POST(req: NextRequest) {
const { htmlContent } = await req.json();
if (!htmlContent) {
return NextResponse.json(
{ message: "HTML content is required" },
{ status: 400 }
);
}
try {
const browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
// HTML 콘텐츠 설정
await page.setContent(htmlContent, { waitUntil: "load" });
// PDF 생성
const pdfBuffer = await page.pdf({
format: "A4",
printBackground: true,
margin: { top: "20mm", bottom: "20mm", left: "10mm", right: "10mm" },
});
await browser.close();
// PDF 반환
const response = new NextResponse(pdfBuffer, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": "attachment; filename=generated.pdf",
},
});
return response;
} catch (error) {
console.error("Error generating PDF:", error);
return NextResponse.json(
{ message: "Failed to generate PDF" },
{ status: 500 }
);
}
}
위 코드는 언핏보면 잘 동작하는 것 같지만 두가지 문제가 있었어요
이미지가 안 보이네?
위 코드를 실행하니 이미지가 모두 깨져서 출력되는 문제가 발생했습니다. 우선 프론트 로직 구조상 이미지는 모두 내부파일을 사용하고 있었습니다. 그래서 경로는 "/icons/testImage.svg"와 같이 상대경로 되어있었죠. 여기서 문제가 발생했습니다.
<img src="/techstack/back/nestjs.svg">
서버로 넘겨주는 HTML의 img태그의 src경로가 상대경로로 되어있어 이미지 인식을 못했던겁니다.
즉, 서버에서 이미지 경로를 절대경로로 변경해야 하는거죠
htmlContent변수를 let으로 수정하고 다음과 같이 상대경로를 절대경로로 변경해주면 됩니다.
const baseUrl = "http://서버주소"; //
htmlContent = htmlContent.replace(/src="\/(.*?)"/g, `src="${baseUrl}/$1"`);
CSS와 폰트 어디갔니?
해당 프로젝트는 tailwind를 사용하고 있었는데요. 이 때문에 pdf를 출력하면 CSS가 모두 초기화되는 현상이 나타났습니다. 서버에선 tailwind가 없어서 className을 인식하지 못해서 당연한 현상이였죠
수정은 간단합니다. tawilwind CDN을 HTML에 추가해주고, 원하는 폰트 또한 스타일에 추가시켜주면 됩니다.
// Tailwind CDN 추가
const tailwindCDN = `
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; }
</style>
`;
htmlContent = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
${tailwindCDN}
</head>
<body>
${htmlContent}
</body>
</html>
`;
이로써 서버측에서 pdf를 생성하는 로직을 구현 완료했습니다. 아직 다듬을 부분이 더 있지만, 이는 추후 다시 수정해봐야겠네요.