Next.js 프로젝트에 PDF 뷰어(react-pdf)를 붙였더니, 페이지를 열 때마다 콘솔에 이런 에러가 찍혔다.
DOMMatrix is not defined
PDF 미리보기 화면을 아직 열지도 않았는데 왜 홈 화면만 들어가도 에러가 났을까. 원인을 따라가 보니 SSR(서버 사이드 렌더링)의 기본 동작 방식을 다시 짚어볼 필요가 있었다.
Next.js는 컴포넌트를 두 번 실행한다
Next.js는 페이지를 브라우저에 보내기 전에, HTML을 미리 만들어두기 위해 Node.js 서버에서 컴포넌트 함수를 한 번 실행한다. 이게 SSR이다. 이렇게 만든 HTML을 먼저 보내서 사용자가 빈 화면 대신 콘텐츠를 바로 보게 하고, 그다음 브라우저가 같은 컴포넌트를 다시 실행해서 클릭 같은 인터랙션을 붙인다(hydration).
즉 컴포넌트 코드는 Node.js에서 한 번, 브라우저에서 한 번 실행된다. "use client" 지시어는 "이 컴포넌트를 클라이언트 번들에도 포함시켜라"는 뜻이지, SSR을 건너뛰라는 뜻이 아니다.
Node.js와 브라우저는 같은 언어를 쓰지만 API가 다르다
여기서 문제가 생긴다. Node.js와 브라우저는 둘 다 자바스크립트를 실행하지만, 제공하는 API가 다르다.
- 브라우저: window, document, DOMMatrix, canvas 등 화면을 그리기 위한 API 제공
- Node.js: fs, process 등 서버 환경을 위한 API 제공, 브라우저용 API는 없음
DOMMatrix는 canvas에 그림을 그릴 때 좌표 변환(확대·회전·이동)을 계산하는 브라우저 전용 객체다. Node.js에는 이 객체가 애초에 구현돼 있지 않다. 나중에 생기는 것도 아니고, 어떤 조건에서도 Node.js 안에서는 존재하지 않는다.
원인: react-pdf가 import 시점에 DOMMatrix를 참조한다
react-pdf가 감싸고 있는 pdfjs-dist는 모듈이 로드되는 순간(top-level) 캔버스 렌더링 환경을 판단하면서 DOMMatrix를 직접 참조한다. typeof DOMMatrix !== "undefined" 같은 안전한 체크 없이.
이 PDF 뷰어 컴포넌트는 import { Document, Page, pdfjs } from "react-pdf" 형태로 상위 컴포넌트에 정적 import 되어 있었다. JS 모듈은 import되는 순간 최상위 코드가 즉시 실행되므로, PDF 화면을 열지 않은 상태에서도 이 import 자체가 실행되면서 pdfjs-dist의 초기화 코드가 함께 돌았다. 그리고 그 초기화 코드가 Node.js(SSR 단계)에서 실행되며 DOMMatrix를 찾다가 에러가 난 것이다.
해결: 해당 코드를 브라우저 전용으로 격리한다
고치는 방법은 이 라이브러리를 쓰는 코드가 서버에서는 아예 실행되지 않도록 만드는 것이다. Next.js는 next/dynamic에 ssr: false 옵션을 제공한다.
const BoardPdfDocument = dynamic(() => import("./board-pdf-document"), {
ssr: false,
});
react-pdf를 사용하는 코드를 별도 파일로 분리하고 이렇게 불러오면, 이 모듈은 Node.js 서버 프로세스에서는 로드조차 되지 않는다. 오직 브라우저에서, DOMMatrix가 실제로 존재하는 환경에서만 로드되고 실행된다.
정리
- SSR 프레임워크에서는 컴포넌트 코드가 Node.js와 브라우저 양쪽에서 실행될 수 있다는 걸 전제해야 한다.
- 일부 라이브러리는 브라우저 API가 항상 존재한다고 가정하고 import 시점에 바로 그 API를 호출한다. pdfjs-dist가 그런 경우였다.
- 이런 라이브러리를 쓸 때는 next/dynamic({ ssr: false }) 같은 방법으로 클라이언트 전용 경계 뒤에 격리해야 SSR 환경에서 안전하다.
'개발 & IT > 프론트엔드' 카테고리의 다른 글
| Next.js Server Function 에러 메시지가 프로덕션에서 사라지는 이유 (0) | 2026.05.28 |
|---|---|
| 서버가 호출하는 API와 브라우저가 호출하는 API는 다르다 : NEXTJS (0) | 2026.05.14 |
| Next.js 캐싱의 기본값과 동적 렌더링 전환 조건 (0) | 2026.04.27 |
| Next.js의 redirect() vs router.push() — nginx 리버스 프록시 + basePath 환경에서의 함정 (0) | 2026.04.06 |
| Vue 개발자가 Next.js를 배우며 겪은 혼란: Server Component와 Client Component 이해하기 (1) | 2026.01.22 |