Zustand 상태관리 완벽 가이드 — Redux를 대체하는 가벼운 해법

ZustandReact상태관리Redux대안TypeScript프론트엔드

Redux의 보일러플레이트에 지쳤다면 Zustand가 해답입니다. 2KB의 작은 크기로 강력한 상태 관리를 제공하며, 2026년 현재 가장 빠르게 성장하는 상태 관리 라이브러리입니다.


Zustand vs Redux vs Context

항목ZustandRedux ToolkitContext API
번들 크기2KB40KB+0KB (내장)
보일러플레이트최소중간낮음
러닝 커브매우 낮음높음낮음
미들웨어지원지원없음
DevTools지원지원제한적
리렌더링 최적화자동수동없음
TypeScript우수우수좋음

설치 및 기본 사용

설치

npm install zustand

첫 스토어 만들기

// stores/counter.ts
import { create } from 'zustand'

interface CounterState {
  count: number
  increment: () => void
  decrement: () => void
  reset: () => void
}

export const useCounterStore = create<CounterState>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}))

컴포넌트에서 사용

// components/Counter.tsx
import { useCounterStore } from '@/stores/counter'

export function Counter() {
  const { count, increment, decrement, reset } = useCounterStore()

  return (
    <div>
      <p>카운트: {count}</p>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      <button onClick={reset}>초기화</button>
    </div>
  )
}

선택적 구독 (성능 최적화)

// 전체 상태 구독 (비추천)
const { count, increment } = useCounterStore()

// 필요한 것만 구독 (추천)
const count = useCounterStore((state) => state.count)
const increment = useCounterStore((state) => state.increment)

// 여러 값 선택
const { count, total } = useCounterStore((state) => ({
  count: state.count,
  total: state.total,
}))

실전 패턴

1. 비동기 액션

// stores/users.ts
import { create } from 'zustand'

interface User {
  id: number
  name: string
  email: string
}

interface UsersState {
  users: User[]
  loading: boolean
  error: string | null
  fetchUsers: () => Promise<void>
  addUser: (user: Omit<User, 'id'>) => Promise<void>
}

export const useUsersStore = create<UsersState>((set, get) => ({
  users: [],
  loading: false,
  error: null,

  fetchUsers: async () => {
    set({ loading: true, error: null })
    try {
      const res = await fetch('/api/users')
      const users = await res.json()
      set({ users, loading: false })
    } catch (error) {
      set({ error: '사용자를 불러오는데 실패했습니다', loading: false })
    }
  },

  addUser: async (userData) => {
    set({ loading: true })
    try {
      const res = await fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(userData),
      })
      const newUser = await res.json()
      set((state) => ({
        users: [...state.users, newUser],
        loading: false,
      }))
    } catch (error) {
      set({ error: '사용자 추가에 실패했습니다', loading: false })
    }
  },
}))

2. 슬라이스 패턴 (대규모 앱)

// stores/slices/authSlice.ts
import { StateCreator } from 'zustand'

export interface AuthSlice {
  user: User | null
  isAuthenticated: boolean
  login: (credentials: Credentials) => Promise<void>
  logout: () => void
}

export const createAuthSlice: StateCreator<AuthSlice> = (set) => ({
  user: null,
  isAuthenticated: false,
  login: async (credentials) => {
    const user = await authApi.login(credentials)
    set({ user, isAuthenticated: true })
  },
  logout: () => set({ user: null, isAuthenticated: false }),
})

// stores/slices/cartSlice.ts
export interface CartSlice {
  items: CartItem[]
  addItem: (item: Product) => void
  removeItem: (id: string) => void
  clearCart: () => void
}

export const createCartSlice: StateCreator<CartSlice> = (set) => ({
  items: [],
  addItem: (item) =>
    set((state) => ({
      items: [...state.items, { ...item, quantity: 1 }],
    })),
  removeItem: (id) =>
    set((state) => ({
      items: state.items.filter((item) => item.id !== id),
    })),
  clearCart: () => set({ items: [] }),
})

// stores/index.ts - 슬라이스 조합
import { create } from 'zustand'
import { AuthSlice, createAuthSlice } from './slices/authSlice'
import { CartSlice, createCartSlice } from './slices/cartSlice'

type StoreState = AuthSlice & CartSlice

export const useStore = create<StoreState>()((...a) => ({
  ...createAuthSlice(...a),
  ...createCartSlice(...a),
}))

3. 계산된 값 (Computed/Derived State)

interface CartState {
  items: CartItem[]
  // getter로 계산된 값
  get totalItems(): number
  get totalPrice(): number
}

export const useCartStore = create<CartState>((set, get) => ({
  items: [],

  // 메서드로 구현
  get totalItems() {
    return get().items.reduce((sum, item) => sum + item.quantity, 0)
  },

  get totalPrice() {
    return get().items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    )
  },
}))

// 또는 selector 패턴
const totalItems = useCartStore((state) =>
  state.items.reduce((sum, item) => sum + item.quantity, 0)
)

미들웨어

devtools (Redux DevTools 연동)

import { create } from 'zustand'
import { devtools } from 'zustand/middleware'

export const useStore = create<State>()(
  devtools(
    (set) => ({
      // 상태 및 액션
    }),
    { name: 'MyStore' } // DevTools에 표시될 이름
  )
)

persist (로컬 스토리지 저장)

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface SettingsState {
  theme: 'light' | 'dark'
  language: string
  setTheme: (theme: 'light' | 'dark') => void
}

