BlogReactuseSyncExternalStore

useSyncExternalStore

목적

useSyncExternalStore는 Concurrent Mode 및 External Store 사용 시 발생하는 상태 불일치(Tearing) 문제를 해결하기 위해 도입된 훅입니다.

주요 개념

  • Tearing: UI 상태가 의도치 않게 일관성이 없는 상태로 렌더링되는 현상입니다.
  • Concurrent Mode: 더 매끄럽고 응답성 있게 업데이트할 수 있도록 비동기 렌더링을 지원하는 새로운 렌더링 모드입니다. startTransition, useTransition을 사용하여 업데이트 함수의 우선 순위를 낮추어 Concurrent Mode를 활성화할 수 있습니다. 또한, useDeferredValue를 사용하여 값에 대한 업데이트 우선 순위를 낮출 수 있습니다.
  • External Store: 리액트의 내부 상태 관리 API(useState, useReducer)가 아닌 외부 상태 관리 라이브러리(예: MobX, Redux, Recoil 등), Web API, 전역 변수 등을 의미합니다.

사용 방법

다음은 useSyncExternalStore를 전역 변수와 함께 사용하는 방법입니다.

export type TodoItem = {
  id: string
  content: string
}
 
export type TodoData = {
  todos: TodoItem[]
}
 
let store: TodoData = {
  todos: [],
}
 
export type TodoReducerAction = { type: 'ADD'; todo: TodoItem } | { type: 'REMOVE'; todoId: string }
 
export function todoReducer(state: TodoData, action: TodoReducerAction): TodoData {
  switch (action.type) {
    case 'ADD': {
      return {
        ...state,
        todos: [...state.todos, action.todo],
      }
    }
    case 'REMOVE': {
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.todoId),
      }
    }
  }
}
 
export function dispatchTodo(action: TodoReducerAction) {
  store = todoReducer(store, action)
  emitChangeListener()
}
 
let listeners: Array<() => void> = []
 
function emitChangeListener() {
  for (const listener of listeners) {
    listener()
  }
}
 
export const registerTodoStore = {
  subscribe(listener: () => void) {
    listeners = [...listeners, listener]
 
    return () => {
      listeners = listeners.filter((l) => l !== listener)
    }
  },
  getSnapshot() {
    return store
  },
}
 
export function useSyncTodoStore() {
  const { subscribe, getSnapshot } = registerTodoStore
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
}

이 코드는 useSyncExternalStore를 사용하여 외부 상태와 동기화된 store를 만드는 방법을 보여줍니다. 상태 변화에 따라 리렌더링할 수 있도록 구독(listener)을 설정하고, 현재 상태의 스냅샷을 반환합니다.