html2canvas와 jsPDF로 PDF 결과지 생성하기

짜장킴·2026년 7월 12일

실무

목록 보기
15/16
  • 견적 결과지 화면을 PDF로 생성해 서버에 저장하고, 이후 카카오톡으로 전송하는 기능을 구현했다.
  • 화면에 보이는 UI를 그대로 PDF로 만들어야 했기 때문에 html2canvas-pro로 DOM을 이미지처럼 캡처하고, jsPDF를 이용해 PDF 파일로 변환하는 방식을 사용했다.

구현 흐름

  1. 결과지 영역 DOM 참조
  2. PDF 캡처용 스타일 적용
  3. html2canvas로 화면 캡처
  4. 캡처 이미지를 jsPDF에 추가
  5. PDF Blob을 File 객체로 변환
  6. 백엔드 저장 API 호출
npm install html2canvas-pro jspdf
const canvas1 = await html2canvas(contentRef.current, {
  scale: 2,
  useCORS: true,
  backgroundColor: "#ffffff",
  windowWidth: 1200,
});

const pdfBlob = pdf.output("blob");

const file = new File([pdfBlob], "estimate.pdf", {
  type: "application/pdf",
});

캡처 영역 분리

  • 각 영역을 별도로 캡처한 뒤 PDF의 1페이지, 2페이지로 추가했다.
const canvas1 = await html2canvas(contentRef.current, ...);
const canvas2 = await html2canvas(commentRef.current, ...);

PDF 캡처용 스타일 적용

  • 캡처 직전에 PDF 전용 클래스를 추가했다.
contentRef.current.classList.add("pdf-capture-mode");
commentRef.current.classList.add("pdf-capture-mode");
.pdf-capture-mode,
.pdf-capture-mode * {
  font-family:
    -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
    Arial, "Apple SD Gothic Neo", "Malgun Gothic", sans-serif !important;

  letter-spacing: 0px !important;
}
  • 프로젝트의 기본 폰트는 Pretendard Variable이었다.
  • 브라우저 화면에서는 문제가 없었지만, html2canvas로 캡처할 때는 Variable Font가 기대한 것과 다르게 렌더링될 수 있었다.
  • 특히 PDF 캡처 과정에서는 다음과 같은 문제가 발생할 수 있다.
    - 글자 간격이 화면과 다르게 보임
    - 일부 텍스트가 줄바꿈됨
    - 한글 폰트가 깨지거나 두껍게 보임
    - PDF 이미지 캡처 결과가 브라우저 화면과 달라짐
    - 숫자의 line height가 이상해짐
  • 그래서 PDF 캡처 시에는 Variable Font 대신 시스템 폰트 fallback을 사용하도록 변경했다.
  • letter-spacing:0px !important : 이는 캡처 시 글자 폭이 달라지면서 레이아웃이 밀리는 문제를 줄이기 위한 처리였다.

캡처 전 레이아웃 고정

  • PDF는 화면 크기에 따라 레이아웃이 달라지면 안 된다.
  • 그래서 캡처 직전에 컨테이너 너비를 고정했다.
containerRef.current.style.width = "1600px";
containerRef.current.style.maxWidth = "1600px";
containerRef.current.style.minWidth = "1600px";
  • 캡처가 끝난 뒤에는 원래 스타일로 복구했다.
containerRef.current.style.width = "";
containerRef.current.style.maxWidth = "";
containerRef.current.style.minWidth = "";

폰트 로딩 대기

  • PDF를 생성하기 전에 폰트 로딩이 끝날 때까지 기다렸다.
await document.fonts.ready;
await new Promise((resolve) => setTimeout(resolve, 100));
  • 폰트가 완전히 로드되기 전에 캡처하면 fallback 폰트로 먼저 캡처될 수 있기 때문에, 렌더링이 안정화된 뒤 캡처하도록 처리했다.

이미지 품질 설정

  • html2canvas 캡처 시 scale 값을 2로 설정했다.
scale: 2,
  • scale이 낮으면 PDF에서 텍스트나 그래프가 흐릿하게 보일 수 있다.
  • 그래서 캡처 해상도를 높여 PDF에서도 비교적 선명하게 보이도록 했다.
useCORS: true,
  • 결과지에 외부 이미지가 포함될 가능성이 있어 true 옵션을 적용하였다.
  • 이를 통해 CDN, S3 등 다른 도메인에서 제공되는 이미지도 html2canvas가 정상적으로 읽어 PDF에 포함할 수 있도록 구성하였다.

PDF 생성

  • 캡처한 canvas는 이미지 데이터로 변환한 뒤 PDF에 추가했다.
const imgData1 = canvas1.toDataURL("image/jpeg", 1.0);

pdf.addImage(
  imgData1,
  "JPEG",
  0,
  0,
  pageWidth,
  calculatedHeight
);
  • 첫 번째 영역을 추가한 뒤 새로운 페이지를 생성하고, 두 번째 캡처 이미지를 추가했다.
pdf.addPage([pageWidth, calculatedHeight], "p");

중복 저장 방지

  • 저장 버튼을 여러 번 클릭하면 동일한 PDF가 중복 생성되거나 API가 중복 호출될 수 있다.
  • 그래서 useRef를 이용해 저장 중 여부를 관리했다.
if (isSavingRef.current) return;

isSavingRef.current = true;
setIsSaving(true);
  • useState만 사용할 경우 상태 반영 타이밍 때문에 빠른 연속 클릭을 완전히 막기 어려울 수 있다.
  • 그래서 즉시 값이 반영되는 useRef를 함께 사용했다.

네트워크 예외 처리

  • PDF 저장 전 네트워크 상태도 확인했다.
if (!navigator.onLine) {
  setFailSaveNetworkModal(true);
  return;
}
  • 오프라인 상태에서는 PDF 생성 이후 서버 저장에 실패할 수 있기 때문에, 저장 전에 먼저 네트워크 상태를 먼저 확인한다.

마무리

  • PDF 캡처용 폰트 적용
  • letter-spacing 초기화
  • 캡처 전 컨테이너 너비 고정
  • 폰트 로딩 대기
  • scale 설정으로 화질 개선
  • 페이지 영역 분리
  • 중복 저장 방지
  • 등의 기능 적용
profile
프론트엔드

0개의 댓글