구독하기

pdf.js를 Next.js에 붙이는 방법은 이미 반쯤 바뀌었다

Whiimsy

브라우저에서 문서 다루기 #1 — 검색해서 나오는 설정 네 가지 중 셋은 버려도 되고, 하나는 지금도 필요해요. 빈 next.config로 직접 확인해 볼게요.

PDF를 웹에서 보여줘야 할 일이 생기면 보통 react-pdf로 갑니다. 그리고 검색을 하면 거의 모든 글이 next.config부터 고치라고 하는데요, 그대로 따라 했다가 오히려 한참 헤맸어요.

이 글에서는 그 처방들이 지금도 유효한지 하나씩 확인하고, 왜 그중 하나만 살아남는지 정리해 볼게요. 확인은 빈 설정으로 만든 최소 재현 저장소로 했습니다.

검색하면 나오는 네 가지 처방

대략 이 넷이 세트로 따라다닙니다.

// ① 압축 끄기
module.exports = { swcMinify: false }

// ② canvas를 빈 모듈로 보내기
webpack: (config) => { config.resolve.alias.canvas = false; return config }

// ③ 워커를 public/ 으로 복사하고 경로 직접 지정
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js'

// ④ 컴포넌트를 next/dynamic 으로 감싸고 ssr: false

넷 다 실제로 있었던 문제에서 나왔어요. 그래서 보통 한 묶음으로 취급됩니다.

셋은 정말 없어졌어요

react-pdf v9.2.0 릴리스 노트에 이렇게 적혀 있습니다.

This version updates PDF.js to 4.8.69, significantly simplifying setup in Next.js. You no longer need to do any changes to Next.js config!

말만 믿기엔 찜찜해서 직접 확인했어요. next.config.ts를 완전히 비운 채로 Next.js 16.3.6 · Turbopack · pdfjs-dist 5.4.296에서 돌려봤습니다.

처방 결과
① swcMinify: false 없어도 정상
② canvas 별칭 Can't resolve 'canvas' 안 나옴
③ 워커 복사 복사 안 해도 로드됨

특히 ③이 재미있는데요, 워커 경로를 화면에 찍어보니 이렇게 나왔어요.

/_next/static/media/pdf.worker.min.2th4soq4xwzz7.mjs

public/에 아무것도 넣지 않았는데 Turbopack이 워커를 직접 번들하고 해시까지 붙였습니다. 코드는 이 한 줄이 전부예요.

pdfjs.GlobalWorkerOptions.workerSrc = new URL(
  'pdfjs-dist/build/pdf.worker.min.mjs',
  import.meta.url,
).toString()

③을 그대로 따라 하면 오히려 깨져요

낡은 처방이 불필요하기만 하면 다행인데, ③은 새 버그를 만듭니다.

워커를 public/에 복사하는 순간 그 파일은 그때 버전으로 고정돼요. 나중에 pdfjs-dist를 올리면 본체만 올라가고 워커는 옛날 것이 남아서 이렇게 터집니다.

Error: The API version "5.x.x" does not match the Worker version "4.x.x".

new URL(..., import.meta.url)은 번들러가 실제로 설치된 패키지를 해석하니까 어긋날 수가 없습니다. CDN 주소를 박는 방법도 같은 이유로 안 써요. 사내망이나 오프라인에서 죽고, 버전 고정을 사람이 기억해야 하거든요.

조건이 하나 있는데요, react-pdf README가 workerSrc는 컴포넌트를 쓰는 같은 모듈에서 설정해야 한다고 못박고 있습니다. 설정 파일이나 공통 모듈에 몰아두면 기본값이 덮어쓸 수 있어요.

④는 지금도 필요합니다

여기가 이 글의 본론이에요.

'use client'를 붙였으니 브라우저에서만 돌겠거니 하고 그냥 import 했더니, 첫 요청이 500으로 떨어졌습니다.

ReferenceError: DOMMatrix is not defined
app/pdf-viewer.tsx (4:1) @ module evaluation
> 4 | import { Document, Page, pdfjs } from 'react-pdf'

스택에 찍힌 경로가 원인을 그대로 말해줘요.

.next/dev/server/chunks/ssr/node_modules_...

