React 18 핵심: useSyncExternalStore 훅 완벽 가이드
React 개발자라면 누구나 한 번쯤 전역 상태 관리 라이브러리를 사용하며
고민에 빠진 적이 있을 것입니다.
Redux, Zustand, Recoil 등 수많은 라이브러리가 존재하지만, React 18 버전이
출시되면서 우리는 새로운 도전에 직면하게 되었습니다.
바로 동시성 렌더링(Concurrent Rendering)의 등장입니다.
"왜 내 앱의 UI가 순간적으로 일치하지 않지?"라는 의문을 가져본 적이
있으신가요?
이것은 React가 렌더링을 멈추고 다른 급한 작업을 처리하는 동안, 외부
데이터가 변경되어 발생하는 테어링(Tearing) 현상일 수
있습니다.
오늘은 이 문제를 해결하기 위해 React 팀이 야심 차게 내놓은 무기,
useSyncExternalStore에 대해 깊이 있게 파헤쳐
보겠습니다.
🚀 이 글을 통해 얻어갈 수 있는 것:
- React 18의 동시성 기능과 테어링(Tearing) 문제의 본질적 이해
- useEffect와의 결정적인 차이점 분석
- 외부 스토어(Store)를 React와 안전하게 동기화하는 실전 패턴
🧐 도대체 '테어링(Tearing)'이 무엇인가요?
본격적인 훅 설명에 앞서, 우리가 해결해야 할 '적'을 먼저 식별해야
합니다.
React 18 이전의 동기적 렌더링에서는 렌더링이 시작되면 멈추지 않고 끝까지
진행되었습니다.
하지만 동시성 렌더링에서는 React가 렌더링 도중 잠시
멈추고, 더 높은 우선순위의 작업(예: 클릭 이벤트)을 처리할 수 있게
되었습니다.
문제는 여기서 발생합니다.
컴포넌트 트리의 일부는 변경 전의 데이터(Red)로
렌더링되었는데, 렌더링이 일시 중지된 사이 외부 스토어의 값이
변경(Blue)되어 버립니다.
그 후 나머지 컴포넌트가 렌더링 될 때는
변경된 데이터(Blue)를 참조하게 됩니다.
결국 사용자는 한 화면에서
서로 다른 두 가지 상태의 UI를 동시에 목격하게 되는데,
이를 화면이 찢어졌다는 의미로 테어링(Tearing)이라고
부릅니다.
⚠️ 핵심 요약:
테어링은 렌더링 프로세스 도중에 외부 상태가 변경되어,
UI의 일관성이 깨지는 시각적 결함입니다.
🛡️ useEffect로는 부족한 이유와 해결책
"그냥 useEffect로 상태 변경을 구독하면 되지 않나요?"라고 반문하실 수
있습니다.
하지만 useEffect나 useLayoutEffect는 렌더링이
완료된 '이후'에 실행되거나 커밋 단계에서 실행됩니다.
동시성 모드에서는 이것만으로는 렌더링 도중 발생하는 상태 불일치를 완벽하게
막을 수 없습니다.
📊 useEffect vs useSyncExternalStore 비교
| 구분 | useEffect + useState | useSyncExternalStore |
|---|---|---|
| 렌더링 타이밍 | 렌더링 후 비동기 실행 | 렌더링 과정 중 동기적 실행 |
| 동시성 지원 | 테어링 발생 가능성 있음 | 테어링 완벽 방지 |
| 주 사용처 | 일반적인 사이드 이펙트 | 전역 상태 라이브러리, 브라우저 API 구독 |
useSyncExternalStore는 이름 그대로 외부 저장소(External
Store)를 React 렌더링과 동기화(Sync) 시키는 훅입니다.
이 훅을 사용하면 React는 스토어의 변경 사항이 있는지 렌더링 도중에도
지속적으로 확인하며, 변경이 감지되면 즉시 동기적으로 다시 렌더링을
수행하여 UI의 일관성을 강제합니다.
💻 실전 구현: 코드로 이해하기
이제 실제 코드를 통해 사용법을 알아보겠습니다.
이 훅은 크게 세 가지 인자를 받습니다.
subscribe, // 스토어 변경을 감지할 구독 함수
getSnapshot, // 현재 스토어 상태를 반환하는 함수
getServerSnapshot? // (옵션) SSR을 위한 초기 상태 함수
);
🛠️ 브라우저의 'online' 상태 추적하기
가장 쉬운 예시로, 사용자의 네트워크 상태(온라인/오프라인)를 추적하는
커스텀 훅을 만들어 보겠습니다.
브라우저의 API는 React 외부의 데이터이므로 이 훅을 사용하기에 아주 적절한
대상입니다.
스토어가 변경될 때 호출될 콜백을 등록하고, 구독을 해제하는 클린업 함수를
반환해야 합니다.
여기서는 window 객체의 이벤트를 구독합니다.
현재의 데이터를 반환합니다.
중요: 스토어가 변하지 않았다면 항상 동일한 값을
반환해야 불필요한 리렌더링을 막을 수 있습니다.
const isOnline = useSyncExternalStore(
subscribe,
getSnapshot
);
return isOnline;
}
function getSnapshot() {
return navigator.onLine;
}
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
위 코드를 사용하면 React는 네트워크 상태가 변경되는 즉시 컴포넌트를
동기화하여, 렌더링 도중 상태가 꼬이는 것을 방지합니다.
라이브러리 제작자가 아니더라도, 이러한 패턴을 익혀두면
window.resize, matchMedia 등 다양한 브라우저
API를 안전하게 연동할 수 있습니다.
🏁 더 견고한 앱을 위하여
React 18의 useSyncExternalStore는 단순한 API 추가가 아닌,
동시성 시대를 맞이하는 React의 철학을 보여주는 기능입니다.
물론 일반적인 앱 개발에서 애플리케이션 상태(Props, State)만으로 충분한
경우에는 굳이 이 훅을 사용할 필요는 없습니다.
하지만 전역 상태 라이브러리를 직접 구현하거나, 복잡한 외부 데이터 소스를
다뤄야 한다면 필수적으로 고려해야 할 도구입니다.
✅ Action Plan: 오늘 바로 적용해보기
-
현재 프로젝트에서
useEffect로window이벤트를 구독하고 있는 코드를 찾아보세요. -
해당 코드를
useSyncExternalStore를 사용하는 커스텀 훅으로 리팩토링 해보세요. - React 개발자 도구의 'Concurrent Mode' 시뮬레이션을 켜고 UI가 찢어지는 현상이 없는지 확인해 보세요.

댓글 쓰기