[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
}
값 변경시 전파 과정
// 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들 무효화
}
}