useSyncExternalStore로 테마 상태 다루기

silvrash·2026년 5월 19일

React

목록 보기
6/6

다크 모드 토글을 구현하다 보면 한 번쯤 마주치는 패턴이 있다. 테마 값을 localStorage에 저장하고, document.documentElementdata-theme 속성에 반영하고, React에서는 그 값을 읽어서 UI에 표시하는 식이다. 이때 React state와 DOM attribute, 두 군데에 같은 값이 존재하게 되는데 이 둘을 어떻게 동기화할 것인가가 관건이다.

기존에는 useEffect로 마운트 시점에 DOM 값을 읽어와 useState에 복사하는 방식을 썼다. 동작은 했지만 React lint가 경고를 띄웠고, 첫 렌더 직후 한 번 더 리렌더링이 발생하는 구조였다. useSyncExternalStore로 바꾸고 나니 훨씬 깔끔해졌기에 정리해 둔다.

useSyncExternalStore가 무엇인가

useSyncExternalStore는 React가 React 밖에 있는 상태를 안전하게 읽고 구독하기 위한 훅이다.

이번 케이스에서는 테마 상태의 실제 원천이 React state가 아니라 DOM의 document.documentElement.dataset/data-theme이다. 그래서 React 입장에서는 data-theme외부 저장소가 된다.

기본 형태는 다음과 같다.

const value = useSyncExternalStore(
  subscribe,
  getSnapshot,
  getServerSnapshot,
);

각 인자의 의미는 이렇다.

  • subscribe(listener)
    외부 상태가 바뀌었을 때 React에게 알려주는 함수다. 상태가 바뀌면 listener()를 호출해야 한다.
  • getSnapshot()
    현재 외부 상태를 읽는 함수다. 브라우저에서 실제 값을 반환한다.
  • getServerSnapshot()
    SSR 중 사용할 기본값이다. Next.js에서는 서버 렌더링이 있으므로 필요하다.

테마 코드에 적용하기

지금 ThemeProvider에서는 대략 이런 구조를 가진다.

const theme = useSyncExternalStore(
  subscribeTheme,
  getThemeSnapshot,
  getServerThemeSnapshot,
);

즉 React는 다음과 같이 동작한다.

  1. 렌더링할 때 getThemeSnapshot()으로 현재 data-theme를 읽는다.
  2. subscribeTheme()으로 테마 변경 알림을 구독한다.
  3. setDocumentTheme()data-theme를 바꾸고 listener들을 호출한다.
  4. React가 다시 snapshot을 읽고 UI를 갱신한다.

원래 코드가 가진 문제

원래 코드의 문제는 이거였다.

useEffect(() => {
  const t = document.documentElement.getAttribute("data-theme") || "light";
  setTheme(t);
}, []);

첫 렌더는 "light"로 하고, effect가 실행된 직후 다시 setTheme()을 호출해서 한 번 더 렌더링했다.
React lint가 이 패턴을 "effect 안에서 동기 setState 호출"로 경고한 것이다.

useSyncExternalStore를 쓰면 data-theme처음부터 외부 상태로 취급하므로, effect로 React state에 복사하지 않아도 된다.

언제 쓰면 좋은가

쓸 만한 상황은 이렇다.

  • DOM, localStorage, browser API, 전역 store처럼 React 밖 상태를 구독할 때
  • 외부 값이 바뀌면 React UI도 같이 업데이트되어야 할 때
  • SSR/Concurrent Rendering 환경에서 외부 상태를 안정적으로 읽어야 할 때

반대로 굳이 안 써도 되는 경우도 있다.

  • 버튼 클릭 같은 일반 UI 상태
  • 폼 입력값
  • 모달 열림/닫힘
  • React 컴포넌트 안에서만 관리되는 상태

이런 건 그냥 useState가 맞다.

정리

이번 테마 코드는 실제 상태가 DOM attribute에 있고 React는 그걸 표시만 해야 하는 구조이기 때문에 useSyncExternalStore가 맞는 선택이었다.

훅 이름이 길고 인자도 셋이라 처음 보면 부담스럽지만, 결국 하는 일은 단순하다. "어떻게 읽을지, 어떻게 변화를 구독할지, 서버에서는 뭘 보여줄지" 세 가지를 React에게 알려주는 것이다. 외부 상태를 React state로 굳이 복사하지 않고도 동기화할 수 있다는 점에서, 비슷한 패턴을 만날 때마다 먼저 떠올려 볼 만한 도구다.

profile
Frontend Developer

0개의 댓글