Next.js에서 "window is not defined" 오류 해결하기

Yunsung·2025년 6월 18일
post-thumbnail

🚨 문제 상황

프로젝트 개발 중 IntersectionObserver를 사용해 무한 스크롤 기능을 구현하려고 했는데, 다음과 같은 오류가 발생했습니다.

// 오류가 발생한 코드
const observer = new window.IntersectionObserver(
  (entries) => {
    if (entries[0].isIntersecting) {
      // 데이터 로딩 로직
    }
  },
  { threshold: 0.2 }
);

오류 메시지 ReferenceError: 'window' is not defined

🔍 오류 원인 분석

SSR (Server-Side Rendering) 환경의 문제
Next.js는 SSR을 지원하는 프레임워크입니다. 이는 서버에서도 컴포넌트를 렌더링한다는 의미입니다.

  • 서버 환경: window 객체가 존재하지 않음 (브라우저 전용 API)
  • 브라우저 환경: window 객체가 존재함
// 서버에서 실행될 때
console.log(window); // undefined ❌

// 브라우저에서 실행될 때  
console.log(window); // Window 객체 ✅

왜 이런 일이 발생할까요?

  • window 객체는 브라우저에서만 존재하는 전역 객체입니다
  • 서버(Node.js 환경)에는 window 객체가 없습니다
  • Next.js는 서버에서 먼저 페이지를 렌더링한 후 브라우저로 전송합니다
  • 따라서 서버 렌더링 단계에서 window에 접근하면 오류가 발생합니다

💡 해결 방법들

1. typeof 연산자로 안전하게 접근

사용 시기: 단순한 브라우저 API 접근

if (typeof window !== "undefined") {
  const observer = new window.IntersectionObserver(callback);
}
  • 장점: 간단하고 직관적
  • 단점: ESLint에서 여전히 window을 인식하지 못할 수 있음, 깜빡임 발생 가능

2. useEffect Hook 사용

사용 시기: 컴포넌트 마운트 시 한 번만 실행

useEffect(() => {
  // 여기서는 window 객체가 확실히 존재함
  const observer = new window.IntersectionObserver(callback);
  return () => observer.disconnect();
}, []);
  • 장점: 클라이언트 사이드에서만 실행됨을 보장
  • 단점: 초기 렌더링 시점 제어 어려움, 깜빡임 발생 가능

3. useRef + 조건문 - 복잡한 API 관리

사용 시기: IntersectionObserver, ResizeObserver 등 복잡한 API

// 사용 시기: IntersectionObserver, ResizeObserver 등 복잡한 API
const observerRef = useRef();
useEffect(() => {
  if (typeof window !== "undefined") {
    observerRef.current = new IntersectionObserver(callback);
  }
}, []);
  • 장점: 메모리 안전성, 재사용성
  • 단점: 코드 복잡도 증가

4. next/dynamic 사용

import dynamic from 'next/dynamic'

const ClientOnlyComponent = dynamic(() => import('./Component'), { 
  ssr: false 
})
  • 장점: SSR 완전 제거, 번들 최적화
  • 단점: 컴포넌트 레벨에서만 적용 가능, 초기 로딩 지연

5. Isomorphic Hook 패턴 (현재 사용한 방법)

// Isomorphic Hook 패턴
const useIsomorphicLayoutEffect = typeof window !== 'undefined' 
  ? useLayoutEffect 
  : useEffect;

// 클라이언트 상태 체크 hook
const useClientOnly = () => {
  const [isClient, setIsClient] = useState(false);
  
  useEffect(() => {
    setIsClient(true);
  }, []);
  
  return isClient;
};
  • 장점: 깜빡임 방지, 재사용 가능한 패턴, 성능 최적화, SSR 안전성 확보, 테스트 용이성
  • 단점: 초기 설정이 복잡할 수 있음

🎯 왜 이 방법을 선택했나?

선택한 방법: Isomorphic Hook 패턴

import React, { useState, useEffect, useRef, useLayoutEffect } from "react";

// 가장 많이 사용되는 isomorphic hook 패턴
const useIsomorphicLayoutEffect = typeof window !== 'undefined' 
  ? useLayoutEffect 
  : useEffect;

// 클라이언트 상태 체크 hook
const useClientOnly = () => {
  const [isClient, setIsClient] = useState(false);
  
  useEffect(() => {
    setIsClient(true);
  }, []);
  
  return isClient;
};

const MoimListPage = () => {
  const [visibleCount, setVisibleCount] = useState(8);
  const [loading, setLoading] = useState(false);
  const isClient = useClientOnly();
  const loader = useRef();
  const observerRef = useRef();

  // Intersection Observer로 무한 스크롤 구현
  useIsomorphicLayoutEffect(() => {
    if (!isClient) return;

    const currentLoader = loader.current;
    
    if (currentLoader && typeof window !== 'undefined' && 'IntersectionObserver' in window) {
      observerRef.current = new window.IntersectionObserver(
        (entries) => {
          if (
            entries[0].isIntersecting &&
            !loading &&
            visibleCount < filteredMoims.length
          ) {
            setLoading(true);
            setTimeout(() => {
              setVisibleCount((prev) =>
                Math.min(prev + 8, filteredMoims.length)
              );
              setLoading(false);
            }, 800);
          }
        },
        { threshold: 0.1, rootMargin: "100px" }
      );
      
      observerRef.current.observe(currentLoader);
    }

    return () => {
      if (observerRef.current) {
        observerRef.current.disconnect();
      }
    };
  }, [filteredMoims.length, loading, visibleCount, isClient]);

  return (
    // JSX 렌더링
  );
};

선택 이유

  1. 성능 최적화
    • useLayoutEffect를 사용하여 깜빡임 방지
    • 동기적으로 실행되어 사용자 경험 향상
  2. 재사용성
    • Custom Hook으로 분리하여 다른 컴포넌트에서도 사용 가능
    • 관심사 분리로 코드 유지보수성 향상
  3. 무한 스크롤 최적화
    • reshold: 0.1, rootMargin: "100px"로 부드러운 로딩
    • 부 렌더링으로 불필요한 로딩 방지
  4. SSR 안전성
    • 와 클라이언트 환경 모두에서 안전하게 동작
    • 드레이션 오류 방지

🎉 결론
현재 프로젝트에서는 가장 안전하고 견고한 방법인 "useRef + 조건문"을 사용했습니다!

profile
풀스택 개발자로서의 도전을 하는 중입니다. 많은 응원 부탁드립니다!!😁

0개의 댓글