Next/server와 puppeteer을 활용한 PDF다운로드

Park Bumsoo·2025년 3월 21일
0

Next.js14 AppRouter

목록 보기
4/10

목적

html을 기반으로 pdf다운로드 기능을 구현해보기

사용 라이브러리

이름: puppteer

이유: 타 라이브러리보다 css에 대한 안정성이 좋았고 next.js내부 api로 호출하는 방식에 대해 호환성이 좋았다

주요 기능

  1. 의존성 임포트:
    • puppeteer: HTML을 PDF로 변환하는 데 사용.
    • NextRequest, NextResponse: Next.js 14 App Router의 API 핸들러를 정의하기 위한 객체.
    • fs, path: 파일 시스템과 경로를 조작하기 위한 Node.js 기본 모듈.
    • loggerSetting: 커스텀 로깅 설정 모듈.
  2. Base64 이미지 처리:
    • logo.svgsignature_example.svg 파일을 Base64로 인코딩하여 data:image/svg+xml;base64 형식으로 변환.
    • HTML 콘텐츠에서 이미지를 인라인으로 사용할 수 있게 함.

API 동작 흐름

1. PDF 생성 요청 처리

  • POST 메서드:
    • POST 요청을 처리하기 위해 작성됨.
    • 요청 본문에서 reservationDate를 포함한 데이터를 JSON 형식으로 받음.

2. 날짜 변환

  • 받은 reservationDate를 기준으로 1일 후 날짜를 계산함.

3. Puppeteer로 HTML 생성 및 PDF 변환

  • Puppeteer 브라우저 실행:
    • headless 모드로 브라우저를 실행.
    • -no-sandbox, -disable-setuid-sandbox 플래그는 보안 설정과 관련.
  • HTML 콘텐츠 설정:
    • PDF로 변환할 HTML 콘텐츠를 문자열로 정의.
    • await page.setContent(htmlContent)를 사용해 페이지에 설정.
  • PDF 생성:
    • page.pdf 메서드를 호출해 PDF를 생성.
    • A4 크기와 배경 포함 출력 설정.

4. PDF 반환

  • PDF 데이터를 NextResponse로 반환:
    • Content-Type: application/pdf로 PDF임을 명시.
    • Content-Disposition: attachment로 파일 다운로드를 유도.

서버 전체코드 (api/pdfDownload/route.ts)

import puppeteer from "puppeteer";
import { NextRequest, NextResponse } from "next/server";
import fs from "fs";
import path from "path";
import loggerSetting from "@/util/logging";

const logoPath = path.join(process.cwd(), "public/layout/logo.svg");
const signPath = path.join(
  process.cwd(),
  "public/dashboard/consulting/estimateSheet/signature_example.svg"
);
const logoBase64 = fs.readFileSync(logoPath).toString("base64");
const signBase64 = fs.readFileSync(signPath).toString("base64");
const logoSrc = `data:image/svg+xml;base64,${logoBase64}`;
const signSrc = `data:image/svg+xml;base64,${signBase64}`;

export async function POST(req: NextRequest): Promise<NextResponse> {
  const { logger } = loggerSetting();

  let today = new Date();
  today.setHours(today.getHours() + 9);
  const logDate = today.toISOString();
  try {
    const body = await req.json();
    const reservationDate = new Date(body.reservationDate);
    const transMiliSecond = new Date(reservationDate).setDate(
      new Date(reservationDate).getDate() + 1
    );
    const transDate = new Date(transMiliSecond);

    const browser = await puppeteer.launch({
      headless: true,
      args: ["--no-sandbox", "--disable-setuid-sandbox"],
    });

		
    
    const page = await browser.newPage();
    const htmlContent = `
	    <html lang="ko">
	      // ...html내용
		  </html>
    `;
    
    // 폰트가 깨지지 않게 폰트 직접지정
    await page.addStyleTag({
      url: "https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&display=swap",
    });

    // PDF로 변환할 HTML 내용 설정
    await page.setContent(htmlContent);

    const pdfBuffer = await page.pdf({
      format: "A4",
      printBackground: true,
    });

    await browser.close();

    // PDF 반환
    return new NextResponse(pdfBuffer, {
      headers: {
        "Content-Type": "application/pdf",
        "Content-Disposition": "attachment; filename=document.pdf",
      },
    });
  } catch (error) {
    const logDate = new Date().toISOString();
    const errMessage = error instanceof Error ? error.message : "Unknown error";
    const errStack =
      error instanceof Error ? error.stack : "No stack available";

    const errString = `dateTime=${logDate} msg=${errMessage}, stack=${errStack}, url='/api/pdfDownload', method=POST`;

    console.error("PDF Generation Error:", error);
    logger.error({ errString });
    return new NextResponse("Failed to generate PDF", { status: 500 });
  }
}

