notes
useSyncExternalStore
useSyncExternalStore의 외부 스토어 구독·스냅샷 처리와 React 연동 메모.
- React 밖에 있는 값을 구독과 스냅샷으로 읽는 훅이다
- useEffect 로 흉내 내면 SSR 과 동시 렌더링에서 값이 어긋난다
- getSnapshot 은 같은 상태면 꼭 같은 값을 돌려줘야 한다
왜 필요한가
window.innerWidth, matchMedia, localStorage, online 이벤트 같은 건 React 가 모르는 값이다. 예전에는 useState 에 담고 useEffect 에서 이벤트를 달아 갱신했다.
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
이렇게 하면 두 군데서 문제가 생긴다. 서버에는 window 가 없어서 hydration 이 어긋나고, 동시 렌더링 중간에 값이 바뀌면 한 화면 안에서 컴포넌트마다 값이 다르게 보일 수 있다(tearing). useSyncExternalStore 는 이걸 React 가 알아서 맞춰 주도록 만든 훅이다.
인자 세 개
subscribe 는 값이 바뀔 때 불릴 callback 을 등록하고 해제 함수를 돌려준다. getSnapshot 은 지금 값을 돌려주는데, 상태가 안 바뀌었으면 같은 값을 돌려줘야 한다. getServerSnapshot 은 서버 렌더링과 hydration 때 쓸 값이고, 클라이언트 전용이면 빼도 된다.
const snapshot = useSyncExternalStore(
subscribe, // (callback) => unsubscribe
getSnapshot, // () => 현재 값
getServerSnapshot? // () => SSR 용 값 (선택)
);
동작 흐름
처음에 getSnapshot 으로 값을 읽고 subscribe 로 변경을 기다린다. 외부 값이 바뀌어 callback 이 불리면 React 가 getSnapshot 을 다시 부르고, 이전 값과 Object.is 로 비교해서 다를 때만 다시 렌더한다.
function subscribe(callback: () => void) {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
}
function getSnapshot() {
return window.innerWidth;
}
function getServerSnapshot() {
return 0; // SSR 기본값
}
function useWindowWidth() {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}let count = 0;
const listeners = new Set<() => void>();
const store = {
subscribe(cb: () => void) {
listeners.add(cb);
return () => listeners.delete(cb);
},
getSnapshot() {
return count;
},
increment() {
count += 1;
listeners.forEach((cb) => cb());
},
};
function Counter() {
const n = useSyncExternalStore(store.subscribe, store.getSnapshot);
return <button onClick={() => store.increment()}>{n}</button>;
}어디에 쓰이나
직접 쓸 일은 많지 않다. Zustand 나 react-redux 같은 상태 라이브러리가 안에서 이 훅을 쓰고 있고, 직접 쓴다면 matchMedia, online, visibility 같은 브라우저 값이나 React 밖 이벤트 버스를 구독할 때 정도다. 회사 코드베이스에도 직접 쓴 곳은 없고 라이브러리 안에서만 돌고 있었다.
조심할 것
getSnapshot 에서 매번 새 객체를 만들어 돌려주면 매번 다른 값으로 보여서 무한 리렌더가 난다. 객체를 돌려줘야 하면 캐시해 두고 값이 바뀔 때만 새로 만든다. Next.js 에서 window 같은 브라우저 값을 읽을 때는 getServerSnapshot 을 넣는 게 안전하다.
정리하면 useEffect 는 mount 뒤 부수 효과, use 는 Promise 나 Context 읽기, useSyncExternalStore 는 React 밖 스토어 구독이다.