Vue 3.2에서 pdf.js 사용 시 발생하는 'Cannot read from private field' 오류 해결 방법

본 프로젝트에서는 Vue 3.2, Vite, TypeScript 조합을 사용 중이며, pdf.js 라이브러리를 활용하여 PDF 파일을 로드하고 미리보기 기능을 구현하려고 했습니다. 이 과정에서 발생한 문제점과 해결 방법을 공유합니다.

  1. pdf.js 설치

pdf.js 라이브러리는 npm을 통해 간단히 설치할 수 있습니다.

npm install pdfjs-dist
  1. 문제점 분석 및 해결

라이브러리를 프로젝트에 추가한 후, 콘솔에 다음과 같은 오류 메시지가 발생했습니다: Cannot read from private field

이 오류는 pdfDoc.getPage 메서드 사용 시 발생하며, Vue 3의 Proxy 구현과 관련이 있습니다.

Vue 2와 Vue 3의 데이터 관리 방식 차이:

  • Vue 2: defineProperty를 사용하여 데이터 변화 감지
  • Vue 3: Proxy 객체를 통해 데이터 변화 관리

pdfjs-dist 라이브러리는 내부적으로 특정 객체 유형을 검증하는 로직을 포함하고 있습니다. Vue 3에서는 이 검증 과정에서 Proxy 객체가 전달되어 오류가 발생하는 것입니다.

핵심 해결책: pdfDoc 변수는 반응형(reactive)으로 선언하지 마세요!

아래는 수정된 코드 예시입니다:

<template>
	<div class="pdf-viewer-container">
		<div class="pdf-display-area">
        <canvas :id="`pdfCanvas${currentPage}`" v-for="currentPage in totalPages" :key="currentPage"></canvas>
	    </div>
	    <div class="pdf-controls">PDF 조작 영역</div>
	</div>
</template>

<script lang="ts" setup>
import { reactive, onMounted, nextTick } from 'vue';
import { ElMessage } from 'element-plus';
import * as PDF from "pdfjs-dist";
import workerSrc from "pdfjs-dist/build/pdf.worker.entry.js";

const pdfState = reactive({
    pdfUrl: "/documents/sample.pdf", // public 폴더 내 PDF 파일 경로
    totalPages: 0, // 총 페이지 수
    pdfWidth: "", // PDF 너비
    zoomLevel: 1.0, // 확대/축소 비율
});

// 반응형이 아닌 일반 변수로 PDF 문서 객체 저장
let pdfDocument: any = null;

// 컴포넌트 마운트 시 초기화
onMounted(() => {
	initializePdfViewer();
});

// PDF 뷰어 초기화
const initializePdfViewer = () => {
    PDF.GlobalWorkerOptions.workerSrc = workerSrc;
    console.log("PDF 뷰어 로드 시작");
    loadPdfDocument(pdfState.pdfUrl);
};

// PDF 문서 로드
const loadPdfDocument = (url: string) => {
    const loadingTask = PDF.getDocument(url);
    loadingTask.promise.then((pdf: any) => {
      console.log('PDF 로드 완료');
      pdfDocument = pdf;
      pdfState.totalPages = pdf.numPages;
      nextTick(() => {
          renderPdfPage(1); // 첫 페이지 렌더링
      });
    });
}

// 특정 페이지 렌더링
const renderPdfPage = (pageNumber: number) => {
  console.log(`PDF 페이지 렌더링 시작: ${pageNumber}`);
  
  pdfDocument.getPage(pageNumber).then((page: any) => {
    console.log('페이지 로드 완료');
    const canvas = document.getElementById(`pdfCanvas${pageNumber}`);
    const context = canvas.getContext("2d");
    
    // 디바이스 픽셀 비율 계산
    const devicePixelRatio = window.devicePixelRatio || 1;
    const backingStoreRatio = 
        context.webkitBackingStorePixelRatio ||
        context.mozBackingStorePixelRatio ||
        context.msBackingStorePixelRatio ||
        context.oBackingStorePixelRatio ||
        context.backingStorePixelRatio ||
        1;
    
    const scaleRatio = devicePixelRatio / backingStoreRatio;
    const viewport = page.getViewport({ scale: pdfState.zoomLevel });
    
    // 캔버스 크기 설정
    canvas.width = viewport.width * scaleRatio;
    canvas.height = viewport.height * scaleRatio;
    canvas.style.width = "100%";
    canvas.style.height = "100%";
    
    pdfState.pdfWidth = `${viewport.width}px`;
    
    // 캔버스 컨텍스트 변환 설정
    context.setTransform(scaleRatio, 0, 0, scaleRatio, 0, 0);
    
    // PDF 페이지 렌더링
    const renderContext = {
        canvasContext: context,
        viewport: viewport,
    };
    
    page.render(renderContext);
    
    // 다음 페이지 렌더링 (재귀 호출)
    if (pdfState.totalPages > pageNumber) {
        renderPdfPage(pageNumber + 1);
    }
  });
}
</script>

<style lang="scss" scoped>
.pdf-viewer-container{
  .pdf-display-area {
    width: 80vw;
    height: 80vh;
    margin: 0 auto;
    border: 1px solid #f90;
    overflow: auto;
  }
  
  .pdf-controls {
    margin-top: 20px;
    text-align: center;
  }
}
</style>
  1. 대체 솔루션

서버에 pdfjs-viewer 파일 디렉토리를 배포하고 다음과 같은 형식으로 PDF를 미리보는 방법도 있습니다:

http://your-server/pdfjs-viewer/web/viewer.html?file=http%3A%2F%2Fyour-server%2Fdocuments%2Fsample.pdf

주의: 로컬 환경에서 테스트 시 CORS 오류가 발생하며, 실제 서버에 배포해야 정상적으로 작동합니다.

pdfjs 오류 메시지:
Uncaught (in promise) Error: file origin does not match viewer's

대체 구현 코드:

<PdfViewer v-if="selectedFile?.fileType?.includes('pdf')" :pdfPath="selectedFile?.url"></PdfViewer>

<!--
 * PDF 뷰어 컴포넌트
-->
<template>
	<div class="pdf-viewer">
		<iframe id="pdfFrame" :src="viewerUrl" frameborder="0" width="100%" height="100%"></iframe>
	</div>
</template>

<script setup lang="ts">
import { reactive, watch } from 'vue';

const props = defineProps({
	// PDF 파일 경로
	pdfPath: {
		type: String,
		default: () => '',
	},
});

const viewerState = reactive({
	pdfPath: '', // 로컬 PDF 파일 경로 (public 폴더 내)
	viewerBaseUrl: '/pdfjs-viewer/web/viewer.html', // pdfjs 뷰어 URL
	viewerUrl: '', // 최종 표시될 PDF URL
});

// PDF 경로 변경 감지
watch(
	() => props.pdfPath,
	(newPath: string) => {
		if (newPath) {
			initializeViewer(newPath);
		}
	},
	{
		immediate: true,
	}
);

// PDF 뷰어 초기화
const initializeViewer = (pdfPath: string) => {
    viewerState.viewerUrl = `${viewerState.viewerBaseUrl}?file=${encodeURIComponent(pdfPath)}`;
};
</script>

<style scoped lang="scss">
.pdf-viewer {
	width: 100%;
	height: 100%;
}
</style>

태그: Vue.js PDF.js JavaScript TypeScript PDF 렌더링

6월 28일 17:24에 게시됨