프론트엔드 호출 (.tsx)

  const onClickEstimateSheet = async (data) => {
    try {
      const requestBody = {
        ...data
      };
      // 호출영역
      const response = await fetch("/api/pdfDownload", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(requestBody), // JSON 형태로 전송
      });

      if (!response.ok) {
        throw new Error("Failed to fetch PDF");
      }

      const blob = await response.blob();
      const link = document.createElement("a");
      link.href = URL.createObjectURL(blob);
      link.download = "document.pdf";
      link.click();
      URL.revokeObjectURL(link.href); // 메모리 정리
    } catch (error) {
      console.error("PDF Download Error:", error);
    }
  };

주의사항

해당 라이브러리는 브라우저 의존성에 따른 환경별 추가 라이브러리가 필요하다

위 코드는 localhost에서는 정상적인 동작을 하지만 실제 IP나 도메인에선 기능이 동작하지 않는다.

따라서 아래의 코드로 ssh에 접속하여 필요한 의존성 라이브러리를 설치를 해주어야한다.

ssh ubuntu@${TARGET_IP} "
sudo apt-get update &&
sudo apt-get install -y \
    libatk1.0-0 \
    libatk-bridge2.0-0 \
    libcups2 \
    libdrm2 \
    libgbm1 \
    libnspr4 \
    libnss3 \
    libxcomposite1 \
    libxdamage1 \
    libxrandr2 \
    libasound2 \
    libx11-xcb1 \
    libxtst6 \
    libxshmfence1 \
    libegl1 \
    libjpeg-dev \
    libxkbcommon0 \
    gconf-service \
    libappindicator1 \
    libappindicator3-1 \
    fonts-liberation \
    lsb-release \
    xdg-utils
"

젠킨스를 통한 설정도가능한데 이 방법은 젠킨스 빌드시 항시 동작하기에 배포 속도에 영향을 끼친다

    stages {
        stage('Git Clone Project') {
            steps {
                cleanWs()
                git credentialsId: "${REPOSITORY_CREDENTIAL_ID}", branch: "${TARGET_BRANCH}", url: "${REPOSITORY_URL}"
            }
        }
        stage('Install Puppeteer Dependencies') {
            steps {
                sshagent (credentials: ['key-jenkins']) {
                    sh '''
                        echo "🔄 Installing Puppeteer dependencies on ${TARGET_IP}"
                        ssh ubuntu@${TARGET_IP} "
                        sudo apt-get update &&
                        sudo apt-get install -y \
                          libatk1.0-0 \
                          libatk-bridge2.0-0 \
                          libcups2 \
                          libdrm2 \
                          libgbm1 \
                          libnspr4 \
                          libnss3 \
                          libxcomposite1 \
                          libxdamage1 \
                          libxrandr2 \
                          libasound2 \
                          libx11-xcb1 \
                          libxtst6 \
                          libxshmfence1 \
                          libegl1 \
                          libjpeg-dev \
                          libxkbcommon0 \
                          gconf-service \
                          libappindicator1 \
                          libappindicator3-1 \
                          fonts-liberation \
                          lsb-release \
                          xdg-utils &&
                        echo '✅ Puppeteer dependencies installed successfully!'
                        "
                    '''
                }
            }
        }
profile
프론트엔드 개발자 ( React, Next.js ) - 업데이트 중입니다.

0개의 댓글