본 프로젝트에서는 Vue 3.2, Vite, TypeScript 조합을 사용 중이며, pdf.js 라이브러리를 활용하여 PDF 파일을 로드하고 미리보기 기능을 구현하려고 했습니다. 이 과정에서 발생한 문제점과 해결 방법을 공유합니다.
- pdf.js 설치
pdf.js 라이브러리는 npm을 통해 간단히 설치할 수 있습니다.
npm install pdfjs-dist
- 문제점 분석 및 해결
라이브러리를 프로젝트에 추가한 후, 콘솔에 다음과 같은 오류 메시지가 발생했습니다:
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>
- 대체 솔루션
서버에 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>