server/chunks/ssr. App Router에서 'use client'는 "서버에서 실행되지 않는다"가 아니라 "클라이언트 번들에도 포함된다"는 표시예요. 서버 렌더링은 그대로 한 번 일어납니다. 그래서 파일 맨 위 import 줄이 Node에서 평가되고, 거기서 끝납니다.

npm run build를 돌리면 더 선명해져요. pdf.js 소스의 몇 번째 줄인지까지 나옵니다.

Error occurred prerendering page "/"
ReferenceError: DOMMatrix is not defined
    at module evaluation (webpack://pdf.js/src/display/canvas.js:63:22)
> 63 | const SCALE_MATRIX = new DOMMatrix();
Export encountered an error on /page: /, exiting the build.

DOMMatrix는 브라우저 API인데 모듈 최상단에서 객체를 하나 만들어 둡니다. 함수 안이 아니라 최상단이라, import 되는 순간 실행돼요. 조건부 렌더링으로는 못 막습니다.

그래서 ④는 살아 있습니다.

const PdfViewer = dynamic(() => import('./pdf-viewer'), { ssr: false })

이렇게 감싼 경로는 같은 조건에서 콘솔이 깨끗했어요.

왜 이게 헷갈리는가

넷이 한 묶음으로 돌아다니는 게 문제예요.

"이제 Next.js 설정 바꿀 필요 없다"는 문장은 ①②③에 대해서만 참인데, 넷을 세트로 외운 사람은 ④까지 빼게 됩니다. 그리고 dev에서 화면이 뜨기 때문에 한동안 모르다가 빌드에서 처음 터져요.

고쳐진 것과 안 고쳐진 것이 같은 문단에 적혀 있으면, 읽는 사람은 둘을 구분하지 못합니다.

그래도 설정이 필요해지는 경우

물론 빈 설정이 모든 상황의 정답은 아닙니다.

  • 모노레포에서 프로젝트 루트 밖 파일을 해석해야 하면 turbopack.root를 지정해야 해요. Turbopack은 루트 밖을 기본적으로 안 봅니다.
  • 서버에서 PDF를 그려야 하면(썸네일 생성 같은 것) 이야기가 완전히 달라집니다. 그때 canvas는 없애는 게 아니라 설치하는 쪽이에요. 빌드 로그에도 Please use the legacy build in Node.js environments 경고가 같이 뜹니다.
  • react-pdf v9.2.0 이전에 묶여 있다면 옛날 처방이 여전히 맞습니다.

저도 아직 못 푼 게 있어요. 텍스트 레이어를 켜면 한글 PDF에서 선택 영역이 글자와 어긋나는 경우가 있는데, 원인을 아직 못 찾았습니다.

직접 해보기

최소 재현 저장소를 GitHub에 올려뒀어요. next.config.ts가 비어 있는 게 핵심이고, 두 경로를 나란히 뒀습니다.

  • / — 감싸지 않은 쪽. 실패하도록 두었습니다
  • /ssr-skipped — ssr:false로 감싼 쪽

받아서 이 순서로 보시면 좋겠어요.

  • /를 열면 콘솔에 500과 DOMMatrix is not defined가 뜨나요?
  • /ssr-skipped는 깨끗한가요?
  • npm run build가 / 에서 멈추나요?
  • 워커 경로가 /_next/static/media/...로 찍히나요? public/을 안 썼는데도요
  • 콘솔에 Can't resolve 'canvas'가 정말 없나요?

쓰고 계신 조합에서 다르게 나온다면 버전과 함께 알려주시면 저도 배우고 싶어요. 확인한 조합은 Next.js 16.3.6 · react-pdf 10.x · pdfjs-dist 5.4.296 · Turbopack 입니다.

다음 편에서는 PDF 위에 그린 도형의 좌표가 확대·축소하면 왜 어긋나는지를 다뤄보려고 해요.

감사합니다.

참고

  • danbom/nextjs-pdfjs-minimal — 이 글의 최소 재현 저장소
  • wojtekmaj/react-pdf — v9.2.0 릴리스 노트, README의 workerSrc 설정 안내
  • Next.js 문서 — turbopack 설정 (root, resolveAlias, rules)
  • mozilla/pdf.js — src/display/canvas.js의 SCALE_MATRIX
  • vercel/next.js #64165 · #64657, wojtekmaj/react-pdf #1856 (모두 종료된 이슈입니다. 검색 상단에 남아 있으니 날짜를 보세요)