PDF.js를 활용해 PDF 임베드하기: pdfjs-dist 사용법
배경
회사 프로젝트에서 PDF 자료를 웹 페이지에 직접 임베드해야 하는 작업을 맡았다. 이 과정에서 다양한 PDF 뷰어 라이브러리를 조사한 뒤, 요구사항을 충족할 수 있는 POC를 진행하며 pdfjs-dist를 선택하게 되었다. 이 글에서는 다음 내용을 다루고자 한다.
- pdfjs-dist를 선택한 이유
- pdfjs-dist 사용 방법
- 사용 중 발생한 문제와 해결 과정 (트러블슈팅)
왜 pdfjs-dist인가?
PDF 뷰어 라이브러리에는 대표적으로 pdfjs-dist, react-pdf, react-reader 등이 있다. 이 중에서 pdfjs-dist를 선택한 이유는 아래와 같다.
- 꾸준한 업데이트: 최근까지 유지보수가 활발히 이루어지고 있다. (마지막 업데이트: 10일 전)
- 높은 사용량: 커뮤니티의 신뢰도가 높아 관련 자료를 찾기 쉬웠다.
- 요구사항 충족: 프로젝트의 모든 기능적 요구사항을 만족했다.
요구사항 분석 및 해결
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로 교체를 예고하고 있었다.
해결 방법
- 참고한 자료
- pdf.js에서도 canvas 라이브러리가 업데이트가 잘 되지 않는 이슈로 @napi-rs/canvas로 교체 예정이라고 한다. mozilla/pdf.js#19015.
- canvas 의존성을 선택적으로 제외하여 이슈해결한 코멘트: mozilla/pdf.js#15652 (comment).
- 해결 방법
- 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 설정이 필요하지만, 기본적으로 이 값이 제공되지 않았다.
해결 방법
- 설정 추가
아래와 같이 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 페이지 수가 많아질수록 모든 페이지를 한꺼번에 렌더링하면서 브라우저 성능 저하가 발생했다. 특히, 창 크기를 조정할 때 레이아웃이 깜박이는 문제가 있었다.
해결 과정
- 원인
- 모든 페이지를 한 번에 로드하는 기존 방식은 렌더링 시간과 메모리 사용량을 증가시킴.
- 브라우저 크기 변경 시, DOM 업데이트 과정에서 레이아웃 쉬프트가 발생.
- 해결 방법: 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 관련 프로젝트를 진행하시는 분들께 조금이나마 도움이 되길 바라며, 이만 글을 마칩니다! 😊
'TIL' 카테고리의 다른 글
You Don't Know JS 책 공부 시작 -! (0) | 2025.03.01 |
---|---|
좋은 코드의 기준이란? feat. 토스 Frontend Fundamentals (3) | 2025.02.01 |
[React 까보기 시리즈] React concurrent mode 맛보기? performConcurrentWorkOnRoot 까보기 (0) | 2024.11.07 |
[React 까보기 시리즈] 리액트가 WORK를 스케줄링하는 과정을 까보자 (2) | 2024.11.07 |
[React 까보기 시리즈] Virtual DOM 의 맨 위에는 어떤 node 가 있을까? root node 까보기(feat. CRA) (0) | 2024.11.07 |