← 전체 글로 돌아가기

Next.js

Next.js 서버 컴포넌트에서 window를 쓰면 안 되는 이유

서버 컴포넌트에서 window나 document를 참조하면 런타임 에러가 난다. 왜 그런지, 어떻게 피하는지 정리했다.

Next.js App Router로 넘어오고 나서 가장 자주 만나는 에러 중 하나가 이거다.

ReferenceError: window is not defined

클라이언트에서는 잘 동작하던 코드를 서버 컴포넌트에 넣었을 때 터진다.

왜 서버 컴포넌트에는 window가 없나

Next.js의 서버 컴포넌트는 Node.js 환경에서 실행된다. Node.js에는 window, document, navigator, localStorage 같은 브라우저 전용 API가 없다. 이 객체들은 브라우저의 DOM API이기 때문에 서버에서 참조하면 ReferenceError가 발생한다.

Pages Router 시절에도 SSR 환경에서 같은 문제가 있었는데, App Router에서는 기본이 서버 컴포넌트라서 이 에러를 더 자주 만나게 된다.

해결 방법 1: use client 추가

가장 직접적인 방법은 해당 컴포넌트를 클라이언트 컴포넌트로 바꾸는 것이다.

'use client'

import { useEffect, useState } from 'react'

export function ScrollTracker() {
  const [scrollY, setScrollY] = useState(0)

  useEffect(() => {
    const handler = () => setScrollY(window.scrollY)
    window.addEventListener('scroll', handler)
    return () => window.removeEventListener('scroll', handler)
  }, [])

  return <div>스크롤 위치: {scrollY}</div>
}

useEffect 안에서 window를 참조하면 클라이언트 사이드에서만 실행되기 때문에 안전하다.

해결 방법 2: typeof 가드

서버 컴포넌트 안에서 조건부로 실행해야 한다면 typeof window !== 'undefined'로 가드를 친다. 다만 이 방법은 서버에서 해당 코드가 실행되지 않을 뿐이지, 컴포넌트 자체가 서버에서 렌더링되는 건 막지 못한다.

const isBrowser = typeof window !== 'undefined'
const width = isBrowser ? window.innerWidth : 0

해결 방법 3: dynamic import로 SSR 비활성화

서드파티 라이브러리가 내부적으로 window를 참조해서 직접 수정하기 어려운 경우에 쓴다.

import dynamic from 'next/dynamic'

const Chart = dynamic(() => import('./Chart'), { ssr: false })

ssr: false를 주면 해당 컴포넌트는 클라이언트에서만 렌더링된다. 단, 초기 HTML에 포함되지 않으니 SEO가 중요한 콘텐츠에는 적합하지 않다.

어디서 window를 쓰는지 빠르게 찾기

서버 컴포넌트에서 window 참조가 어디서 오는지 파악이 안 될 때는 에러 스택 트레이스를 따라가거나, 다음 명령으로 파일을 좁힌다.

grep -r 'window\.' src/app --include='*.tsx' --include='*.ts' | grep -v 'use client'

use client 지시어 없이 window를 참조하는 파일을 빠르게 찾을 수 있다.