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

발행 읽는 시간 약 2 분

  • Zustand
  • React
  • 상태관리
  • Redux대안
  • TypeScript
  • 프론트엔드

글 소개

Zustand로 React 상태 관리를 간소화하는 방법을 다룹니다. 기본 사용법부터 미들웨어, 비동기 처리, TypeScript 패턴까지.

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


Zustand vs Redux vs Context#

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

설치 및 기본 사용#

설치#

Bash
npm install zustand

첫 스토어 만들기#

TypeScript
// 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 }),
}))

컴포넌트에서 사용#

TSX
// 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>
  )
}

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

TSX
// 전체 상태 구독 (비추천)
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. 비동기 액션#

TypeScript
// 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. 슬라이스 패턴 (대규모 앱)#

TypeScript
// 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)#

TypeScript
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 연동)#

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

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

persist (로컬 스토리지 저장)#

TypeScript
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 (불변성 쉽게)#

TypeScript
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
        }
      }),
  }))
)

미들웨어 조합#

TypeScript
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 외부에서 사용#

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

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

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

// 구독 해제
unsubscribe()

서버 사이드에서 사용#

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

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

TypeScript 패턴#

타입 안전한 스토어#

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)

제네릭 스토어 팩토리#

TypeScript
// 재사용 가능한 스토어 팩토리
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>()

테스트#

TypeScript
// __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)#

TypeScript
// 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)#

TypeScript
// 전부 한 파일에
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는 이미 "작은 프로젝트용"을 넘어 프로덕션 표준으로 자리 잡았습니다.

Follow · Ask · Work

이 글이 도움이 됐다면

짧은 작업 기록과 데모는 인스타그램에 올립니다. 만들고 싶은 것이 있다면 프로젝트 의뢰나 협업 제안도 편하게 보내 주세요.

글쓴이

파란여우

직접 만든 AI 앱·제품의 개발·배포를 공개하는 실전형 테크 크리에이터

파란여우 소개 Instagram @bluefox._.hi

시리즈 · 개발 노트

시리즈 전체 보기

이어서 보기

만들고 싶은 것이 있다면 프로젝트 의뢰로 알려 주세요.