React 프로젝트 내에서 pdf.js를 이용하여 pdf 뷰어 구현

JH.P·2023년 2월 6일

pdf.js란?

  • PDF 문서를 파싱하고, 렌더링 하기 위한 웹 기반 범용 표준 플랫폼
  • 즉, PDF 문서를 파싱하여, 렌더링 하기위한 라이브러리

pdf.js의 구조

다음과 같이 3가지의 요소로 이루어져있다.

  • Core
  • Display
  • Viewer

Core부터 공식문서를 통해 먼저 살펴보자.
The core layer is where a binary PDF is parsed and interpreted. This layer is the foundation for all subsequent layers. It is not documented here because using it directly is considered an advanced usage and the API is likely to change. For an example of using the core layer see the PDF Object Browser.
core 계층에서는 binary로 구성된 PDF파일이 파싱되고 해석된다고 한다. 그리고 해당 계층은 다른 후속 계층들의 가장 근본이 되는 계층이다.

그 다음 Display 게층
The display layer takes the core layer and exposes an easier to use API to render PDFs and get other information out of a document. This API is what the version number is based on.
Core 계층을 가져오고, PDF를 렌더링하고 문서에서 다른 정보를 가져오기 위하여 사용하기 용이한 API들을 보이는 역할을 수행한다. 해당 API는 버전 번호의 근간이 된다.

마지막으로 Viewer 계층이다.
The viewer is built on the display layer and is the UI for PDF viewer in Firefox and the other browser extensions within the project. It can be a good starting point for building your own viewer. However, we do ask if you plan to embed the viewer in your own site, that it not just be an unmodified version. Please re-skin it or build upon it.
Viewer 계층은 Display 계층에서 만들어지며, 파이어폭스, 다른 확장 브라우저 내 PDF 뷰어를 위한 UI이다. 사용자만의 뷰어를 만들기 위한 시작점이 될 수도 있다.

구현을 위한 초기 시도

  • 공식문서에 나와있는 대로 따라해보았다.
  1. Prebuilt 프로젝트를 다운로드 받았다.
  2. 공식문서에 나와있는 대로, 브라우저 내에서 web/viewer.html 를 실행하여 test PDF가 잘 로드되는지 확인해보았다.
  3. 하지만 로드가 잘 되지 않았고, 원인을 찾아보니 CORS 에러가 발생하였다.
  4. 공식문서에는 로드가 잘 이루어지지 않으면, Server를 이용하라고 한다.
  5. 마찬가지로 해당 현상을 구글링해보았으며, 서버 측에서 실행해야 해당 현상을 해결할 수 있다는 사실을 확인하였다.
  6. 내가 생성한 프로젝트는 클라이언트만 존재하기 때문에, 다른 방법을 통해 구현을 시도했다.

React.js 내에서 pdf 렌더링

해당 글을 참고하였다.
https://stackoverflow.com/questions/55371402/how-to-reference-pdf-js-library-in-react

먼저 아래와 같이, pdj.js 를 설치한다.

npm install pdfjs-dist

pdf 뷰어 역할을 수행할 컴포넌트를 생성한 뒤, 필요한 모듈들을 import 한다.

import React, { useEffect, useState, useRef, useCallback } from 'react';
import Style from './pdfView.style';
import * as pdfjsLib from 'pdfjs-dist/webpack';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';

여기서 3번째 줄은, 기존에는 다음과 같았다.

import pdfjsLib from 'pdfjs-dist/webpack';

하지만, undefined 에러가 발생하였고, 위 내용와 같이 수정하니 해결되었다. 모듈 내 모든 파일들을 '*'로 import 하여, pdfjsLib라 명명하여 해결한 것이다. 아마 pdfjsLib 라는 이름이 잘못된 것 같다.

canvas를 참조할 Ref를 생성한다.

 const canvasRef = useRef();

그리고 다음 코드를 반드시 추가해야한다. 추가하지 않으면 에러가 발생한다. workerSrc의 속성을 지정해주는 코드이다.

 pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;

그리고 pdf 정보를 담을 useState, 현재 페이지 정보를 담을 useState를 생성하자. 초기 페이지 값은 1이다.

 const [pdfRef, setPdfRef] = useState();
 const [currentPage, setCurrentPage] = useState(1);

그 뒤, PDF, 페이지 정보를 이용하여 페이지에 렌더링 하는 함수를 useCallback을 이용하여 생성한다.

  const renderPage = useCallback(
    (pageNum, pdf = pdfRef) => {
      pdf &&
        pdf.getPage(pageNum).then(function (page) {
          const viewport = page.getViewport({ scale: 1.3 });
          const canvas = canvasRef.current;
          canvas.height = viewport.height;
          canvas.width = viewport.width;
          const renderContext = {
            canvasContext: canvas.getContext('2d'),
            viewport: viewport,
          };
          page.render(renderContext);
        });
    },
    [pdfRef]
  );

