Next.js window is not defined 오류 해결: 브라우저 API를 안전하게 쓰기

2026.05.15·수정 2026.09.12·약 17분·작성: 해비·블로그 소개

학습 목표·선수 지식: 서버에서 window 예외가 나는 코드를 Effect로 수정합니다. 선수 지식: React state·Effect.

Next.js window is not defined 오류에서 먼저 확인할 것

Client Component도 첫 방문 때 서버에서 사전 렌더링될 수 있습니다. ‘use client’는 훅과 이벤트를 쓰는 경계이며 window 접근을 서버에서 없애는 설정이 아닙니다. 렌더 본문과 모듈 최상단을 먼저 검사하고, 브라우저 값은 Effect 또는 이벤트에서 읽습니다.

실습 경로: 현재 ZIP전체 코드. 먼저 설명에서 지정한 파일만 읽고, 설정·테스트 파일은 필요한 때 확인하세요.

증상부터 정확히 보기

Next.js window is not defined 오류 해결 방법의 원인을 단계별로 점검하는 다이어그램

Next.js window is not defined 오류를 만났을 때 먼저 볼 것은 에러 문구 자체보다 문제가 발생한 위치입니다. 같은 증상처럼 보여도 설정 파일에서 생긴 문제인지, 렌더링 시점 문제인지, 데이터나 상태를 바꾸는 방식 문제인지에 따라 해결 방법이 달라집니다.

window 오류는 해당 코드가 서버 렌더링 중 실행되는지부터 확인해야 합니다. 브라우저 전용 로직을 작은 컴포넌트로 분리한 뒤 useEffect 또는 동적 로딩이 필요한지 판단하면 원인을 빠르게 좁힐 수 있습니다.

왜 이런 문제가 생기는지

이 문제의 중심에는 브라우저 전용 객체를 서버 실행 시점에 사용해 발생하는 오류가 있습니다. window, document, localStorage, 브라우저 크기 같은 값은 서버 환경에 존재하지 않습니다. 따라서 모듈 최상단이나 서버 컴포넌트 렌더링 중에 이런 값을 바로 읽으면 Next.js가 HTML을 만들거나 빌드하는 과정에서 오류가 날 수 있습니다.

특히 Next 작업에서는 설정, 실행 환경, 상태 참조, 렌더링 순서가 함께 영향을 줍니다. 그래서 “왜 안 되지?”에서 멈추지 말고 어떤 조건에서 재현되는지, 새로고침 후에도 같은지, 빌드 환경에서도 같은지 나눠서 보는 것이 좋습니다.

실제 코드에서 고치는 방법

아래 수정 전/수정 방법/수정 후 코드에서 app/WidthLabel.tsx의 서버 렌더 접근을 비교합니다.

코드를 바꿀 때는 결과만 확인하지 말고 “왜 이 방식이면 문제가 사라지는지”를 같이 확인해야 합니다. 컴포넌트가 사용자 이벤트, effect, 브라우저 상태를 사용한다면 'use client' 경계가 필요합니다. 단순히 렌더링 이후에 브라우저 값을 읽으면 되는 경우에는 useEffect 안에서 접근합니다. 외부 라이브러리 자체가 import 시점에 window를 요구한다면 해당 컴포넌트를 클라이언트 컴포넌트로 분리하고 dynamic(() => import(...), { ssr: false })를 검토합니다.

수정 후 확인할 체크리스트

Next.js window is not defined 오류 해결 방법 수정 후 확인할 체크리스트 인포그래픽

수정 후에는 개발 서버에서만 확인하지 말고 새로고침, 빌드, 실제 데이터 조건을 함께 봅니다. 조건이 하나만 달라져도 문제가 다시 보일 수 있기 때문입니다.

체크할 순서는 간단합니다. 먼저 재현 조건이 사라졌는지 확인하고, 그 다음 같은 패턴이 다른 파일에 남아 있는지 봅니다. 마지막으로 임시 회피 코드가 남아 있지 않은지 정리합니다.

전체 파일로 교체하고 재현합니다

app/WidthLabel.tsx의 렌더에서 window.innerWidth를 직접 읽으면 서버 사전 렌더링에서 ReferenceError가 발생합니다. 아래 수정 후 전체 파일은 number | null 상태를 명시하고 첫 서버/브라우저 출력에 같은 확인 중 문구를 사용합니다. resize 이벤트를 등록하고 정리합니다. 테스트 ZIP의 app/page.tsx가 이 컴포넌트를 가져옵니다. WidthLabel.tsx 전체 코드

라이브러리 import 자체가 window를 읽는 경우에는 app/ClientWrapper.tsx에 use client와 dynamic(() => import(“./BrowserWidget”), { ssr: false })를 둡니다. BrowserWidget을 서버 page에서 직접 import하지 않습니다. 이 대안은 위 너비 실습에 필요하지 않습니다.

