트러블슈팅›DevOps•
3분
•

PDF 미리보기 팝업 구현 기록

PDF 미리보기 팝업 구현 기록
증상원인해결
미리보기 눌렀는데 파일이 다운로드됨서버가 Content-Disposition: attachment로 응답. iframe은 이 헤더를 따름fetch로 받아 Blob URL로 변환 후 embed
embed is not definedUMD 클로저 내부 지역 함수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는 정상. 폰에서는 빈 화면.

원인 두 가지

  1. 모바일 브라우저에 내장 PDF 뷰어가 없음. iframe/embed에 PDF를 넣어도 그려줄 주체가 없음. iOS는 첫 페이지만 이미지처럼, 안드로이드는 대개 빈 화면.
  2. PDFObject가 UA로 모바일을 차단함. 소스에 Mobi|Android|iPhone|iPad 매칭되면 무조건 미지원 반환. 주석: "2023년 6월 기준 모바일 브라우저는 인라인 PDF를 제대로 지원하지 않음".

방향

PDF.js로 canvas에 직접 렌더링. 브라우저 뷰어를 안 쓰므로 기기 무관.

주의: 76페이지를 한 번에 다 그리면 메모리 수백 MB → iOS에서 탭 강제 종료. IntersectionObserver로 보이는 페이지만 그려야 함.

댓글

(0)
PDF 미리보기 팝업 구현 기록 | 강민석의 개발블로그