정의
React 밖에 있는 상태(라우터·전역 스토어·캐시 등)를 컴포넌트에 붙여 화면과 동기화하고 싶을 때 쓰는 훅이다. useSyncExternalStore는 외부 스토어를 React 렌더 트리에 동기화한다. 스토어가 바뀌었다는 알림(구독 콜백)이 온 뒤, getSnapshot()(현재 상태를 한 값으로 뽑아내는 함수)이 돌려준 값을 이전 스냅샷과 Object.is로 비교한다. Object.is는 JS 기본 동등 비교로, 원시값(문자열·숫자)은 값이 같으면, 객체는 같은 참조일 때만 "같다"고 본다. 같다고 판정되면 리렌더를 건너뛴다. 그래서 스냅샷이 "내용은 바뀌었는데 비교상 같은 값"이면 구독자는 업데이트를 영원히 놓친다. 이는 얕은 비교(객체 속성을 한 겹 비교)나 구조적 공유(structural sharing, 바뀐 부분만 새 객체로 갈고 나머지는 재사용)와 다른, 스냅샷 값 자체의 동일성 문제다.
맥락에서 이해하기
id별 구독으로 행 리렌더를 줄이려면, 내용이 안 바뀐 행은 getSnapshot()이 이전과 같은 객체 참조를 돌려줘야 React가 리렌더를 스킵한다. 반대로 스냅샷에 매번 새 객체를 만들거나, 의미는 같은데 원시값이 Object.is로 같으면, store는 바뀌었는데 화면은 안 바뀌는 양방향 함정이 생긴다. read-model projection과 한 쌍으로 이해하는 것이 좋다.
왜 필요한가
레이아웃 매니저, 포커스 스토어, 캐시처럼 React 밖에 있는 상태를 UI에 붙일 때 useSyncExternalStore가 표준 경로다. 스냅샷에 new Date().toISOString()처럼 밀리초 해상도 타임스탬프만 넣으면, 같은 ms 안에 mount와 update가 연속될 때 두 스냅샷 문자열이 동일해진다. React는 “변경 없음”으로 판단해 리렌더를 드롭하고, 부하가 높을수록(테스트 burst, 빠른 연속 mutation) 재현률이 올라가 부하 의존 flake가 된다. SSR에서는 getServerSnapshot이 없으면 서버에서 “Missing getServerSnapshot” 에러가 난다.
동작 원리
- 컴포넌트가
subscribe(listener)로 스토어에 등록한다. - 스토어가 바뀌면
listener가 호출되고, React가getSnapshot()을 다시 읽는다. Object.is(prevSnapshot, nextSnapshot)이true면 리렌더를 하지 않는다.- 참조가 달라도 원시값(문자열·숫자)이 같으면 스킵된다.
subscribe만 되고 스냅샷이 안 바뀌면 UI는 그대로다.
| 항목 | 역할 | 주의점 |
|---|---|---|
| 스냅샷 비교 | Object.is(prev, next) | 값 동일 = 리렌더 없음 |
| 단조 revision | iso#1, iso#2처럼 항상 증가하는 suffix | 소비자가 문자열만 비교할 때 안전 |
getServerSnapshot | SSR/하이드레이션용 초기 스냅샷 | 누락 시 서버 요청마다 에러 |
subscribe | 변경 시 리스너 호출 | 스냅샷이 안 바뀌면 리렌더 없음 |
실무 적용
타임스탬프는 UX 표시용으로만 쓰고, 동일성 키로는 단조 증가 counter(또는 #revision suffix)를 스냅샷에 포함한다.
SSR을 타는 컴포넌트는 세 번째 인자 getServerSnapshot을 항상 제공한다. 회귀 테스트는 같은 틱/같은 ms burst(예: 연속 update)에서 스냅샷 문자열이 매번 달라지는지 검증한다.
트레이드오프
- revision suffix는 구현이 단순하고
Object.is실패를 보장하지만, 스냅샷 문자열이 “표시용 시각”과 섞이면 파싱이 필요해진다. 표시용 필드와 동일성 키를 분리하는 편이 낫다. - 매 mutation마다 새 객체 참조를 스냅샷으로 쓰면 항상 리렌더되지만, 불필요한 렌더 비용이 커진다. 원시값 + revision이 보통 더 싸다.
사용하면 안 되는 경우
- 스토어가 React state/
useReducer만으로 충분할 때 — 외부 스토어 구독이 불필요하다. - 스냅샷이 매 렌더마다 새 객체를 만들어 항상 다른 참조가 되는 설계 — 구독 모델의 이점이 사라지고 렌더 폭주만 난다.
흔한 실수
- 스냅샷을 timestamp·랜덤·비결정적 값에만 의존한다.
subscribe는 호출되는데getSnapshot이 같은 문자열을 반환해 “가끔” UI가 안 바뀐다.- SSR 경로에서
getServerSnapshot을 빼먹는다. - 실패가 부하와 상관있을 때 “테스트 flaky”로만 치부하고 스냅샷 동일성을 의하지 않는다.
관련 개념
- ssr-hydration-mismatch — 서버/클라이언트 첫 렌더 정합과
getServerSnapshot이 맞닿는 지점. - focused-guest-editor — 포커스 스토어를 외부 상태로 둘 때 같은 구독 모델이 자주 쓰인다.