useSyncExternalStore 훅을 들어본 적 있으신가요?
이 훅은 React 18버전에 등장했으나, 저는 최근에 알았습니다 💦
localStorage에 저장하고 있는 값을 사용하고 있었는데요. 만약 사용자가 개발자도구를 켜서 localStorage의 값을 바꾼다면 어떻게 될까요? React는 이 변경을 알아차릴까요?
localStorage는 web API라 외부 상태에 속합니다. 그래서, React에서 localStorage의 변경을 감지하고 싶다면 구독해야 합니다.
이런 경우에 useSyncExternalStore 훅을 사용할 수 있습니다.
useSyncExternalStore는 외부 store를 구독할 수 있는 React Hook입니다. - 공식문서
여기서 말하는 외부 store는 무엇을 가리키는 걸까요?
React에서 주로 state, props, context로 데이터를 관리합니다. 하지만, 이 외에 외부에서 저장된 데이터도 관리해야 하는 경우가 있습니다.
이 경우들을 외부 store라고 생각하시면 됩니다.
주로 커스텀 훅에서 호출합니다.
export const useStorage = (key: string, initialValue: UserRecordType[]) => {
const store = useSyncExternalStore(subscribe, getSnapshot);
// ...
};
Object.is
로 비교하여 변경 감지)사용법까지 알았으니 이제 훅으로 localStorage를 구독해볼까요?
특정 key에 해당하는 값을 구독하기 위해 인수로 key 값을 받습니다.
그리고 나서, 커스텀 훅 안에 useSyncExternalStore를 호출합니다.
export const useLocalStorage = (key: string) => {
const subscribe = (listener: () => void) => { /.../ };
const getSnapshot = () => { /.../ };
const store = useSyncExternalStore(subscribe, getSnapshot);
// ...
};
getSnapshot은 localStorage에서 key에 해당하는 값을 반환해줍니다.
subscribe 함수에서 storage 이벤트 핸들러를 달아줍니다. 여기서 storage 이벤트는 localStorage, sessionStorage와 같은 storage가 업데이트 될 때 발생합니다.
그리고 이벤트 핸들러를 제거하는 함수를 반환해주세요.
export const useLocalStorage = (key: string) => {
const getSnapshot = () => localStorage.getItem(key)
const subscribe = (listener: () => void) => {
window.addEventListener("storage", listener);
return () => {
window.removeEventListener("storage", listener);
};
};
const store = useSyncExternalStore(subscribe, getSnapshot);
return [store]
};
저희는 localStorage의 key에 해당하는 값인 store에 접근할 수 있게 됐습니다.
const [name] = useLocalStorage("name");
const [user] = useLocalStorage("user");
user.name
user.age
만약 구독하고 있는 store가 객체라서 위처럼 쓰고 싶은 경우에는 어떻게 하면 될까요?
커스텀 훅에서 store의 타입을 제너럴로 받아주면 됩니다.
useSyncExternalStore이 반환하는 store의 타입은 key에 해당하는 데이터가 있을 경우 string, 없을 경우 null이 됩니다.
store가 null인 경우를 대비하여 사용할 initialValue
를 인수로 받습니다.
const useLocalStorage = <T,>(key: string, initialValue: T) => {
// ...
const store = useSyncExternalStore(subscribe, getSnapshot); // string | null 타입
const value = useMemo(() => {
// localStorage의 key에 데이터가 없을 경우
if (store === null) return initialValue;
// localStorage의 key에 데이터가 있을 경우
const parsedValue = JSON.parse(store) as T;
return parsedValue;
}, [store, initialValue]);
return [value] as const;
};
그럼 아래와 같이 사용할 수 있습니다.
localStorage.setItem(key, newValue)
로 데이터를 변경할 수 있을 것 같은 느낌이 들어요 🤔
const useLocalStorage = <T,>(key: string, initialValue: T) => {
// ...
const setValue = (newValue: T) => {
const newValueStr =
typeof newValue === "string" ? newValue : JSON.stringify(newValue);
localStorage.setItem(key, newValueStr);
};
return [value, setValue] as const;
};
아래 예시는 이전에 봤던 name이랑 age가 저장된 user 코드에서 age를 1 증가하는 버튼을 추가하였습니다.
근데 아무리 버튼을 눌러도 화면에 age 값이 증가되지 않았습니다. 왜그럴까요?
그 이유는 age는 localStorage.setItem
으로 값이 변경되어, 브라우저에서만 변경을 감지하고 React에서 감지하지 못합니다.
React가 age 값 변경을 알아차리기 위해 storage 이벤트를 일으켜 핸들러를 실행해야 합니다. 이를 위해 사용하는 메서드는 dispatchEvent인데요. 이 메서드는 Event를 전달하여, 해당 이벤트에 대해 등록된 핸들러들을 실행해줍니다.
const useLocalStorage = <T,>(key: string, initialValue: T) => {
// ...
const setValue = (newValue: T) => {
const newValueStr =
typeof newValue === "string" ? newValue : JSON.stringify(newValue);
localStorage.setItem(key, newValueStr);
window.dispatchEvent(
new StorageEvent("storage", {
key,
newValue: newValueStr,
})
);
};
return [value, setValue] as const;
};
이제 +1 버튼을 누르면 age가 렌더링됩니다!