전형적인 Hydration error가 발생하였다.
내가 실행하는 환경은 Next.js로 SSR이 가능하기 때문에
처음 코드가 실행될 때 client side가 아닌 server side에서 실행될 수도 있다.
local storage는 client side에서만 실행이 가능하기 때문에
(server side에는 window 객체가 없기 때문에)
client side에서만 실행이 가능하도록 수정해주어야 한다.
Error: Hydration failed because the initial UI does not match what was rendered on the server.
Warning: Expected server HTML to contain a matching <div> in <div>.
mounted 된 순간부터 client side이다.
useEffect + deps []
에 실행되는 시점이 처음으로 mounted되는 시점이기 때문에
mounted를 체크하는 상태를 추가해주면 된다
const [isMounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
},[])
isMounted의 초기 상태는 false 여야 한다.
mounted 상태를useState
로 관리해야한다.
만약useRef
로 관리하게 된다면, isMounted가 바뀌어도 화면이 렌더링 되지 않기 때문이다.
import { useState } from 'react';
/**
* @description 페이지 새로 고침을 통해 상태가 유지되도록 로컬 저장소에 동기화합니다.
*
* @param key 로컬 저장소에 저장될 키
* @param initialValue 초기 값
* @returns [storedValue, setValue] - 로컬 저장소에 저장된 값, 저장 함수
*/
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue] as const;
}
export default useLocalStorage;
기존 코드에서 isMounted
를 체크하는 코드만을 추가하면 된다.
import { useEffect, useState } from 'react';
/**
* @description 페이지 새로 고침을 통해 상태가 유지되도록 로컬 저장소에 동기화합니다.
*
* @param key 로컬 저장소에 저장될 키
* @param initialValue 초기 값
* @returns [storedValue, setValue] - 로컬 저장소에 저장된 값, 저장 함수
*/
function useLocalStorage<T>(key: string, initialValue: T) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
console.error(error);
}
};
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
if (isMounted) {
return [storedValue, setValue] as const;
}
return [initialValue, setValue] as const;
}
export default useLocalStorage;