export const useSettingsStore = create<SettingsState>()(
  persist(
    (set) => ({
      theme: 'light',
      language: 'ko',
      setTheme: (theme) => set({ theme }),
    }),
    {
      name: 'settings-storage', // localStorage 키
      partialize: (state) => ({ theme: state.theme }), // 일부만 저장
    }
  )
)

immer (불변성 쉽게)

import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'

interface TodoState {
  todos: Todo[]
  addTodo: (text: string) => void
  toggleTodo: (id: string) => void
}

export const useTodoStore = create<TodoState>()(
  immer((set) => ({
    todos: [],

    addTodo: (text) =>
      set((state) => {
        // immer로 직접 수정 가능
        state.todos.push({
          id: crypto.randomUUID(),
          text,
          completed: false,
        })
      }),

    toggleTodo: (id) =>
      set((state) => {
        const todo = state.todos.find((t) => t.id === id)
        if (todo) {
          todo.completed = !todo.completed
        }
      }),
  }))
)

미들웨어 조합

import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'

export const useStore = create<State>()(
  devtools(
    persist(
      immer((set) => ({
        // 상태 및 액션
      })),
      { name: 'app-storage' }
    ),
    { name: 'AppStore' }
  )
)

React 외부에서 사용

// 스토어 외부에서 직접 접근
const state = useCounterStore.getState()
console.log(state.count)

// 직접 업데이트
useCounterStore.setState({ count: 10 })

// 구독
const unsubscribe = useCounterStore.subscribe(
  (state) => console.log('상태 변경:', state.count)
)

// 구독 해제
unsubscribe()

서버 사이드에서 사용

// 서버 액션에서
import { useCartStore } from '@/stores/cart'

export async function addToCart(productId: string) {
  const { addItem } = useCartStore.getState()
  // API 호출 후
  addItem(product)
}

TypeScript 패턴

타입 안전한 스토어

import { create } from 'zustand'
import { StateCreator } from 'zustand'

// 상태 타입
interface BearState {
  bears: number
  increase: (by: number) => void
}

// StateCreator 사용
const createBearSlice: StateCreator<BearState> = (set) => ({
  bears: 0,
  increase: (by) => set((state) => ({ bears: state.bears + by })),
})

// 스토어 생성
export const useBearStore = create<BearState>()(createBearSlice)

제네릭 스토어 팩토리

// 재사용 가능한 스토어 팩토리
function createEntityStore<T extends { id: string }>() {
  return create<{
    entities: T[]
    add: (entity: T) => void
    remove: (id: string) => void
    update: (id: string, updates: Partial<T>) => void
  }>((set) => ({
    entities: [],
    add: (entity) =>
      set((state) => ({ entities: [...state.entities, entity] })),
    remove: (id) =>
      set((state) => ({
        entities: state.entities.filter((e) => e.id !== id),
      })),
    update: (id, updates) =>
      set((state) => ({
        entities: state.entities.map((e) =>
          e.id === id ? { ...e, ...updates } : e
        ),
      })),
  }))
}

// 사용
const useProductStore = createEntityStore<Product>()
const useUserStore = createEntityStore<User>()

테스트

// __tests__/counter.test.ts
import { renderHook, act } from '@testing-library/react'
import { useCounterStore } from '@/stores/counter'

describe('Counter Store', () => {
  // 각 테스트 전 상태 초기화
  beforeEach(() => {
    useCounterStore.setState({ count: 0 })
  })

  it('증가 테스트', () => {
    const { result } = renderHook(() => useCounterStore())

    act(() => {
      result.current.increment()
    })

    expect(result.current.count).toBe(1)
  })

  it('감소 테스트', () => {
    useCounterStore.setState({ count: 5 })
    const { result } = renderHook(() => useCounterStore())

    act(() => {
      result.current.decrement()
    })

    expect(result.current.count).toBe(4)
  })
})

마이그레이션 가이드 (Redux → Zustand)

Before (Redux)

// actions
const INCREMENT = 'INCREMENT'
const increment = () => ({ type: INCREMENT })

// reducer
const counterReducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case INCREMENT:
      return { ...state, count: state.count + 1 }
    default:
      return state
  }
}

// store
const store = createStore(counterReducer)

// component
const count = useSelector((state) => state.count)
const dispatch = useDispatch()
dispatch(increment())

After (Zustand)

// 전부 한 파일에
const useCounterStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}))

// component
const count = useCounterStore((state) => state.count)
const increment = useCounterStore((state) => state.increment)
increment()

마치며

Zustand 선택 이유:

  1. 심플함: 보일러플레이트 거의 없음
  2. 크기: 2KB로 초경량
  3. 유연함: React 외부에서도 사용 가능
  4. 타입스크립트: 완벽한 타입 추론
  5. 성능: 자동 리렌더링 최적화

Redux가 필요한 경우: 매우 복잡한 상태 로직, 타임트래블 디버깅 필수, 대규모 팀

Zustand가 적합한 경우: 대부분의 React 프로젝트, 빠른 개발, 간단한 상태 관리

2026년, Zustand는 이미 "작은 프로젝트용"을 넘어 프로덕션 표준으로 자리 잡았습니다.

궁금한 점이 있으신가요?

협업·의뢰는 아래로, 가벼운 소통은 인스타그램 @bluefox._.hi도 환영이에요.