pdf.js는 비동기 promise 함수를 이용하여 구현된다. 따라서 위에서 처럼 then을 이용하여 동작을 이어나가는 것을 확인할 수 있다.
pdf 정보로부터 인자로 받은 pageNum에 해당하는 page를 먼저 얻는다.
그리고, 조회할 화면의 크기를 getViewport를 이용하여 설정하였고, canvas를 이용하여 렌더링하게 되기 때문에 방금 설정한 화면 크기를 이용하여 canvas의 width, height를 설정한다.
그리고 방금 입력한 정보를 renderContext 내에 속성 값을 지정하고, canvasContext 값을 추가하여 마지막으로 해당 정보를 이용하여 렌더링을 진행하게 된다.
참고) canvas는 웹에서 그래픽적인 요소를 다룰 때 사용되는 태그이다. 여기서는 2D 뷰어를 렌더링하기 때문에 위와 같은 값을 입력하였다.

그 다음, props로 받게 될 pdf의 url을 이용하여 pdf 정보를 위에서 생성했던 useState에 갱신시키는 useEffect 구문이다.

 useEffect(() => {
    const loadingTask = pdfjsLib.getDocument(url);
    loadingTask.promise.then(
      (loadedPdf) => {
        setPdfRef(loadedPdf);
      },
      function (reason) {
        console.error(reason);
      }
    );
  }, [url]);

pdfjsLib의 getDocument를 이용하였으며, 위와 마찬가지로 비동기 promise를 이용한 것을 확인할 수 있다. 로드가 성공하면 pdfRef에 해당 정보를 갱신시킨다.

페이지 정보, pdf 정보가 변경될 때마다 위에서 만들었던 pdf 렌더링 함수를 실행시킬 useEffect 구문

  useEffect(() => {
    renderPage(currentPage, pdfRef);
  }, [pdfRef, currentPage, renderPage]);

그리고 현재 페이지 이동을 위한 함수이다.

  const nextPage = () =>
    pdfRef && currentPage < pdfRef.numPages && setCurrentPage(currentPage + 1);

  const prevPage = () => currentPage > 1 && setCurrentPage(currentPage - 1);

아래와 같이 렌더링한다. canvas 태그를 이용하여 PDF를 렌더링 한 것을 확인할 수 있다.

  return (
    <>
      <Style.ButtonsBox>
        <Style.Button onClick={prevPage}>이전 페이지</Style.Button>
        <Style.CurrentPage>현재 페이지 : {currentPage}</Style.CurrentPage>
        <Style.Button onClick={nextPage}>다음 페이지</Style.Button>
      </Style.ButtonsBox>
      <canvas ref={canvasRef}></canvas>
    </>
  );

PDF 뷰어 컴포넌트 최종 코드

import React, { useEffect, useState, useRef, useCallback } from 'react';
import Style from './pdfView.style';
import * as pdfjsLib from 'pdfjs-dist/webpack';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';

export default function PdfViewer({ url }) {
  const canvasRef = useRef();
  pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;

  const [pdfRef, setPdfRef] = useState();
  const [currentPage, setCurrentPage] = useState(1);

  const renderPage = useCallback(
    (pageNum, pdf = pdfRef) => {
      pdf &&
        pdf.getPage(pageNum).then(function (page) {
          const viewport = page.getViewport({ scale: 1.3 });
          const canvas = canvasRef.current;
          canvas.height = viewport.height;
          canvas.width = viewport.width;
          const renderContext = {
            canvasContext: canvas.getContext('2d'),
            viewport: viewport,
          };
          page.render(renderContext);
        });
    },
    [pdfRef]
  );

  useEffect(() => {
    renderPage(currentPage, pdfRef);
  }, [pdfRef, currentPage, renderPage]);

  useEffect(() => {
    const loadingTask = pdfjsLib.getDocument(url);
    loadingTask.promise.then(
      (loadedPdf) => {
        setPdfRef(loadedPdf);
      },
      function (reason) {
        console.error(reason);
      }
    );
  }, [url]);

  const nextPage = () =>
    pdfRef && currentPage < pdfRef.numPages && setCurrentPage(currentPage + 1);

  const prevPage = () => currentPage > 1 && setCurrentPage(currentPage - 1);

  return (
    <>
      <Style.ButtonsBox>
        <Style.Button onClick={prevPage}>이전 페이지</Style.Button>
        <Style.CurrentPage>현재 페이지 : {currentPage}</Style.CurrentPage>
        <Style.Button onClick={nextPage}>다음 페이지</Style.Button>
      </Style.ButtonsBox>
      <canvas ref={canvasRef}></canvas>
    </>
  );
}

마지막으로, pdf 페이지에서 해당 컴포넌트를 이용하여 렌더링하면 된다. 로컬에 저장된 pdf 파일을 이용하였다.

import PDFViewer from '../components/pdfViewer/pdfViewer';
import ContentsLayout from '../UI/contentsLayout';
import file from '../pdf_sample.pdf';

const PdfsPage = () => {
  return (
    <ContentsLayout>
      <PDFViewer url={file} />
    </ContentsLayout>
  );
};

export default PdfsPage;

완성본

공개된 블로그인 관계로, pdf 파일 내용은 가리고 업로드하였다.

profile
꾸준한 기록

0개의 댓글