상태관리 라이브러리 중 하나인 zustand는 많은 프론트엔드 개발자들에게 사랑 받고 있습니다.
이번 스터디에서는 그 핵심을 더 깊게 이해하기 위해서,
zustand를 싹싹 훑어 보려고 합니다!
🎯 이번주 학습목표
" zustand의 내부 로직이 구현되어있는 vanila.ts 파일을 분석하기 "
이 파일은 상태를 생성하고 관리하는 zustand의 핵심 엔진부분입니다.
자주 사용하는 crate함수의 바탕이 되는 코드로,
파일에 담긴 함수와 인터페이스를 살펴보고 이해해 보도록 하겠습니다
🔨 vanila.ts의 전체 구조와 주요 설명
❓ 상태 생성 원리 - createStore
const createStore = ((createState) =>
createState ? createStoreImpl(createState) : createStoreImpl) as CreateStore
zustand는 createStore 함수를 통해서 상태를 만든다
createStore함수는 단순히 상태를 초기화 하는 함수는 아니다.
상태를 만들고, 변경하고, 구독하고, 초기화 하는 부분까지 한번에 처리하는 함수이다.
➡️ 포인트는 createState의 여부에 따라서 return하는 값이 달라진다는 것이다.
1. createState가 있는 경우 함수의 실행 결과를 반환
import { createStore } from 'zustand/vanilla'
const store = createStore((set) => ({
bears: 0,
increase: () => set((state) => ({ bears: state.bears + 1 })),
}))
- createState가 있기 때문에 createStoreImpl(createState)가 실행됩니다.
- 결과적으로 store는 로직을 가진 store객체가 됩니다.
2. createState가 없는 경우 함수 자체를 반환
import { createStore } from 'zustand/vanilla'
const createBearStore = createStore()
const store = createBearStore((set) => ({
bears: 0,
}))
- 첫 줄에서 createState는 없고, createStoreImpl 함수 자체를 반환합니다.
- 나중에 createStore() 에서 createState를 넣어서 store를 생성합니다.
✅ 함수 자체를 반환하는게 포인트인 이유
1. 필요할때만 store를 생성할 수 있습니다.
2. 특정 로직을 여러번 재 사용할때 커스터마이즈된 factory를 만들수 있습니다.
3. 의존성 주입이 필요한 상황에서는 더 유연한 처리가 가능합니다.
// store 정의는 여기에 두고
export const createUserStore = () => createStore((set) => ({ ... }))
// 나중에 외부 조건에 따라 store 생성
const store = isLoggedIn ? createUserStore() : fallbackStore
📌 실제 사용 예시
import { createStore } from 'zustand/vanilla'
const store = createStore((set, get) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
➡️ (set, get) => ({ ... }) 이 createStore이다.
👉🏻 createStoreImpl함수 분석
const createStoreImpl: CreateStoreImpl = (createState) => {
type TState = ReturnType<typeof createState>
type Listener = (state: TState, prevState: TState) => void
let state: TState
const listeners: Set<Listener> = new Set()
// 상태 변경 함수
const setState: StoreApi<TState>['setState'] = (partial, replace) => {}
// 현재 상태 조회 함수
const getState: StoreApi<TState>['getState'] = () => state
// 초기 상태 기억용 초기 상태 조회 함수
const getInitialState: StoreApi<TState>['getInitialState'] = () => initialState
// 상태 변화 감지 함수
const subscribe: StoreApi<TState>['subscribe'] = (listener) => {}
//최종 스토어 생성
const api = { setState, getState, getInitialState, subscribe }
const initialState = (state = createState(setState, getState, api))
return api as any
}
🔎 핵심 흐름 뜯어보기
type TState = ReturnType<typeof createState>
➡️ createState를 실행하면 사용자가 정의한 상태 객체의 타입을 TState로 저장하는 부분입니다.
type Listener = (state: TState, prevState: TState) => void
const listeners: Set<Listener> = new Set()
➡️ 상태가 바뀔 때 마다 실행할 구독 함수를 담을 Set
🟡 setState(상태 변경 함수)
const setState: StoreApi<TState>['setState'] = (partial, replace) => {
// partial이 함수이면, 현재 상태를 인자로 넣어 실행해 다음 상태를 계산한다.
// partial이 객체라면 그냥 그대로 사용한다.
const nextState = typeof partial === 'function'? partial(state): partial
// Object.is는 값이 완전히 같을 경우는 true이다.
// 불필요한 업데이트 방지를 위해서 새 상태와 이전 상태가 다른 경우에만 아래 로직을 사용한다.
if (!Object.is(nextState, state)) {
// 상태를 변경하기 전에 기존 상태를 보관한다.
const previousState = state
// replace가 true면 전체 상태를 새 상태로 완전 교체
// replace가 undefined이고, nextState가 객체가 아니라면 교체
// replace가 undefined이고, nextState가 객체 라면 기존 상태과 nextState를 병합
state =
(replace ?? (typeof nextState !== 'object' || nextState === null))
? (nextState as TState)
: Object.assign({}, state, nextState)
// 상태가 바뀌었기 때문에 등록된 모든 listener에게 새 상태와 이전 상태를 전달합니다.
listeners.forEach((listener) => listener(state, previousState))
}
}
partail의 형태
set({ count: 1 }) // 👉 partial은 객체
set((prev) => ({ count: prev.count + 1 })) // 👉 partial은 함수
🟢 getState (현재 상태 조회 함수)
const getState: StoreApi<TState>['getState'] = () => state
- 현재 상태를 단순하게 리턴합니다.
🔵 getInitialState (초기 상태 기억용 함수)
const getInitialState: StoreApi<TState>['getInitialState'] = () => initialState
- createState가 처음 실행되었을 때의 상태를 기억해서 리턴합니다.
📌 예시
const createState = (set, get) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
})
// 실행 시
const initialState = (state = createState(set, get, api))
// 이 순간:
- createState(...) 실행 결과는 { count: 0, increment: ... }
- 이걸 state와 initialState에 모두 저장함
// 호출 시
const getInitialState = () => initialState
console.log(getInitialState()) // 👉 { count: 0, increment: ... }
- createStoreImpl 함수에 하단에 보면 초기 상태를 정의하고 저장하는 부분이 존재합니다.
🟣 subscribe ( 상태 변화 감지 함수 )
const subscribe: StoreApi<TState>['subscribe'] = (listener) => {
listeners.add(listener)
// Unsubscribe
return () => listeners.delete(listener)
}
- 리스너를 등록하고, 구독 해제 함수를 반환합니다.
🧠 상태 변화 감지 함수의 흐름
1. 상태를 구독한다
const unsubscribe = store.subscribe((newState, prevState) => {
console.log('상태 바뀜!', newState, prevState)
})
- listener는 listener Set에 등록되고, 실행되지는 않고 그냥 대기 중인 상태 입니다.
2. 상태를 변경한다.
store.setState({ count: 1 }) // 또는 set((prev) => {...})
- 내부에서 상태가 바뀌면 `listeners.forEach((listener) => listener(state, previousState))` 가 실행됩니다.
- 이 타이밍에 subscribe()에 등록한 리스너 함수가 실행됩니다.
- 상태가 실제로 바뀔때만 실행됩니다.
🟠 최종적으로 만들어지는 store API객체
export interface StoreApi<T> {
setState: (
partial: T | Partial<T> | ((state: T) => T | Partial<T>),
replace?: boolean
) => void
getState: () => T
getInitialState: () => T
subscribe: (listener: (state: T, prevState: T) => void) => () => void
}
const api = { setState, getState, getInitialState, subscribe }
return api as any
- zustand 내부에서 사용하는 store의 객체의 형태입니다.
const store = createStore((set) => ({
bears: 0,
increase: () => set((state) => ({ bears: state.bears + 1 })),
}))
👉🏻 store는 내부적으로 아래와 같은 메서드를 가진 객체입니다.
store = {
setState: function,
getState: function,
getInitialState: function,
subscribe: function
}
💡 create 함수와 createStore함수
vanila.ts에 있는 createStore는 React없이 Vanila JS에서 사용된다.
실제로 우리가 docs에서 보고 있는 create함수를 사용한 store생성은
createStore를 호출하고, useStore라는 커스텀 훅으로 감싸서 반환한것으로
vanila 로직을 기반으로 React에 맞게 래핑한 것이다.
👉🏻 정리하기
이번주는 zustand의 핵심 파일인 vanila.ts를 중심으로 내부 구조와 동작 방식을 살펴봤습니다.
zustand는 단순하게 create() 함수 하나로 상태를 다루는 것 처럼 보였습니다.
하지만 내부는 달랐습니다.
- createStore로 상태를 만들고
- setState, getState, subscribe같은 API로 상태를 변경하고, 구독하는
구조를 가지고 있었습니다.
특히
1. 불필요한 연산을 최소화 하기 위한 전략이있다는 점
2. 옵션을 통해 상태를 전체 교체할지, 병합으로 할지 선택할 수 있다는 점
3. 리액트의 useState 처럼 현재 상태를 기반으로 다음 상태를 계산하는 방식을 지원한다는 점
등이 zustand가 많이 사용되고 있는 이유라는 생각이 들었습니다.
다음주는 zustand의 미들웨어에 대해 다루어 보겠습니다.
'FE' 카테고리의 다른 글
git push 하기전에 자동으로 build 확인하기 (0) | 2025.07.05 |
---|---|
[zustand]WEEK2 미들웨어 redux.ts와 subscribewithSelector.ts (0) | 2025.07.02 |
[CSS] next + tailwind로 프로그레스 바 만들기 (0) | 2023.12.29 |
CSS & tailwind -정사각형 이미지 만들기 (0) | 2023.12.29 |
CSS - Reset vs Normalize CSS: 웹 스타일링 초기화 방법 비교 (0) | 2023.08.30 |