현재 실습 ZIP

현재 본문과 같은 실습 파일 내려받기

전체 실행 파일

ZIP의 starter는 의도적으로 빌드에 실패하는 수정 전 프로젝트, complete는 수정 후 프로젝트입니다. 각 폴더에서 npm install 후 npm run build로 비교합니다. 수정 후 실행은 npm run start, 개발은 npm run dev, 타입 검사는 npm run typecheck입니다. Node.js20.9 이상(검증24.19.0), Next16.3.5·React19.3.0·TS7.0.2입니다.

README.md

# 실습 2995

Node.js 20.9 이상 (검증: 24.19.0). 이 폴더에서 npm install, npm run build, npm run start. 개발은 npm run dev. 타입 검사는 npm run typecheck.
Next.js 16.3.5 / React 19.3.0 / TypeScript 7.0.2. 외부 DB·인증 서비스는 제공하지 않습니다.
검증: 로컬 production build와 HTTP 검사. 브라우저 조작과 실제 배포는 별도 확인합니다.

app/WidthLabel.tsx

"use client";
export default function WidthLabel() {
  return <p>{window.innerWidth}</p>;
}

app/layout.tsx

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko">
      <body>{children}</body>
    </html>
  );
}

app/page.tsx

import WidthLabel from "./WidthLabel";
export default function Page() {
  return (
    <main>
      <h1>브라우저 너비</h1>
      <WidthLabel />
    </main>
  );
}

next-env.d.ts

/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

package.json

{
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build --webpack",
    "start": "next start",
    "typecheck": "next typegen && tsc --noEmit"
  },
  "engines": {
    "node": ">=20.9.0"
  },
  "dependencies": {
    "next": "16.3.5",
    "react": "19.3.0",
    "react-dom": "19.3.0"
  },
  "devDependencies": {
    "typescript": "7.0.2",
    "@types/react": "19.3.0",
    "@types/react-dom": "19.3.0",
    "@types/node": "22.20.2"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "esnext"],
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "plugins": [
      {
        "name": "next"
      }
    ],
    "allowJs": true,
    "incremental": true
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    ".next/dev/types/**/*.ts"
  ],
  "exclude": ["node_modules"]
}

app/WidthLabel.tsx를 교체합니다

렌더 본문의 window.innerWidth 직접 읽기를 제거합니다. number | null 상태와 useEffect를 추가해 서버와 첫 클라이언트 렌더에 같은 확인 중 문구를 사용하고, resize 리스너를 등록·정리합니다. use client만으로 서버 사전 렌더링을 끄지 못합니다.

다른 파일은 변경하지 않습니다. starter에서 npm run build로 실패를 확인하고 app/WidthLabel.tsx만 수정 후 complete와 대조합니다. 다시 npm run build가 성공해야 합니다. 아래 코드는 diff가 아닌 전체 파일이므로 + 또는 – 기호를 붙여 복사하지 않습니다.

실제 검증: 시작 상태 build exit1 → 수정 후 exit0. 수정 후 HTTP200 확인. 브라우저에서 resize가 반영되는지는 별도 확인합니다.

README.md

# 실습 2995

Node.js 20.9 이상 (검증: 24.19.0). 이 폴더에서 npm install, npm run build, npm run start. 개발은 npm run dev. 타입 검사는 npm run typecheck.
Next.js 16.3.5 / React 19.3.0 / TypeScript 7.0.2. 외부 DB·인증 서비스는 제공하지 않습니다.
검증: 로컬 production build와 HTTP 검사. 브라우저 조작과 실제 배포는 별도 확인합니다.

app/WidthLabel.tsx

"use client";
import { useEffect, useState } from "react";
export default function WidthLabel() {
  const [width, setWidth] = useState<number | null>(null);
  useEffect(() => {
    const update = () => setWidth(window.innerWidth);
    update();
    window.addEventListener("resize", update);
    return () => window.removeEventListener("resize", update);
  }, []);
  return <p>{width === null ? "확인 중" : width + "px"}</p>;
}

app/layout.tsx

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko">
      <body>{children}</body>
    </html>
  );
}

app/page.tsx

import WidthLabel from "./WidthLabel";
export default function Page() {
  return (
    <main>
      <h1>브라우저 너비</h1>
      <WidthLabel />
    </main>
  );
}

next-env.d.ts

/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

package.json

