지난 Zustand 핵심 로직 정복 1편에서 다뤘던 'createStore부터 React 연동까지'에 이어, 2편을 가지고 왔습니다. 2편을 읽는데에 1편의 로직을 읽는 것이 도움이 되니, 혹시 2편을 읽다가 'createStore가 그래서 어떻게 돌아가는건데?'라는 의문점을 가지게 되신 분들은 1편 글을 참고하시면 좋을 것 같습니다 :)
2025.06.28 - [개발] - Zustand 핵심 로직 완전 정복 (1편): createStore부터 React 연동까지
Zustand 핵심 로직 완전 정복 (1편): createStore부터 React 연동까지
Zustand는 React 생태계에서 가볍고 직관적인 상태 관리 라이브러리로 많이 사용되고 있습니다. 이 글에서는 Zustand의 핵심 로직을 직접 분석하며 학습한 내용을 정리하였습니다. 특히 createStore 함수
soyoondaily.com
(개인적으로는 직접 분석해보는 과정도 추천드립니다. 굉장히 뿌듯하거든요👍)
Zustand의 큰 강점 중 하나는 middleware를 통한 store 확장성입니다. Redux DevTools 연동, 로깅, 퍼시스턴스(persist) 등 여러 기능을 간단히 추가할 수 있는데요. 이번 글에서는 대표적인 미들웨어인 redux와 subscribeWithSelector를 중심으로 내부 구현을 뜯어보겠습니다.
✅ 학습 목표
- Zustand의 redux middleware가 어떤 구조로 동작하는지 이해합니다.
- subscribeWithSelector가 selector 기반 구독을 어떻게 가능하게 하는지 파악합니다.
⚒️ 분석 방법
- 실제 사용 예제 코드 → Zustand 문서나 예시 코드를 직접 실행
- middleware 내부 로직 → reduxImpl, subscribeWithSelectorImpl 등의 구현체를 확인
- 타입 제거 버전으로 단순화 → 핵심 로직을 읽기 쉽게 정리
🤔 redux middleware
Zustand에서 redux 미들웨어를 적용하면, store에 dispatch와 reducer 기반 상태 업데이트 로직을 추가할 수 있습니다. 이는 기존 Redux를 쓰던 개발자에게 친숙한 패턴을 제공합니다.
실제 사용 코드
type PersonStoreState = {
firstName: string
lastName: string
email: string
}
type PersonStoreAction =
| { type: 'person/setFirstName'; firstName: string }
| { type: 'person/setLastName'; lastName: string }
| { type: 'person/setEmail'; email: string }
type PersonStore = PersonStoreState & PersonStoreActions // --> PersonStoreActions s?
const personStoreReducer = (
state: PersonStoreState,
action: PersonStoreAction,
) => {
switch (action.type) {
case 'person/setFirstName': {
return { ...state, firstName: action.firstName }
}
case 'person/setLastName': {
return { ...state, lastName: action.lastName }
}
case 'person/setEmail': {
return { ...state, email: action.email }
}
default: {
return state
}
}
}
const personStoreInitialState: PersonStoreState = {
firstName: 'Barbara',
lastName: 'Hepworth',
email: 'bhepworth@sculpture.com',
}
const personStore = createStore<PersonStore>()(
redux(personStoreReducer, personStoreInitialState),
)
→ redux 미들웨어를 적용하면 store는 dispatch(action)을 통해 reducer 기반 상태 업데이트가 가능해집니다.
reduxImpl 로직
reduxImpl 로직을 살펴보니 굉장히 복잡하고 파악하기가 어려웠습니다. 아래는 reduxImpl의 실제 코드를 가져왔지만, 이를 타입을 제거한 버전으로 정리한 후 가독성을 높여 이해하고자 했습니다.
const reduxImpl: ReduxImpl = (reducer, initial) => (set, _get, api) => {
type S = typeof initial
type A = Parameters<typeof reducer>[1]
// 타입 단언으로 시작되는 경우, 앞에 ; 추가
;(api as any).dispatch = (action: A) => {
;(set as NamedSet<S>)((state: S) => reducer(state, action), false, action)
return action
}
;(api as any).dispatchFromDevtools = true
return { dispatch: (...args) => (api as any).dispatch(...args), ...initial }
}
export const redux = reduxImpl as unknown as Redux
reduxImpl 로직 (타입 제거 ver)
// (set, _get, api) === createStore의 return 값
const reduxImpl = (reducer, initial) => (set, _get, api) => {
// api.dispatch === action
// reducer(state, action)
api.dispatch = (action) => {
// partial, replace
set((state) => reducer(state, action), false, action)
return action
}
api.dispatchFromDevtools = true
// dispatch: api.dispatch(...args) => args - action
// inital === initialState
return { dispatch: (...args) => api.dispatch(...args), ...initial }
}
- set을 통해 reducer 기반 상태 업데이트가 발생합니다.
- dispatchFromDevtools를 true로 설정하여 Redux DevTools와 연동할 수 있습니다.
- 최종적으로 반환된 store는 dispatch와 초기 상태를 함께 제공합니다.
🤔 subscribeWithSelector middleware
다음은 subscribeWithSelector 미들웨어입니다. 이 기능을 사용하면 특정 state의 부분만 선택적으로 구독할 수 있습니다.
이전에 분석했던 create와 마찬가지로, ~Impl과 연결되어 있습니다.
export const subscribeWithSelector =
subscribeWithSelectorImpl as unknown as SubscribeWithSelector
실제 사용 코드
const positionStore = createStore<PositionStore>()(
subscribeWithSelector((set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
})),
)
// state === { position: {x: 0, y: 0}, user: {name: "Kim"} }
positionStore.subscribe((state) => state.position, render)
positionStore.subscribe((state) => state.position.x, logger)
→ subscribeWithSelector 덕분에 state.position.x 같은 특정 값만 추적할 수 있고, 불필요한 리렌더링을 방지할 수 있습니다.
subscribeWithSelectorImpl 로직
// zustand/src/middleware/subscribeWithSelector.ts
const subscribeWithSelectorImpl: SubscribeWithSelectorImpl =
// fn === (set) => ({})
// (set, get, api) === createStore에서 반환되는 값
(fn) => (set, get, api) => {
type S = ReturnType<typeof fn>
type Listener = (state: S, previousState: S) => void
// 기존 api.subscribe 저장. 변경된 값이 있었을 경우 계속 초기에 업데이트 해줌
const origSubscribe = api.subscribe as (listener: Listener) => () => void
// api.subscribe 업데이트
// selector : state 선택, optListener : 실행할 로직
api.subscribe = ((selector: any, optListener: any, options: any) => {
// listener === selector === (state) => state.position | subscribe 사용 시 첫번째로 넘겨주는 인자
let listener: Listener = selector // if no selector
if (optListener) {
// option으로 들어온 값. default는 Object.js
const equalityFn = options?.equalityFn || Object.is
// selector(api.getState()) - 전체를 가져옴
// selector(api.getState()) === (state) => state.position === state.position
let currentSlice = selector(api.getState())
listener = (state) => {
// selector === (state) => state.position
// selector(state) === state.position
const nextSlice = selector(state)
if (!equalityFn(currentSlice, nextSlice)) {
// 값 업데이트
const previousSlice = currentSlice
// subscribe 두번째 인자로 넘어오는 함수 실행
optListener((currentSlice = nextSlice), previousSlice)
}
}
if (options?.fireImmediately) {
optListener(currentSlice, currentSlice)
}
}
// 기존 api.subscribe에 변경된 listener 전달
return origSubscribe(listener)
}) as any
const initialState = fn(set, get, api)
return initialState
}
코드를 읽으면서 분석해나가는 과정은 위 코드 내 주석으로 남겨두었습니다.
- 기존 api.subscribe를 오버라이드하여 selector 기반 구독이 가능하도록 확장합니다.
- equalityFn(기본값 Object.is)을 사용해 이전 slice와 새로운 slice를 비교합니다.
- 값이 바뀌었을 때만 optListener가 실행되므로, 불필요한 업데이트를 방지할 수 있습니다.
- fireImmediately 옵션을 사용하면 구독 즉시 현재 값을 실행할 수 있습니다.
이번 분석을 통해 Zustand 미들웨어의 구조와 동작 방식을 구체적으로 이해할 수 있었습니다.
미들웨어는 복잡한 로직을 캡슐화하여 간단한 API 형태로 제공하기 때문에, 개발자는 손쉽게 store에 다양한 기능을 추가할 수 있습니다.
즉, createStore의 기본 API를 미들웨어를 통해 유연하게 확장함으로써, 상태 관리 로직을 더욱 강력하고 직관적으로 다룰 수 있게 되는 것입니다.
Zustand에서 늘 create, set, useShallow 등만 사용했던 분이라면, 한번쯤 이런 기능들도 참고해보시면 좋을 것 같습니다 :)
'개발' 카테고리의 다른 글
AI 기반 테스트 코드 작성 가이드 – 개발 생산성을 높이는 방법 (10) | 2025.08.26 |
---|---|
Zustand 핵심 로직 완전 정복 (1편): createStore부터 React 연동까지 (7) | 2025.06.28 |
[함수형 코딩] 반응형 아키텍처와 어니언 아키텍처 (+ 짧은 책 후기) (8) | 2025.06.02 |
[함수형 코딩] 비동기 타임 라인 컨트롤 (6) | 2025.05.21 |
2025 프론트엔드 3년차 경력직 면접 질문 정리 (+ 실전 팁) (13) | 2025.05.11 |