본문 바로가기

TIL

PDF.js를 활용해 PDF 임베드하기: pdfjs-dist 사용법

PDF.js를 활용해 PDF 임베드하기: pdfjs-dist 사용법

배경

회사 프로젝트에서 PDF 자료를 웹 페이지에 직접 임베드해야 하는 작업을 맡았다. 이 과정에서 다양한 PDF 뷰어 라이브러리를 조사한 뒤, 요구사항을 충족할 수 있는 POC를 진행하며 pdfjs-dist를 선택하게 되었다. 이 글에서는 다음 내용을 다루고자 한다. 

  1. pdfjs-dist를 선택한 이유
  2. pdfjs-dist 사용 방법
  3. 사용 중 발생한 문제와 해결 과정 (트러블슈팅)

왜 pdfjs-dist인가?

PDF 뷰어 라이브러리에는 대표적으로 pdfjs-dist, react-pdf, react-reader 등이 있다. 이 중에서 pdfjs-dist를 선택한 이유는 아래와 같다.

  1. 꾸준한 업데이트: 최근까지 유지보수가 활발히 이루어지고 있다. (마지막 업데이트: 10일 전)
  2. 높은 사용량: 커뮤니티의 신뢰도가 높아 관련 자료를 찾기 쉬웠다.
  3. 요구사항 충족: 프로젝트의 모든 기능적 요구사항을 만족했다.

 

 

 

요구사항 분석 및 해결

1. 뷰어 비율 조정 가능 여부

PDF 문서의 비율을 유지하거나, 지정된 비율로 스케일을 조정할 수 있어야 했다.

  • getViewport 메서드의 scale 값을 통해 간단히 조정 가능하다.
const scale = 1.5; 
const viewport = page.getViewport({ scale });
 
 

2. 확대 및 축소 기능

PDF 문서를 확대하거나 축소할 수 있는 인터랙션을 지원해야 했다.

  • scale 값을 동적으로 변경하여 구현할 수 있다.

3. 툴바 커스터마이징

다운로드 버튼, 인쇄 버튼 등 기본 툴바를 숨기고 PDF 문서만 노출하고 싶었다.

  • pdfjs-dist는 기본 툴바가 없으므로 완전히 커스터마이징된 UI를 제공할 수 있다.

4. 전체 페이지 렌더링

PDF의 모든 페이지를 로드하고 화면에 렌더링해야 했다.

  • pdfDocument.numPages를 통해 페이지 수를 파악한 뒤 반복적으로 렌더링 가능했다.

iframe과의 비교: 왜 pdfjs-dist인가?

iframe을 활용한 PDF 임베드는 설정이 간단하지만, PDF 뷰어의 툴바를 커스터마이징할 수 없다. 반면 pdfjs-dist는 완전히 제어 가능한 커스터마이징 옵션을 제공하기 때문에 요구사항에는 pdfjs-dist가 더 적합했다. 


pdfjs-dist 사용 방법

1. 라이브러리 설치

npm install pdfjs-dist
 

2. Worker 설정

PDF.js는 worker를 설정해야만 정상 작동하기 때문에 (그렇지 않으면 에러가 난다ㅠㅠ.. ) 아래 코드를 추가해주어야 한다. 

import * as pdfjs from 'pdfjs-dist'; 

pdfjs.GlobalWorkerOptions.workerSrc = 
new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();

3. PDF 문서 로드

getDocument를 사용해 PDF 데이터를 로드한다.

const pdfDoc = await pdfjs.getDocument(src).promise;
 
 

코드 구현

PDF 전체 렌더링 컴포넌트

아래 코드는 PDF 문서를 로드한 후, 각 페이지를 개별적으로 렌더링하는 컴포넌트이다. 

PDF.tsx

 
const PDF = ({ src }: PDFProps) => {
  const [numPages, setNumPages] = useState<number>(0);
  const [pdfDocument, setPdfDocument] = useState<PDFDocumentProxy | null>(null);

  useEffect(() => {
    const loadPdf = async () => {
      const pdfDoc = await pdfjs.getDocument(src).promise;
      setPdfDocument(pdfDoc);
      setNumPages(pdfDoc.numPages);
    };
    loadPdf();
  }, [src]);

  return (
    <div>
      {Array.from({ length: numPages }, (_, index) => (
        <PDFReader
          key={index}
          pageNum={index + 1}
          pdfDocument={pdfDocument!}
        />
      ))}
    </div>
  );
};
 

개별 페이지 렌더링

PDF의 페이지를 canvas에 렌더링한다. 

PDFReader.tsx

 
const PDFReader = ({ pageNum, pdfDocument }: PDFReaderProps) => {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);

  useEffect(() => {
    const renderPage = async () => {
      const page = await pdfDocument.getPage(pageNum);
      const viewport = page.getViewport({ scale: 1.5 });

      if (canvasRef.current) {
        const context = canvasRef.current.getContext('2d');
        canvasRef.current.width = viewport.width;
        canvasRef.current.height = viewport.height;

        await page.render({ canvasContext: context!, viewport }).promise;
      }
    };
    renderPage();
  }, [pageNum, pdfDocument]);

  return <canvas ref={canvasRef} />;
};

 

PDF.js 트러블슈팅: 문제와 해결 과정

 

