TechFeedTechFeed
Frontend

window is not defined, document, 브라우저 API | use client를 붙였는데도 빌드가 죽으면?

use client를 붙여도 window is not defined가 나면, 클라이언트 컴포넌트가 서버에서 한 번 그려진 겁니다. 렌더와 파일 맨 위가 아니라 useEffect나 ssr false로 옮깁니다. 넥스트, 리액트, document, 카카오 지도, 다음 주소, 프론트엔드, 1인 개발자 기준. 2026년 9월 Next.js, React 공식 문서.

by

window is not defineduse client가 모자라서가 아니라, 클라이언트 컴포넌트도 서버에서 HTML을 한 번 그리기 때문에 납니다. 렌더 본문이나 파일 맨 위에서 windowdocument를 읽으면 노드에는 그 객체가 없어서 next build가 죽습니다.


로컬 개발 서버에선 화면이 열리다가 배포 직전 빌드만 빨간 줄인 경우, 카카오 지도나 다음 주소 검색을 컴포넌트 최상단에 붙였을 때 흔합니다. 훅이 막히는 자리와는 다릅니다.


useEffect나 클릭 핸들러로 옮기거나, 브라우저만 필요한 덩어리는 next/dynamicssr: false로 빼면 됩니다. 근거는 넥스트 서버와 클라이언트 문서지연 로드 문서에 있습니다.


use client를 붙여도 왜 죽나

클라이언트 컴포넌트도 서버에서 한 번 그립니다. 'use client'는 그 파일부터 브라우저 번들에 넣는다는 뜻이지, 서버를 건너뛴다는 뜻이 아닙니다. 넥스트는 첫 HTML을 노드에서 만들고, 그다음에 브라우저가 이벤트를 붙입니다.


그래서 렌더 함수 안에서 window.innerWidth를 읽거나, 파일 맨 위에서 document를 건드리면 서버 평가 순간에 ReferenceError가 납니다. 메시지는 window is not defined 또는 document is not defined로 찍힙니다. 훅이 없어서 나는 빨간 줄과는 칸이 다릅니다. 훅은 지시자만 있으면 되고, 브라우저 객체는 실행 시점이 브라우저여야 합니다.


공식 문서도 클라이언트 컴포넌트를 쓸 자리로 상태, 이벤트, 생명주기, localStoragewindow 같은 브라우저 전용 API를 적습니다. 다만 그 파일의 첫 그리기는 여전히 서버입니다. 한국에서 넥스트를 혼자 돌리면 카카오 로그인 버튼은 지시자로 풀리고, 지도 스크립트는 이 줄에서 한 번 더 막히는 일이 잦습니다.


먼저 기억할 것 | use client는 훅과 클릭을 살리는 지시자입니다. window를 서버 그리기에서 빼 주지는 않습니다. 읽기 시점을 마운트 뒤나 클릭 뒤로 옮기세요.


터지는 자리는 세 칸이다

모듈 최상단, 렌더 본문, 서버 컴포넌트 세 칸이면 원인이 갈립니다. 표로 먼저 나눕니다.


증상먼저 할 일
파일 맨 위임포트만 해도 빌드가 죽음지도·주소 SDK를 동적 불러오기로
렌더 본문컴포넌트 함수 안에서 window를 읽음값을 useEffect로 옮김
서버 컴포넌트페이지 파일에 브라우저 객체가 있음클라이언트 자식으로 쪼갬
페이지 라우터getServerSideProps나 페이지 본문에서 읽음요청 핸들러에 window를 두지 않음
라이브러리 내부내 코드는 깨끗한 데 패키지가 최상단에서 읽음그 컴포넌트만 ssr: false

다섯 번째 칸이 오래 걸립니다. 내가 쓴 줄에는 window가 없어도, 카카오 지도나 리플렛처럼 패키지가 불러오는 순간에 객체를 읽으면 같은 줄이 납니다. 스택이 node_modules 안을 가리키면 내 렌더를 고치는 게 아니라 그 파일을 서버 그리기에서 빼야 합니다.


훅 에러와 섞지 마세요. useState가 막히는 칸은 서버 컴포넌트에 훅을 넣은 자리입니다. 여기는 지시자를 붙여도 객체를 서버에서 읽어서 프로세스가 죽는 칸만 봅니다.


서버에서 바로 죽는 두 줄
'use client' // 파일 맨 위에서 읽으면 모듈이 평가되는 순간 죽음 const startWidth = window.innerWidth export default function Hero() { // 렌더에서 읽어도 서버 HTML을 그릴 때 죽음 const dark = document.documentElement.classList.contains('dark') return <p>{startWidth} / {String(dark)}</p> }
서버 노드에는 window가 없어 브라우저 객체를 읽으면 빌드가 죽는 개념 이미지
클라이언트 컴포넌트도 첫 HTML은 서버에서 그린다. 그 순간에 window는 없다

