실무에서 PDF 문서 열람과 다운로드는 잦은 요구사항입니다. React에서 pdf.js 기반 라이브러리를 사용하면 빠르게 뷰어를 만들고, 인증이 필요한 파일도 안전하게 내려받을 수 있습니다. 아래 예제로 바로 적용해보세요.
1. 개요
목표는 다음과 같습니다: 브라우저 내 PDF 뷰어 구현, 페이지 이동/확대 기능 제공, 파일 다운로드 버튼 제공, 인증 및 CORS 고려, 모바일 대응과 성능 최적화입니다.
2. 설치 및 준비
pdf.js를 감싼 react-pdf 라이브러리를 사용합니다. 워커 설정이 필요합니다.
// 설치
// npm
// npm i react-pdf
// yarn
// yarn add react-pdf// pdf.js 워커 설정 (전역 1회)
import { pdfjs } from 'react-pdf';
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.js`;CDN 워커가 불편하면 빌드 도구에 맞게 pdf.worker를 번들링하세요. Next.js/Vite 환경에서는 동적 import 또는 public 경로 배치를 권장합니다.
3. 기본 PDF 뷰어 컴포넌트
Document와 Page를 사용해 첫 페이지를 렌더링합니다.
import React, { useState } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.js`;
export default function PdfViewer({ fileUrl }) {
const [numPages, setNumPages] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
return (
<div style={{ maxWidth: 900, margin: '0 auto' }}>
{loading && <p>문서를 불러오는 중입니다...</p>}
{error && <p>불러오기 실패: {String(error)}</p>}
<Document
file={fileUrl}
onLoadSuccess={({ numPages }) => { setNumPages(numPages); setLoading(false); }}
onLoadError={(e) => { setError(e.message); setLoading(false); }}
>
<Page pageNumber={1} renderTextLayer={false} renderAnnotationLayer={false} />
</Document>
{numPages && <p>총 {numPages} 페이지</p>}
</div>
);
}텍스트/주석 레이어 비활성화는 초반 성능에 도움이 됩니다. 접근성/검색이 필요하면 켜주세요.
4. 페이지 네비게이션 및 확대/축소
페이지 이동과 줌을 제공해 UX를 개선합니다.
import React, { useState } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.js`;
export function PdfViewerWithControls({ fileUrl }) {
const [numPages, setNumPages] = useState(0);
const [page, setPage] = useState(1);
const [scale, setScale] = useState(1.2);
const nextPage = () => setPage((p) => Math.min(p + 1, numPages));
const prevPage = () => setPage((p) => Math.max(p - 1, 1));
const zoomIn = () => setScale((s) => Math.min(s + 0.2, 3));
const zoomOut = () => setScale((s) => Math.max(s - 0.2, 0.5));
return (
<div>
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
<button onClick={prevPage} disabled={page <= 1}>이전</button>
<span>{page} / {numPages || '-'}</span>
<button onClick={nextPage} disabled={!numPages || page >= numPages}>다음</button>
<button onClick={zoomOut}>-</button>
<span>{Math.round(scale * 100)}%</span>
<button onClick={zoomIn}>+</button>
</div>
<Document
file={fileUrl}
onLoadSuccess={({ numPages }) => setNumPages(numPages)}
>
<Page
pageNumber={page}
scale={scale}
renderTextLayer={false}
renderAnnotationLayer={false}
/>
</Document>
</div>
);
}모바일 대응을 위해 컨테이너 너비에 비례한 scale 계산을 추가하면 좋습니다. ResizeObserver로 컨테이너 폭을 감지해 자동 확대/축소를 적용하세요.
5. 다운로드 버튼 구현
서버에서 PDF를 받아 blob으로 저장하고 파일명을 Content-Disposition 헤더에서 추출하는 방식을 권장합니다.
function getFilenameFromDisposition(disposition) {
if (!disposition) return null;
const match = /filename\*=UTF-8''([^;\n]+)/.exec(disposition) || /filename="?([^";\n]+)"?/.exec(disposition);
return match ? decodeURIComponent(match[1]) : null;
}
async function downloadPdf(fileUrl, authToken) {
const res = await fetch(fileUrl, {
headers: {
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
},
cache: 'no-store',
credentials: 'include', // 쿠키 기반 인증 시
});
if (!res.ok) throw new Error('다운로드 실패');
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const filename = getFilenameFromDisposition(res.headers.get('Content-Disposition')) || 'document.pdf';
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
}
// 버튼 예시
// <button onClick={() => downloadPdf('/api/files/123', token)}>PDF 다운로드</button>대용량 파일은 스트리밍 응답을 사용해도 브라우저 다운로드 동작에는 지장이 없습니다.
6. 서버 인증/보안 고려
인증이 필요한 경우:
- Bearer 토큰 또는 쿠키 기반 인증을 적용합니다. 위 예시처럼 Authorization 헤더 또는 credentials 옵션을 사용합니다.
- CORS 설정에서 Access-Control-Allow-Origin과 Access-Control-Expose-Headers: Content-Disposition을 설정해야 파일명 추출이 가능합니다.
- 다운로드 전 권한 체크를 서버에서 반드시 수행하고, 응답 헤더에 Content-Type: application/pdf, Content-Disposition: attachment; filename="..."를 설정합니다.
7. 모바일 및 성능 최적화
- 첫 렌더에서 renderTextLayer/renderAnnotationLayer를 끄고 필요 시 토글합니다.
- 페이지 이동 시 스크롤을 활용하는 여러 Page 렌더링은 비용이 큽니다. 한 페이지씩 렌더링하거나 가상화(보이는 페이지만 렌더) 전략을 사용합니다.
- 워커 경로를 CDN 대신 앱 내부에 두면 네트워크 의존성을 줄일 수 있습니다.
- 이미지가 많은 PDF에서는 scale을 과도하게 올리지 않도록 UX 가이드라인을 제공합니다.
8. 오류 처리와 UX
- 로딩 스피너, 재시도 버튼, 오류 메시지(네트워크/권한/손상 파일 구분)를 제공합니다.
- onLoadError, onSourceError 핸들러로 에러 로그를 수집하고 사용자에게 다음 액션(새로고침, 관리자 문의)을 안내합니다.
- 암호화된 PDF는 onPassword 콜백을 활용해 비밀번호 입력 UI를 제공할 수 있습니다.
9. 프레임워크/배포 시 주의점
- Next.js: SSR에서 pdf.js는 브라우저 API에 의존합니다. 동적 import로 클라이언트 전용 처리합니다.
// Next.js 예시
// pages/pdf-viewer.js
import dynamic from 'next/dynamic';
const PdfViewerWithControls = dynamic(() => import('../components/PdfViewerWithControls'), {
ssr: false,
});
export default function Page() {
return <PdfViewerWithControls fileUrl="/api/files/123" />;
}- Vite/CRA: 워커 파일 경로가 올바른지 확인합니다. public 폴더에 pdf.worker.min.js를 두고 상대 경로로 지정해도 됩니다.
10. 마무리
react-pdf를 사용하면 뷰어와 다운로드 기능을 빠르게 구축할 수 있습니다. 인증, CORS, 워커 설정만 정확히 잡으면 실무에서도 안정적으로 동작합니다. 위 컴포넌트를 프로젝트에 옮겨 적용하고, 성능과 모바일 UX를 단계적으로 개선해보세요.
'React' 카테고리의 다른 글
| React 앱에서 로컬 개발 환경과 프로덕션 환경 분리하기 (0) | 2026.05.27 |
|---|---|
| React에서 CSS-in-JS 라이브러리 비교 및 선택 가이드 (1) | 2026.05.27 |
| React에서 상태 관리 없이 URL 파라미터로 데이터 전달하기 (0) | 2026.05.26 |
| React 앱에서 Web Push 알림 기능 추가하기 (0) | 2026.05.26 |
| React에서 이미지 드래그 앤 드롭 정렬 기능 구현하기 (0) | 2026.05.25 |