PDF 미리보기 팝업 구현 기록
| 증상 | 원인 | 해결 |
|---|---|---|
| 미리보기 눌렀는데 파일이 다운로드됨 | 서버가 Content-Disposition: attachment로 응답. iframe은 이 헤더를 따름 | fetch로 받아 Blob URL로 변환 후 embed |
embed is not defined | UMD 클로저 내부 지역 함수 | PDFObject.embed로 호출 |
| 파일 열수록 메모리 증가 | Blob URL 미폐기 | revokeObjectURL |
| 모바일 빈 화면 | 모바일에 내장 PDF 뷰어 없음 + PDFObject가 UA로 차단 | PDF.js 필요 (별도 정리) |
핵심 한 줄: 다운로드/표시를 정하는 건 JS가 아니라 서버 응답 헤더. 문제 생기면 Network → Response Headers부터 확인.
1. 다운로드 문제
상황
// 처음 코드
const url = `/api/cm/fileDownload?apdflId=${id}&apdflSqno=${sqno}`;
embed(url, "#pdfViewBody");
_popup.open();
버튼 누르면 PDF가 다운로드 폴더에 저장되고, 팝업은 비어 있는 채로 뜸.
시도했다가 실패한 것
_popup.open()을 위로 올림 → 실패- iframe이 DOM에 붙는 순간 요청이 나가는 건 맞지만, 순서와 무관한 문제였음
원인
브라우저가 주소를 직접 열 때(주소창, 링크, iframe) 응답 헤더를 보고 판단함.
| 헤더 | 동작 |
|---|---|
Content-Disposition: attachment | 무조건 다운로드 (Content-Type 무관) |
Content-Type: application/octet-stream | 표시 불가 → 다운로드 |
inline + application/pdf | 표시 |
/api/cm/fileDownload는 다운로드 버튼용 공통 API라 attachment로 응답 중이었음.
해결
Content-Disposition은 브라우저가 주소를 열 때만 적용됨. fetch로 받으면 무시됨.
const res = await fetch(url);
if (!res.ok) throw new Error("HTTP " + res.status);
const buf = await res.arrayBuffer();
const blob = new Blob([buf], { type: "application/pdf" }); // 타입 강제 지정
const blobUrl = URL.createObjectURL(blob);
const viewer = document.createElement("embed");
viewer.src = blobUrl;
viewer.type = "application/pdf";
blob: 주소에는 Content-Disposition이 없음 → 뷰어로 열림.
⚠️ 이건 우회책. 파일 전체를 메모리에 올리고, 뷰어에서 저장 시 파일명이 임의 문자열이 됨. 정석은 서버에
inline+application/pdf로 응답하는 미리보기 전용 API를 따로 두는 것. 공통 API를 못 건드려서 우회함.
2. fetch를 두 번 await 하는 이유
const res = await fetch(url); // 헤더 도착까지
const buf = await res.arrayBuffer(); // 본문 전체 수신까지
- HTTP 응답은 헤더 → 본문 순으로 따로 도착
fetch의 Promise는 헤더 도착 시점에 완료 (표준)res를 받은 시점에res.body는 데이터가 아니라 통로(스트림)- 나눠져 있어서 본문 받기 전에
res.ok로 404 걸러낼 수 있음
3. 메모리 정리
Blob URL은 만들면 브라우저가 데이터를 계속 붙잡고 있음. 변수를 null로 만들거나 뷰어를 지워도 해제 안 됨. revokeObjectURL 호출하거나 페이지를 떠나야 풀림.
목록에서 여러 파일을 열어보는 화면이라 20MB × 10개 = 200MB 누적 가능.
_body.innerHTML = ""; // ① 뷰어 먼저 제거
if (_blobUrl) URL.revokeObjectURL(_blobUrl); // ② 그다음 주소 폐기
_blobUrl = URL.createObjectURL(blob);
순서 주의. ②를 먼저 하면 화면에 남아있는 뷰어가 참조하던 주소가 사라져서 순간 깨진 화면이 보임.
4. 모바일 (미해결, 별도 진행)
증상
PC는 정상. 폰에서는 빈 화면.
원인 두 가지
- 모바일 브라우저에 내장 PDF 뷰어가 없음. iframe/embed에 PDF를 넣어도 그려줄 주체가 없음. iOS는 첫 페이지만 이미지처럼, 안드로이드는 대개 빈 화면.
- PDFObject가 UA로 모바일을 차단함. 소스에
Mobi|Android|iPhone|iPad매칭되면 무조건 미지원 반환. 주석: "2023년 6월 기준 모바일 브라우저는 인라인 PDF를 제대로 지원하지 않음".
방향
PDF.js로 canvas에 직접 렌더링. 브라우저 뷰어를 안 쓰므로 기기 무관.
주의: 76페이지를 한 번에 다 그리면 메모리 수백 MB → iOS에서 탭 강제 종료. IntersectionObserver로 보이는 페이지만 그려야 함.