← 목록으로 돌아가기

[TIL] Jotai

Jotai

기존 전역 상태관리의 한계

  • Redux: action, reducer, selector를 모두 작성해야 하는 복잡한 보일러플레이트
  • Context API: 하나의 값이 변경되면 해당 Context를 구독하는 모든 컴포넌트가 리렌더링되는 성능 이슈
  • Zustand: 간단하지만 스토어가 커질수록 상태 분리와 모듈화가 어려움

Jotai의 접근법

  • Atomic 구조: 상태를 atom이라는 작은 단위로 분리
  • 필요한 atom들을 조합해서 복잡한 상태 구성
  • 사용하는 atom만 구독하므로 불필요한 리렌더링 없음

Pub-Sub 패턴

  • Publisher-Subscriber 패턴을 기반으로 동작
  • 각 atom이 Publisher 역할을 하고, 컴포넌트나 다른 atom들이 Subscriber가 되는 구조
// atom = Publisher
const countAtom = atom(0)

// 컴포넌트 = Subscriber
function Counter() {
  const [count, setCount] = useAtom(countAtom) // countAtom을 구독
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

// 다른 atom도 Subscriber가 될 수 있음
const doubleCountAtom = atom((get) => get(countAtom) * 2) // countAtom을 구독

WeakMap 기반 상태 관리

Jotai는 WeakMap을 사용해 atom을 키로 하는 상태 저장소를 구현합니다. Jotai internals.ts store

// 실제 Jotai buildStore 함수 구조
function buildStore() {
  const store = {
    get(atom) {
      /* atom 값 읽기 */
    },
    set(atom, ...args) {
      /* atom 값 쓰기 */
    },
    sub(atom, listener) {
      /* atom 구독 */
    },
  }

  const buildingBlocks = [
    new WeakMap(), // atomStateMap - atom별 상태 저장
    new WeakMap(), // mountedMap - 마운트된 atom 추적
    new WeakMap(), // invalidatedAtoms - 무효화된 atom들
    new Set(), // changedAtoms - 변경된 atom 목록
    new Set(), // mountCallbacks - 마운트 콜백들
    new Set(), // unmountCallbacks - 언마운트 콜백들
    // ... 기타 building blocks
  ]

  return store
}

값 변경시 전파 과정

Jotai internals.ts setter

// setCount(5) 호출하면
const setter = (atom, ...args) => {
  // 1️⃣ 새 값 설정
  const oldVersion = atomState.n // 버전 1 (이전)
  atomState.v = args[0] // 값을 5로 변경
  atomState.n++ // 버전 2로 변경

  // 2️⃣ 버전이 변경됐으면
  if (oldVersion !== atomState.n) {
    changedAtoms.add(atom) // 변경된 atom을 Set에 추가
    invalidateDependents(atom) // 의존하는 atom들 무효화
  }
}