일반적으로 상태 관리 라이브러리는 React에 종속되어 있습니다.
하지만 Zustand는 다음과 같이
- React에 독립적인 Vanilla Store 방식
- React에 최적화된 React Store 방식
상태 관리를 위해 두 가지 다른 접근 방식을 제공합니다.
// 1. Vanilla Store: 순수 JavaScript 기반
import { createStore } from 'zustand/vanilla'
const vanillaStore = createStore((set) => ({/*...*/}))
// 2. React Store: React Hook 기반
import { create } from 'zustand'
const useStore = create((set) => ({/*...*/}))
이 두 방식은 어떤 차이가 있으며, 내부적으로는 어떻게 구현되어 있을까요? 코드를 자세히 살펴보며 이해해보겠습니다.
Vanilla Store는 Zustand의 핵심 구현체입니다. vanilla.ts를 보면, 프레임워크에 독립적인 상태 관리 메커니즘을 제공합니다.
const createStoreImpl: CreateStoreImpl = (createState) => {
let state: TState
// Set 자료구조를 사용한 구독자 관리
const listeners = new Set<Listener>()
const setState: StoreApi<TState>['setState'] = (partial, replace) => {
// 함수형 업데이트 지원
const nextState = typeof partial === 'function'
? partial(state)
: partial
// 실제 변경이 있을 때만 업데이트 수행
if (!Object.is(nextState, state)) {
const previousState = state
// 불변성 보장을 위한 상태 복사
state = replace
? (nextState as TState)
: Object.assign({}, state, nextState)
// 모든 구독자에게 변경 알림
listeners.forEach((listener) => listener(state, previousState))
}
}
// 현재 상태 반환
const getState: StoreApi<TState>['getState'] = () => state
// 구독 시스템
const subscribe: StoreApi<TState>['subscribe'] = (listener) => {
listeners.add(listener)
// 구독 해제 함수 반환
return () => listeners.delete(listener)
}
const api = { setState, getState, subscribe }
state = createState(setState, getState, api)
return api
}
이 구현의 핵심은 상태 변경과 구독 시스템의 조화에 있습니다.
setState
: 상태 변경을 처리하면서 불변성을 보장- 구독 시스템 : Set 자료구조를 통해 효율적인 리스너 관리를 수행합니다.
React Store는 앞서 살펴본 Vanilla Store의 코어 기능을 기반으로 React에 최적화된 형태로 확장할 목적으로 만들어졌습니다.
주요 핵심 사항은
- React의 Hook 시스템과의 통합
- 동시성 모드 대응
react.ts의 핵심 부분을 살펴보면 다음과 같습니다.
function useStore<TState, StateSlice>(
api: ReadonlyStoreApi<TState>,
selector: (state: TState) => StateSlice = identity as any,
) {
// useSyncExternalStore: React 18의 동시성 모드를 위한 핵심 Hook
const slice = React.useSyncExternalStore(
api.subscribe, // Vanilla Store의 구독 시스템 활용
() => selector(api.getState()), // 선택자 패턴으로 필요한 상태만 구독
() => selector(api.getInitialState()) // SSR을 위한 서버 상태 지원
)
// React DevTools에서의 디버깅 지원
React.useDebugValue(slice)
return slice
}
createImpl
함수를 통해 Vanilla Store를 React Store로 확장합니다.
const createImpl = <T>(createState: StateCreator<T, [], []>) => {
// 1. Vanilla Store 인스턴스 생성
const api = createStore(createState)
// 2. useStore Hook과 결합
const useBoundStore: any = (selector?: any) => useStore(api, selector)
// 3. Vanilla Store의 모든 기능을 Hook에 결합
// 이를 통해 하나의 인터페이스로 모든 기능 사용 가능
Object.assign(useBoundStore, api)
return useBoundStore
}
-
동시성 모드 안전성
useSyncExternalStore
를 통한 외부 상태의 안전한 구독- React의 렌더링 사이클과 동기화된 상태 업데이트
-
선택적 구독 시스템
- selector를 통해 컴포넌트가 필요로 하는 상태만 구독
- 불필요한 리렌더링 방지
-
통합된 개발 경험
- React DevTools 지원으로 상태 디버깅 용이
- SSR 환경 고려한 설계
두 스토어는 같은 기능을 제공하면서도, 각자의 환경에 맞는 다른 접근 방식을 취합니다.
// Vanilla Store - 명령형 API
const store = createStore((set) => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 }))
}))
// 명시적인 상태 접근과 변경
const count = store.getState().count
store.setState({ count: count + 1 })
// React Store - 선언적 API
const useStore = create((set) => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 }))
}))
// 컴포넌트 내부에서의 자연스러운 사용
function Counter() {
const { count, increment } = useStore()
return <button onClick={increment}>{count}</button>
}
Vanilla
: 명령형으로 상태와 메서드에 직접 접근React
: Hook을 통한 선언적 상태 관리와 자동 구독
상태 변화를 감지하고 반영하는 방식에서도 큰 차이를 보입니다:
// Vanilla Store의 구독 시스템
function VanillaExample() {
// 명시적인 구독 설정
const unsubscribe = store.subscribe((state) => {
console.log('New state:', state)
})
// 구독 해제를 직접 관리해야 함
return () => unsubscribe()
}
// React Store의 자동화된 구독 관리
function ReactExample() {
// Hook이 자동으로 구독 설정 및 해제
const state = useStore()
// 컴포넌트의 생명주기와 함께 관리됨
return <div>{state.value}</div>
}
이러한 차이는 각각의 장점을 가집니다:
- Vanilla Store: 세밀한 제어와 커스터마이징 가능
- React Store: React의 선언적 패러다임과 완벽한 통합