{
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build --webpack",
    "start": "next start",
    "typecheck": "next typegen && tsc --noEmit"
  },
  "engines": {
    "node": ">=20.9.0"
  },
  "dependencies": {
    "next": "16.3.5",
    "react": "19.3.0",
    "react-dom": "19.3.0"
  },
  "devDependencies": {
    "typescript": "7.0.2",
    "@types/react": "19.3.0",
    "@types/react-dom": "19.3.0",
    "@types/node": "22.20.2"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "esnext"],
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "plugins": [
      {
        "name": "next"
      }
    ],
    "allowJs": true,
    "incremental": true
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    ".next/dev/types/**/*.ts"
  ],
  "exclude": ["node_modules"]
}

실제 검증: next build: window 직접 접근 ReferenceError exit 1 → Effect 수정본 exit 0. HTTP 초기 fallback 확인. 브라우저 resize 미검증. 코드 뷰어와 ZIP의 모든 파일을 바이트/복사 텍스트로 대조했습니다.

공식 자료 재확인: 2026-09-12. 이 글의 실행하지 않은 브라우저/배포 점검은 독자 확인 과제로 구분합니다.

마무리 점검

Next.js window is not defined 오류 해결은 한 가지 정답 코드보다 원인을 좁히는 순서가 더 중요합니다. 증상, 실행 시점, 클라이언트 컴포넌트 경계, 브라우저 전용 라이브러리 여부를 나눠서 보면 불필요한 수정이 줄고, 다음 문제를 만났을 때도 확인할 지점이 분명해집니다.

참고 기준은 Next.js 공식 문서의 Server and Client Components, Lazy Loading, Rendering components only in the browser, Prerender Error를 함께 확인하면 됩니다.

공식 기준과 확인 범위

확인일: 2026-09-12. API 설명은 Next.js 16 App Router 기준이며 과거 릴리스 글은 본문의 태그 범위를 유지합니다. 부분 API 코드는 기존 프로젝트에 통합하는 예시입니다.

이 글이 도움이 되었나요?

조회 중

Next.js 학습 순서

필수 13개 · 전체 27개

읽음 기록 관리

전체 과정 목차 (27개)
  1. 필수 길잡이 · Next.js App Router 학습 순서: 설치부터 배포까지
  2. 필수 학습 · Next.js package.json: scripts dependencies 이해
  3. 필수 학습 · Next.js App Router로 만드는 첫 프로젝트 완성 실습 가이드
  4. 필수 학습 · Next.js에서 .next 폴더는 어떤 역할을 할까?
  5. 필수 학습 · Next.js 동적 라우트 완전 정리: [slug], params, catch-all
  6. 선택 참고 · Next.js params should be awaited 해결: App Router 기준
  7. 선택 참고 · Next.js window is not defined 오류 해결: 브라우저 API를 안전하게 쓰기 현재 글
  8. 선택 참고 · Next.js hydration failed 오류 해결: 원인과 해결 방법
  9. 필수 학습 · Axios 사용법: React Next.js에서 API 요청 구조 잡는 법
  10. 선택 참고 · Next.js Route Handler 405 오류 해결: GET/POST 파일 위치와 메서드 설정 확인
  11. 필수 학습 · Next.js Server Actions + React Hook Form 검증 기준
  12. 선택 참고 · Next.js useSearchParams Suspense 오류 해결: 빌드 실패 기준
  13. 선택 참고 · Next.js fetch 캐시 문제 해결: 데이터가 바뀌었는데 화면이 그대로일 때
  14. 선택 참고 · Next.js Dynamic server usage 오류 해결: cookies headers 기준
  15. 선택 참고 · Next.js 환경변수 적용 오류 해결: .env.local을 바꿨는데 값이 안 바뀔 때
  16. 필수 학습 · Next.js Metadata API 완전 정리: 정적 metadata와 generateMetadata
  17. 선택 참고 · Next.js metadata가 적용되지 않을 때 확인할 7가지
  18. 선택 참고 · Next.js Image remotePatterns 오류 해결: 외부 이미지 도메인 허용하기
  19. 필수 학습 · Next.js redirects 설정: next.config.js에서 URL 이동 처리
  20. 필수 학습 · Next.js SEO 체크리스트: metadata·초기 HTML·OG 이미지 점검
  21. 선택 참고 · Next.js SEO 완전 가이드: App Router metadata부터 배포 확인까지
  22. 필수 학습 · Next.js 16 성능 최적화 체크리스트: 번들·이미지·캐시·배포
  23. 필수 학습 · Next.js 렌더링 성능 최적화: React 화면이 느릴 때 기준
  24. 필수 학습 · Next.js 16 Proxy 마이그레이션: Node.js Runtime·matcher 검증
  25. 선택 참고 · Next.js 보안 패치 기준: v16.2.5 영향 범위 점검
  26. 선택 참고 · Next.js SEO SSR 적용법: 검색 노출과 렌더링 구조 잡기
  27. 시점·기록 · Next.js 16.3.0-canary.106의 useCache deprecation 경고와 hybrid not-found 수정 이해하기

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기