useEffect와 클릭 핸들러로 옮기기

값을 마운트 뒤에 채우거나, 클릭이 일어난 뒤에만 읽습니다. useEffect는 브라우저에 붙은 다음에 돌므로 그 안에서 window를 읽어도 됩니다. 리액트 문서도 이 훅을 외부 시스템과 동기화하는 자리로 둡니다.


클릭, 제출, 스크롤 같은 이벤트 핸들러도 서버에서는 실행되지 않습니다. 공유 버튼에 navigator.clipboard를 넣는 정도는 핸들러로 충분합니다. 반대로 첫 화면에 가로 폭을 글자로 찍어야 하면, 첫 렌더는 서버와 같은 자리 표시를 그리고 마운트 뒤에 숫자를 넣으세요.


문의 페이지에 다음 우편번호 팝업을 여는 버튼이 있다면, 스크립트 로드와 daum.Postcode 호출을 클릭 뒤로 미루면 빌드가 살습니다. 화면을 그리는 순간에 팝업 객체를 만들 필요는 없습니다.


마운트 뒤와 클릭에서만 브라우저 객체 읽기
'use client' import { useEffect, useState } from 'react' export default function Viewport() { const [width, setWidth] = useState(null) useEffect(() => { setWidth(window.innerWidth) const onResize = () => setWidth(window.innerWidth) window.addEventListener('resize', onResize) return () => window.removeEventListener('resize', onResize) }, []) return <span>{width ?? '측정 중'}</span> } function Share() { return ( <button type="button" onClick={() => window.navigator.clipboard.writeText(location.href)} > 주소 복사 </button> ) }

typeof 가드가 하이드레이션과 갈리는 때

렌더에서 typeof window !== 'undefined'로 화면을 갈라 그리면, 이번 줄은 피해 가도 다음 줄이 납니다. 서버는 거짓 분기를 그리고 브라우저는 참 분기를 그리니, 첫 HTML과 첫 클라이언트 렌더가 달라집니다. 그때 뜨는 건 하이드레이션 미스매치입니다.


모듈 최상단이나 이벤트 밖에서 한 번만 막을 때는 typeof가 맞습니다. 예외 로깅 라이브러리를 파일 위에서 초기화할 때처럼, 화면 글자를 바꾸지 않는 코드에 쓰세요. 화면 글자나 클래스를 서버와 다르게 만들면 가드가 아니라 두 번 그리기가 맞습니다.


날짜 글자가 어긋나는 칸은 또 별글입니다. 서버 UTC와 브라우저 서울이 갈리는 문제는 로케일 텍스트 불일치에서 다룹니다. 여기서는 객체가 없어서 프로세스가 죽는 줄만 고칩니다.


가드로 화면을 나누지 말 것 | 렌더에서 window 존재 여부로 다른 JSX를 반환하면 빌드는 통과해도 하이드레이션이 깨집니다. 첫 화면은 서버와 같게 두고, 브라우저 값은 마운트 뒤에 채우세요.


useEffect로 브라우저 값을 마운트 뒤에 채우는 모니터와 책상 이미지
첫 렌더는 서버와 같게 두고, 가로 폭 같은 값은 마운트 뒤에 채운다

카카오 다음 토스 스크립트는 서버에서 빼기

지도, 우편번호, 결제 위젯처럼 불러오는 순간에 window를 읽는 패키지는 동적 임포트로 서버 그리기를 끕니다. 넥스트 지연 로드 문서는 브라우저 API에 기대는 의존성을 next/dynamicssr: false로 빼라고 적습니다.


함정은 위치입니다. ssr: false는 클라이언트 컴포넌트 안에서만 됩니다. 서버 컴포넌트 페이지에서 바로 쓰면 넥스트가 거절합니다. 지도 칸을 클라이언트 파일로 쪼갠 다음, 그 파일에서 동적 불러오기를 하세요.


카카오 자바스크립트 SDK 문서도 브라우저에서 스크립트를 올린 뒤에 Kakao.init을 하라고 안내합니다. 토스 결제위젯, 채널톡, 네이버 지도도 같은 자리입니다. 1인으로 문의 페이지를 붙일 때 페이지 파일 최상단에 SDK를 두면 개발 화면에선 지도가 보여도 프리뷰 빌드가 한 번에 죽습니다.