프로젝트에서 pdfjs-dist를 사용하는 과정에서 예상치 못한 다양한 문제들을 마주했지만, 이를 해결하며 기능을 완성하였다. 아래는 주요 문제와 해결 과정을 정리한 내용이다.

문제 1: Canvas 버전 호환성 문제

상황

pdfjs-dist는 optional dependency로 canvas를 사용한다. 하지만 canvas가 2023년 4월 이후로 업데이트되지 않아 CI 환경에서 호환성 문제가 발생했다. 또한, pdf.js 팀은 향후 @napi-rs/canvas로 교체를 예고하고 있었다.

 

해결 방법

  1. 참고한 자료
    • pdf.js에서도 canvas 라이브러리가 업데이트가 잘 되지 않는 이슈로 @napi-rs/canvas로 교체 예정이라고 한다. mozilla/pdf.js#19015.
    • canvas 의존성을 선택적으로 제외하여 이슈해결한 코멘트: mozilla/pdf.js#15652 (comment).
  2. 해결 방법
    • npm i --omit=optional pdfjs-dist@특정버전 를 사용하여 필수 패키지만 설치.

테스트 결과 빌드 및 실행 과정에서 더 이상 에러가 발생하지 않음을 확인할 수 있었다. 

 

문제 2: 한글 폰트 렌더링 문제

상황

PDF에 포함된 한글 폰트가 렌더링되지 않고, 아래와 같은 경고 메시지가 콘솔에 출력되었다.

Warning: loadFont - translateFont failed: "UnknownErrorException: Ensure that the cMapUrl and cMapPacked API parameters are provided."
 

원인

  • PDF.js는 아시아계 폰트를 처리하기 위해 CMap 설정이 필요하지만, 기본적으로 이 값이 제공되지 않았다.

해결 방법

  1. 설정 추가
    아래와 같이 CMap 및 기본 폰트 경로를 추가하여 문제를 해결했다.
    • cMapUrl: 아시아 폰트 해석에 필요한 파일 경로.
    • cMapPacked: CMap 파일 압축 여부(true로 설정 시 파일 크기 최적화).
    • standardFontDataUrl: 오래된 PDF의 기본 폰트 경로.
const pdfDoc = await pdfjs.getDocument({
    url: src,
    cMapUrl: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.8.69/cmaps/',
    cMapPacked: true,
    standardFontDataUrl: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.8.69/standard_fonts/',
}).promise;

 

위 설정으로 한글 폰트가 정상적으로 렌더링되고, 경고 메시지가 더 이상 출력되지 않음을 확인할 수 있었다! 

 

문제 3: 대량 페이지 렌더링 시 성능 문제

상황

PDF 페이지 수가 많아질수록 모든 페이지를 한꺼번에 렌더링하면서 브라우저 성능 저하가 발생했다. 특히, 창 크기를 조정할 때 레이아웃이 깜박이는 문제가 있었다.

해결 과정

  1. 원인
    • 모든 페이지를 한 번에 로드하는 기존 방식은 렌더링 시간과 메모리 사용량을 증가시킴.
    • 브라우저 크기 변경 시, DOM 업데이트 과정에서 레이아웃 쉬프트가 발생.
  2. 해결 방법: Lazy Loading
    • IntersectionObserver를 사용하여 사용자가 보고 있는 페이지만 렌더링하도록 수정.
    • PDFReader 컴포넌트에 아래 로직을 추가:
const observer = new IntersectionObserver(
  ([entry]) => {
    setIsVisible(entry.isIntersecting);
  },
  { root: null, rootMargin: '0px', threshold: 0.1 }
);

 

 

3. 최적화된 렌더링 구현

  • 렌더링 로직
    현재 뷰포트에 보이는 페이지만 로드하도록 IntersectionObserver와 조건부 렌더링 적용
if (!isVisible) return;
const page = await pdfDocument.getPage(pageNum);
const viewport = page.getViewport({ scale });
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: context!, viewport }).promise;
  • 렌더링 취소 처리
    기존 작업이 취소되지 않으면 중복 렌더링 문제가 발생할 수 있으므로, renderTaskRef.current.cancel()로 이전 작업을 정리.

뷰포트에 포함된 페이지들만 렌더링을 함으로써 브라우저 크기 조정할 때에 레이아웃 쉬프트가 발생하지 않고, 초기 로딩도 이전보다 빨라져 렌더링 성능이 크게 향상됨을 확인할 수 있었다!

 

마무리 

이번 작업은 단순히 PDF를 띄우는 것으로 끝날 줄 알았지만, 예상보다 다양한 문제들을 마주하며 많은 것을 배울 수 있었다. 특히, 한글 폰트 문제나 성능 이슈를 해결하는 과정에서 예상치 못한 트러블슈팅을 경험하며 문제를 해결해 나가는 즐거움을 느낄 수 있었다.

처음엔 까다롭게 느껴졌던 pdfjs-dist도 설정을 하나씩 이해하고 적용하다 보니 점점 익숙해졌고, 이를 통해 문제를 풀어나가며 개인적으로 한 단계 성장한 것 같다는 생각이 든다!

 

이 경험이 다른 PDF 관련 프로젝트를 진행하시는 분들께 조금이나마 도움이 되길 바라며, 이만 글을 마칩니다! 😊