Next.js App Router: 'ReferenceError: window is not defined' SSR 빌드 에러 원인과 3가지 해결법
- 원인: Next.js 컴포넌트는 브라우저에 도달하기 전 Node.js 서버에서 먼저 렌더링되는데, 서버 환경엔
window·document가 없어서 발생 - 해결: 단순 참조는
typeof window !== 'undefined'분기 / DOM 로직은useEffect로 이동 / 내부에서 window를 남발하는 외부 라이브러리는next/dynamic({'{'} ssr: false {'}'}) - 주의: 차트·지도 SDK처럼 브라우저 전용 기능에 의존하는 외부 패키지를 그대로 가져다 쓸 때 이 에러를 가장 많이 만남
const HeavyChart = dynamic(() => import('./HeavyChart'), { ssr: false });
↓ 왜 이런 에러가 나는지 원리 보기
Next.js App Router 프로젝트를 빌드할 때 'window is not defined' 또는 'document is not defined' 에러로 빌드가 멈추는 경우가 있습니다. 개발 서버(next dev)에서는 문제가 없다가 빌드 시점에만 에러가 나는 이유와, 이를 우회하는 세 가지 방법을 정리합니다.
왜 'window is not defined' 에러가 발생하는가
이 에러의 핵심은 "서버 사이드 렌더링(SSR) 도중 브라우저 전용 전역 객체 참조"에 있습니다. Next.js의 모든 컴포넌트(클라이언트 컴포넌트 포함)는 브라우저에 도달하기 전에 먼저 Node.js 서버 환경에서 최초 1회 사전 렌더링(Pre-rendering)을 거칩니다. 이때 Node.js 서버 환경에는 브라우저 엔진에만 존재하는 window, document, localStorage 등의 객체가 당연히 존재하지 않습니다. 서버가 코드를 실행하는 도중 존재하지 않는 window를 참조하는 순간 ReferenceError: window is not defined라는 런타임 에러가 발생하고, 이 에러 때문에 사전 렌더링 프로세스 자체가 죽으면서 빌드가 실패하는 것입니다. 차트 라이브러리나 지도 SDK처럼 브라우저 전용 기능에 의존하는 외부 패키지를 그대로 가져다 쓸 때 이 에러를 가장 많이 만나게 됩니다.
렌더링 주체별 런타임 환경
window 객체 없음 (undefined)
document / DOM 제어 불가
window / DOM 제어 가능
localStorage / Session API 지원
빌드 에러를 완전히 우회하는 3가지 방법
방법 A: typeof window !== 'undefined' 조건문 사용하기
가장 심플하고 원시적인 검증 방식입니다. 코드가 실행되는 환경이 서버인지 브라우저인지 조건식으로 먼저 필터링합니다.
// 에러: 서버 렌더링 시 window를 읽어 에러 발생
const token = window.localStorage.getItem('token');
// 해결: window 객체의 존재 유무를 먼저 검사 후 안전하게 호출
const tokenSafe = typeof window !== 'undefined'
? window.localStorage.getItem('token')
: null;
방법 B: useEffect 마운트 시점으로 로직 미루기 (가장 권장)
리액트의 useEffect 훅은 컴포넌트가 서버 렌더링을 완전히 마치고 "브라우저 화면(DOM)에 실제로 안착(Mount)한 이후에만 실행"되는 것을 리액트가 보장합니다. 따라서 브라우저 전용 API 연동 로직은 이곳에 작성하는 것이 가장 안전합니다.
import { useState, useEffect } from 'react';
export default function SizeChecker() {
const [width, setWidth] = useState(0);
useEffect(() => {
// 브라우저 마운트가 끝난 시점이라 window 객체가 보장됨
setWidth(window.innerWidth);
}, []);
return <p>현재 화면 너비: {width}px</p>;
}
방법 C: next/dynamic을 사용한 SSR 비활성화 로딩
외부 차트 라이브러리(Chart.js, D3 등)나 리치 텍스트 에디터는 내부 소스코드 전반에 window와 document를 남발하여 조건문 처리조차 불가능합니다. 이럴 때는 컴포넌트 로딩 자체를 동적으로 지연시켜 해결합니다.
import dynamic from 'next/dynamic';
// ssr: false 옵션으로 서버 렌더링 시 해당 외부 컴포넌트 빌드 제외
const HeavyChartComponent = dynamic(
() => import('../components/HeavyChartComponent'),
{ ssr: false }
);
export default function AnalyticsPage() {
return (
<div>
<h1>통계 데이터 대시보드</h1>
<HeavyChartComponent />
</div>
);
}
지도 SDK를 붙이는 작업에서 이 에러를 처음 만났을 때, 라이브러리 문서에는 별다른 경고가 없어서 그냥 임포트만 하면 되는 줄 알았습니다. 로컬 개발 서버는 멀쩡히 돌길래 그대로 배포했다가 프로덕션 빌드가 그 자리에서 실패했고, 로그를 보고 나서야 그 라이브러리가 모듈 최상단에서부터 window를 참조하고 있다는 걸 알았습니다. 조건문으로는 못 막는 구조라 결국 next/dynamic으로 통째로 클라이언트 전용 로딩으로 돌렸습니다.
요약: 윈도우 객체 미정의 에러 해결 전략 맵
변수 초기화 단계에서 window를 바로 호출하면 서버 사이드 컴파일 도중 빌드가 실패할 위험이 있으므로 typeof window !== 'undefined' 분기로 처리하는 것이 기본입니다. 화면 크기 계산처럼 DOM을 직접 다루는 로직은 서버·클라이언트 간 DOM 불일치로 인한 하이드레이션 오류를 막기 위해 useEffect 마운트 시점으로 미루는 것이 안전합니다. 내부적으로 window를 남발하는 무거운 외부 라이브러리는 next/dynamic의 ssr: false 옵션으로 아예 서버 렌더링 대상에서 제외하는 편이 낫습니다.
Next.js의 모듈 동적 로드 구조와 최적화 설정은 공식 문서에서 확인할 수 있습니다.
참고로 이런 에러는 대개 외부 라이브러리가 window나 document를 내부에서 직접 참조할 때 발생하므로, 라이브러리 문서에서 SSR/Next.js 지원 여부를 먼저 확인해두면 같은 문제를 반복해서 만나는 걸 줄일 수 있습니다.
댓글
댓글 쓰기