useSyncExternalStore를 사용하면 동시성 렌더링(concurrent rendering)이 유발할 수 있는 시각적 오류 없이 React 외부의 데이터를 읽을 수 있습니다. 이 훅은 useEffect를 사용하여 외부 값을 로컬 상태로 가져오던 기존 패턴을 대체합니다. 기존 패턴은 UI의 일부가 동일한 렌더링 중에 서로 다른 값을 보여주는 "티어링(tearing)" 현상을 일으킬 수 있습니다.
기존 패턴이 동시성 환경에서 깨지는 이유
React 18 이전에는 다음과 같은 방식이 일반적이었습니다:
const [value, setValue] = useState(initial);
useEffect(() => {
const unsubscribe = externalSource.subscribe(v => setValue(v));
return unsubscribe;
}, []);
이펙트는 컴포넌트가 렌더링된 후에 실행됩니다. React의 동시성 렌더러가 렌더링을 일시 중지, 중단 또는 재실행하기로 결정하면, 이펙트는 상태를 읽는 UI와 다른 시점에 실행될 수 있습니다. 그 결과 불일치가 발생합니다. 한 컴포넌트는 이전 값을 읽고, 다른 컴포넌트는 새로운 값을 읽어 화면에 잠시 일관되지 않은 데이터가 표시될 수 있습니다. 이러한 "티어링(tearing)" 현상은 순수하게 동기적인 환경에서는 해롭지 않지만, React가 여러 버전의 UI를 병렬로 렌더링할 때는 눈에 보이는 버그가 됩니다.
useSyncExternalStore가 문제를 해결하는 방법
이 훅은 두 개의 콜백을 필요로 합니다:
- subscribe – 리스너를 등록하고 이를 제거하는 함수를 반환합니다.
- getSnapshot – 외부 소스의 현재 값을 반환합니다.
React는 컴포넌트가 마운트될 때 subscribe를 호출하고, 언마운트될 때 이를 해제합니다. 매 렌더링마다 getSnapshot을 호출하여 동일한 외부 스토어를 사용하는 모든 컴포넌트가 정확히 동일한 스냅샷을 읽도록 보장합니다. 읽기 작업이 렌더링 단계 내에서 발생하므로, 렌더러는 트리 전체의 업데이트를 조정하여 티어링을 제거할 수 있습니다.
Zustand 및 Redux와 같은 라이브러리는 이미 이 훅으로 스토어를 감싸고 있으므로, React 18의 동시성 기능을 즉시 사용할 수 있습니다.
훅이 오작동하지 않게 하는 규칙들
- 컴포넌트에 필요한 것만 반환하세요 – 큰 객체를 반환하면 React가 매 업데이트마다 깊은 구조를 비교해야 합니다. 좁은 범위의 스냅샷을 유지하면 변경 감지 비용을 낮게 유지할 수 있습니다.
- 참조 동일성(referential identity)을 유지하세요 – 외부 값이 변경되지 않았다면, getSnapshot은 반드시 동일한 객체 참조를 반환해야 합니다. 호출할 때마다 새로운 객체를 반환하면 React가 매 렌더링마다 변경 사항이 있다고 판단하여 무한 렌더링 루프가 발생합니다.
- 외부 소스로 사용을 제한하세요 – 로컬 컴포넌트 상태는
useState나useReducer를 사용해야 합니다. 순수하게 내부적인 값을 위해 외부 스토어 훅을 사용하는 것은 불필요한 간접 참조를 추가할 뿐입니다.
useSyncExternalStore를 사용해야 하는 경우
- 브라우저 API 래핑 (window 크기, 네트워크 온라인 상태, 미디어 쿼리 등).
- React를 모르는 일반 JavaScript 스토어 연결.
- 프롭 드릴링(prop-drilling)을 많은 컴포넌트가 구독하는 전역 읽기 전용 소스로 대체할 때.
반론
useState + useEffect 패턴은 데이터가 단순하고 변경이 드문 경우, 특히 애플리케이션에서 동시성 기능을 활성화하지 않은 경우에는 여전히 작동합니다. 구독 로직이 이펙트 내에 명시적으로 존재하기 때문에 일부 개발자들은 이를 더 이해하기 쉬운 저수준 접근 방식으로 여깁니다. 하지만 동시성 렌더링이 활성화되면 티어링의 위험이 따릅니다. 이미 React 18의 새로운 기능을 사용하는 프로젝트라면, useSyncExternalStore가 제공하는 추가적인 안전성이 약간의 보일러플레이트 증가보다 훨씬 더 큰 가치를 제공합니다.
주의 깊게 살펴볼 사항
- 스토어의 getSnapshot이 안정적인 참조를 반환하는지 확인하세요. 개발 중에 참조를
console.log로 찍어보면 의도치 않은 객체 재생성을 찾아낼 수 있습니다. - 구독 함수를 가볍게 유지하세요. 무거운 작업은 React에 전달되는 콜백이 아니라 외부 소스 내부에서 수행되어야 합니다.
- 티어링이 발생하지 않는지 확인하기 위해 React의 동시성 모드(예:
ReactDOM.createRoot와 함께act사용)에서 컴포넌트를 테스트하세요.
라이브 데모에서는 React 외부의 단순한 카운터를 동기화하고 시각적 오류 없이 여러 컴포넌트를 업데이트하는 훅의 동작을 확인할 수 있습니다. 데모는 https://usesyncexternalstore.vercel.app/ 에서 확인할 수 있습니다. 소스 코드는 GitHub에 있으며, 더 자세한 글에서 구현 세부 사항을 설명합니다.
핵심 요약: React가 UI의 일부를 순서에 상관없이 렌더링할 수 있는 환경에서, useSyncExternalStore는 외부 데이터와 컴포넌트 렌더링 사이를 잇는 신뢰할 수 있는 가교 역할을 합니다. 시각적 일관성을 유지하고, 전역 스토어로 확장 가능하며, 이미 인기 있는 상태 관리 라이브러리에 내장되어 있습니다. React 외부의 상태를 읽어야 하는 곳이라면 어디든 이 훅을 도입하되, 최대한의 이점을 누릴 수 있도록 스냅샷을 작고 안정적으로 유지하세요.