클라이언트 파일에서 ssr false로 지도만 빼기
'use client' import dynamic from 'next/dynamic' const KakaoMap = dynamic(() => import('./kakao-map'), { ssr: false }) export default function ContactClient() { return <KakaoMap /> } // kakao-map.js 안에서만 window.kakao를 읽는다 // 서버 페이지에서 ssr: false를 쓰면 넥스트가 거절한다
상황권장주의
가로 폭, 테마useEffect로 값 채우기첫 렌더는 서버와 같은 값
복사, 공유 버튼onClick에서 읽기렌더에 navigator를 두지 않기
카카오 지도, 다음 주소dynamic ssr: false클라이언트 파일 안에서만
로깅 SDK 초기화typeof window 가드화면 JSX를 가드로 나누지 않기
페이지 전체 지시자지도 칸만 쪼개기글 목록까지 클라이언트로 내리지 않기

개발은 되고 빌드만 죽는 이유

개발 서버는 요청이 올 때 그 경로만 실행하고, 프로덕션 빌드는 정적 페이지를 미리 그립니다. 미리 그리는 순간에 노드가 컴포넌트를 평가하니, 개발에선 넘어가던 줄이 빌드에서 터집니다. next dev 화면이 멀쩡해도 next build를 한 번은 돌려 보세요.


페이지 라우터도 같습니다. getServerSidePropsgetStaticProps는 서버입니다. 거기에 window를 두면 앱 라우터와 같은 줄이 납니다. 미들웨어는 웹 API 일부가 있지만 windowdocument는 없습니다. 엣지에서 화면 폭을 재려 하지 마세요.


중복 리액트 때문에 훅이 죽는 칸은 Invalid hook call입니다. 에러 화면만 보고 지시자를 더 붙이면 객체를 읽는 줄은 그대로입니다. 메시지에 windowdocument가 있으면 실행 시점부터 옮기세요.


로컬 개발은 되고 프로덕션 빌드만 실패하는 서버랙 통로 이미지
개발 서버가 초록이어도 미리 그리는 빌드에서 window를 읽으면 죽는다

참고 자료


내부 연계: useState와 use client, 하이드레이션 미스매치, 로케일 텍스트 불일치


인용한 동작과 옵션은 2026년 9월 공개 문서 기준입니다.


자주 묻는 질문

use client를 붙였는데도 window is not defined가 나요.

지시자는 훅과 클릭을 살릴 뿐, 서버 그리기를 끄지 않습니다. 렌더나 파일 맨 위에서 window를 읽으면 노드에서 그대로 죽습니다. 값을 useEffect로 옮기거나, 패키지가 최상단에서 읽으면 ssr: false로 그 파일만 빼세요.


document is not defined도 같은 원인인가요?

같습니다. document, localStorage, navigator도 브라우저 전용입니다. 메시지 이름만 다를 뿐 서버에 객체가 없는 줄입니다. 고치는 순서도 같습니다.


페이지 파일에 ssr: false를 넣으면 안 되나요?

앱 라우터 서버 컴포넌트에서는 거절됩니다. 지도나 위젯을 클라이언트 자식으로 쪼갠 다음, 그 파일에서 next/dynamic을 쓰세요. 공식 지연 로드 문서가 이 제한을 명시합니다.


typeof window로 감싸면 끝나나요?

화면 글자를 바꾸지 않는 초기화에는 됩니다. 렌더에서 참과 거짓으로 다른 JSX를 그리면 하이드레이션이 깨집니다. 첫 화면용 값은 마운트 뒤에 채우는 편이 안전합니다.


개발 서버에선 되는데 빌드만 실패합니다.

미리 그리는 빌드가 노드에서 컴포넌트를 평가하기 때문입니다. 카카오 지도처럼 최상단에서 window를 읽는 패키지는 개발 요청에선 넘어가도 정적 생성에서 죽습니다. next build를 로컬에서 한 번 돌려 확인하세요.


카카오 로그인은 됐는데 지도만 죽어요.

로그인 버튼은 훅과 클릭이라 지시자면 충분하고, 지도 SDK는 모듈이 평가될 때 브라우저 객체를 읽습니다. 로그인 칸과 지도 칸을 한 파일에 두지 말고, 지도만 동적 불러오기로 분리하세요.


이 빨간 줄은 브라우저 객체를 서버 그리기에서 읽었다는 뜻입니다. 지시자만 추가하지 말고 읽기 시점을 마운트나 클릭 뒤로 옮기면 대부분 풀립니다. 관련 글: useState와 use client, 하이드레이션 미스매치, 중복 리액트 훅.


window is not defineddocument is not defined브라우저 APIuse clientNext.jsReactuseEffect카카오 지도다음 주소프론트엔드SSR개발자

관련 도구

함께 보면 좋은 문제 해결

EXPLORE / Frontend

이어서 읽어보기

전체 토픽 둘러보기