ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Bug Fix] Next.js 14 App Router 버전 window is not defined 문제 해결
    Front-end 개발/FE 버그 2024. 11. 20. 09:19

     

    Problem


    프로젝트를 진행하면서 로컬스토리지로부터 객체 데이터를 가지고 와서, 파싱하고 state에 저장하는 과정이 반복해서 수행되고 있었다. 반복되는 과정의 코드를 줄이기 위해 커스텀 훅 useLocalStorage을 만들었다.

    로컬스토리지는 브라우저의 저장소로 데이터를 key-value 쌍으로 저장할 수 있다. 그런데 Next.js는 서버 사이드 렌더링을 지원하기 때문에, 서버 측에서 코드가 실행될 때 문제가 발생하게 된다. 로컬스토리지를 사용할 때는 Window 객체가 존재하는 클라이언트 사이드에서만 접근해야 한다.

    Reference 에러 발생: window is not defined

    Next.js 클라이언트 컴포넌트에서는 일반적으로 useEffect를 사용하여 컴포넌트가 마운트된 후에 로컬스토리지에 접근하는 방식을 사용한다. 하지만 내 상황에서는 useState를 초기화 해야 하는 과정에서 로컬스토리지 값을 사용하고 싶었기에 맞지 않았다.

    'use client';
    
    import { useState } from 'react';
    
    export default function useLocalStorage<T>(key: string, initialValue?: T) {
      const [storedValue, setStoredValue] = useState<T>(() => {
        const item = window.localStorage.getItem(key);
        return item ? JSON.parse(item) : initialValue;
      });
    
      const setValue = (value: T) => {
        setStoredValue(value);    
        window.localStorage.setItem(key, JSON.stringify(value));
      };
    
      return [storedValue, setValue] as const;
    }

     

    Solution


    이 문제는 서버측에서 클라이언트 컴포넌트를 Next.js 서버에서 실행할 때 발생하는 참조 에러이기 때문에 타입처럼 조건문으로 분기처리를 해줘야 한다. 즉, window 객체를 사용할 때 조건부로 클라이어트 사이드에서만 실행되도록 하면 된다.

    만약 window 객체가 인식 안 될 경우, storedValue는 props로 받은 초기값으로 무조건 초기화 되어버린다. 반면에 window 객체가 인식되면 로컬 스토리지의 데이터를 가져와 초기화를 해준다. 로컬 스토리지가 비어 있다면 마찬가지로 props로 받은 초기값을 활용하여 초기화 한다.

    'use client';
    
    import { useState } from 'react';
    
    export default function useLocalStorage<T>(key: string, initialValue?: T) {
      const [storedValue, setStoredValue] = useState<T>(() => {
        if (typeof window !== 'undefined') {
          const item = window.localStorage.getItem(key);
          return item ? JSON.parse(item) : initialValue;
        }
        return initialValue;
      });
    
      const setValue = (value: T) => {
        setStoredValue(value);
        if (typeof window !== 'undefined') {
          window.localStorage.setItem(key, JSON.stringify(value));
        }
      };
    
      return [storedValue, setValue] as const;
    }

     

    Other Solution

    useEffect를 사용하는 방법에 대해 더 생각해봤는데,

    일단 initialValue로 초기화하고 컴포넌트가 마운트 된 이후에 useEffect를 사용해서 setStoredValue로 sotredValue를 초기화 해주는 방법도 가능할 것 같다.

    실제 프로젝트에서 문제 없는지 확인은 안했지만, 아래 코드도 문제없이 작동 할 것 같다.

    'use client';
    
    import { useEffect, useState } from 'react';
    
    export default function useLocalStorage<T>(key: string, initialValue?: T) {
      const [storedValue, setStoredValue] = useState<T>(initialValue);
    
      const setValue = (value: T) => {
        setStoredValue(value);
        if (typeof window !== 'undefined') {
          window.localStorage.setItem(key, JSON.stringify(value));
        }
      };
      
      useEffect(() => {
        const item = window.localStorage.getItem(key);
        item && setValue(JSON.parse(item))
      }, [key])
    
      return [storedValue, setValue] as const;
    }
Designed by Tistory.