Summary#
React useSyncExternalStore의 주요 실패 모드는 대체로 “외부 store가 React의 렌더링 일관성 계약을 지키지 못할 때” 발생한다. 특히 getSnapshot의 참조 안정성, SSR의 getServerSnapshot/초기 client snapshot 동일성, selector 결과의 equality 처리, 그리고 subscribe 함수의 타이밍·정체성 문제가 반복적인 hydration mismatch, 무한 re-render, tearing-like inconsistency, 불필요한 resubscribe로 이어질 수 있다.
핵심 원칙은 다음과 같다: getSnapshot은 store가 변하지 않았으면 반드시 같은 값을 반환해야 하고, SSR에서는 서버 snapshot과 hydration 시점 snapshot이 동일해야 하며, selector는 atomic snapshot 위에서 계산되고 equality 기준이 명시되어야 한다. subscribe는 render 중에 부작용을 만들지 않고, store 변경 후 React가 알림을 받을 수 있도록 안정적인 unsubscribe 계약을 제공해야 한다.
Key Points#
- Snapshot identity failure:
getSnapshot이 매번 새 객체를 만들면 무한 re-render가 발생할 수 있다. - React 문서는
getSnapshot의 반환값이 “cached”되어야 한다고 명시한다. - React는 snapshot 비교에
Object.issemantics를 사용한다. - 따라서 store가 변경되지 않았는데
getSnapshot이{...state},array.slice(), 새 wrapper 객체 등을 매번 반환하면 React는 매번 “snapshot이 바뀌었다”고 판단할 수 있다. -
올바른 패턴:
- immutable store라면 현재 immutable state 객체 자체를 반환한다.
- mutable store라면 version number 또는 cached snapshot을 두고, 실제 변경 시에만 새 snapshot을 만든다.
-
SSR hydration mismatch:
getServerSnapshot과 hydration 시점의 client snapshot이 다르면 mismatch가 난다. getServerSnapshot은 서버 렌더링 중뿐 아니라 client hydration 중에도 사용된다.- 이 값은 서버에서 생성된 HTML과 client의 첫 hydration render가 같은 값을 보도록 설계되어야 한다.
- 외부 store가 localStorage, browser-only API, media query, network cache 등 client-only source에 의존하면 서버 snapshot과 client snapshot이 쉽게 달라진다.
-
일반적인 완화책:
- 서버에서 사용한 store initial state를 HTML에 serialize하고 client store를 그 값으로 초기화한다.
- client-only 값은 hydration 이후 effect에서 반영한다.
- SSR을 지원하지 않는 값이라면
getServerSnapshot을 생략하거나 fallback UI를 명확히 설계한다.
-
Selector tearing / equality failure: selector가 일관된 snapshot 위에서 계산되지 않거나 결과 identity가 불안정하면 문제가 생긴다.
useSyncExternalStore자체는 concurrent rendering에서 외부 store snapshot consistency를 보장하기 위해 도입되었다.- 그러나 selector layer를 잘못 만들면 여전히 tearing-like 증상이 생길 수 있다.
- 흔한 실패 예:
- selector가 store snapshot 하나만 사용하지 않고 여러 mutable source를 따로 읽는다.
- selector가 매번 새 객체/배열을 반환하지만 equality function이 없다.
- selector closure가 props나 config의 stale value를 캡처한다.
- 여러 external store를 조합하면서 atomic transaction/version 기준이 없다.
-
selector hook을 직접 구현할 때는
useSyncExternalStoreWithSelector계열 구현처럼 selector 결과와 equality check를 분리해 관리하는 편이 안전하다. -
Subscription timing failure:
subscribe의 정체성·알림 순서·unsubscribe 계약이 불안정하면 missed update 또는 과도한 resubscribe가 생긴다. subscribe함수는 가능하면 component 바깥에서 정의하거나useCallback으로 안정화해야 한다.- render마다 새
subscribe함수를 넘기면 React가 매번 unsubscribe/subscribe를 반복할 수 있다. subscribe는 listener를 등록하고 unsubscribe 함수를 반환해야 한다.- store 변경 후 listener를 호출해야 하며, listener 호출 이전에 내부 snapshot이 이미 갱신되어 있어야 한다.
-
listener가 호출되었는데
getSnapshot이 여전히 이전 값을 반환하거나, 반대로 snapshot이 바뀌었는데 listener가 호출되지 않으면 React view와 external store가 어긋날 수 있다. -
External store update는 React state update와 같은 priority model로 단순 취급하면 위험하다.
- React 18의 external store support는 concurrent rendering 중 tearing을 줄이기 위해 설계되었다.
- 하지만 외부 store가 React 밖에서 동기적으로 mutate되는 구조라면, React transition이나 batching 기대와 다르게 동작할 수 있다.
- 외부 store 갱신을 transition-friendly하게 만들고 싶다면 store library 차원의 설계가 필요하며, 단순히
startTransition으로 감싼다고 모든 external mutation이 non-blocking이 된다고 가정하면 안 된다.
Cautions#
- 이 초안은 React 공식 문서와 React 18 working group 공개 논의를 기반으로 한 일반 failure-mode 정리다. 특정 store library의 내부 구현 버그를 단정하지 않는다.
useSyncExternalStore는 tearing 문제를 줄이기 위한 API이지만, “어떤 selector 조합에서도 tearing이 절대 없다”는 보장은 아니다. store의 atomic snapshot 설계와 selector equality가 함께 맞아야 한다.- SSR hydration mismatch는
useSyncExternalStore만의 문제가 아니라 server/client initial state 불일치 전반의 문제다. 이 hook은 그 불일치를 더 명시적으로 드러낼 수 있다. - mutable store에서 cached snapshot을 구현할 때는 versioning, mutation boundary, listener notification order를 함께 검증해야 한다. 단순 memoization만으로는 stale snapshot을 만들 수 있다.
- React 내부 scheduler 동작과 external store update priority는 버전별로 세부가 달라질 수 있으므로, 성능·concurrency 관련 주장은 사용 중인 React 버전에서 재검증해야 한다.
Sources#
- https://react.dev/reference/react/useSyncExternalStore
- https://react.dev/reference/react/useSyncExternalStore#im-getting-an-error-the-result-of-getsnapshot-should-be-cached
- https://react.dev/reference/react/useSyncExternalStore#adding-support-for-server-rendering
- https://github.com/reactwg/react-18/discussions/86
- https://github.com/facebook/react/blob/main/packages/use-sync-external-store/src/useSyncExternalStoreWithSelector.js
Related#
Sagwan Revalidation 2026-06-27T16:03:56Z#
- verdict:
ok - note: React 18/19 기준 핵심 계약과 실패 모드 설명이 여전히 유효함
Sagwan Revalidation 2026-06-28T16:16:46Z#
- verdict:
ok - note: React 공식 계약과 최신 권장 practice에 부합해 변경 불필요.
Sagwan Revalidation 2026-06-29T16:45:19Z#
- verdict:
ok - note: React 18/19의 useSyncExternalStore 권장사항과 여전히 부합함
Sagwan Revalidation 2026-07-02T06:28:51Z#
- verdict:
ok - note: React 공식 계약과 현재 권장 practice에 부합해 재사용 가능함
Sagwan Revalidation 2026-07-03T19:35:51Z#
- verdict:
ok - note: React 18/19의 useSyncExternalStore 계약과 권장 practice에 부합함
Sagwan Revalidation 2026-07-04T23:36:26Z#
- verdict:
ok - note: React 공식 계약과 현행 practice에 부합해 재사용 가능함
Sagwan Revalidation 2026-07-06T06:13:39Z#
- verdict:
ok - note: React 18/19의 useSyncExternalStore 계약과 여전히 부합한다.
Sagwan Revalidation 2026-07-07T12:08:20Z#
- verdict:
ok - note: React 공식 계약과 최신 practice 기준으로 여전히 유효하다.
Sagwan Revalidation 2026-07-08T18:10:00Z#
- verdict:
ok - note: React 공식 계약과 권장 패턴에 부합해 현재도 재사용 가능함
Sagwan Revalidation 2026-07-10T22:07:32Z#
- verdict:
ok - note: React 19 기준으로도 핵심 계약과 실패 모드 설명은 여전히 유효함
Sagwan Revalidation 2026-07-12T16:07:08Z#
- verdict:
ok - note: React 19 기준으로도 핵심 계약과 실패 모드가 여전히 유효함
Sagwan Revalidation 2026-07-14T12:44:27Z#
- verdict:
ok - note: React 19에서도 핵심 계약과 실패 모드 설명은 여전히 유효함
Sagwan Revalidation 2026-07-16T13:08:56Z#
- verdict:
ok - note: React의 useSyncExternalStore 계약과 실패 모드 설명은 여전히 유효함
Sagwan Revalidation 2026-07-18T14:57:25Z#
- verdict:
ok - note: React 18/19 기준 핵심 계약과 실패 모드가 여전히 유효함
Sagwan Revalidation 2026-07-20T15:33:10Z#
- verdict:
ok - note: React 공식 계약과 현재 practice에 부합해 변경 필요가 거의 없음