Zustand 완벽 가이드: 초보자부터 고급 사용자까지

odada·2025년 1월 14일
0

next.js

목록 보기
12/12

목차

  1. Zustand란?
  2. 왜 Zustand인가?
  3. 기본 개념
  4. 단계별 예제
  5. 실전 팁과 트릭

Zustand란?

Zustand는 독일어로 'state'(상태)를 의미하며, React 애플리케이션을 위한 작고 빠르며 확장 가능한 상태 관리 라이브러리입니다.

🔗 공식 문서: https://docs.pmnd.rs/zustand/getting-started/introduction

왜 Zustand인가?

Redux와 비교했을 때의 장점

  • 보일러플레이트 코드가 거의 없음
  • 설정이 매우 간단함
  • 작은 번들 사이즈 (Redux: ~22.4kb vs Zustand: ~1.6kb)
  • 미들웨어 지원이 더 간단함

Recoil과 비교했을 때의 장점

  • 더 간단한 API
  • 외부 Provider 컴포넌트 불필요
  • 더 성숙한 생태계
  • 더 작은 학습 곡선

Zustand가 적합한 프로젝트 특징

  1. 중소규모 프로젝트
  2. 빠른 개발이 필요한 프로젝트
  3. 팀 러닝커브를 최소화해야 하는 경우
  4. 번들 사이즈에 민감한 프로젝트
  5. SSR/SSG를 사용하는 프로젝트

기본 개념

Store

// store/counterStore.js
import create from 'zustand'

// 기본 스토어 생성
const useStore = create((set) => ({
  // 초기 상태
  count: 0,
  
  // 액션
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}))

주요 API 설명

  • set: 상태를 업데이트하는 함수
  • get: 현재 상태를 가져오는 함수
  • subscribe: 상태 변화를 구독하는 함수

단계별 예제

초급: 카운터 만들기

// stores/counterStore.js
import create from 'zustand'

const useCounterStore = create((set) => ({
  // 상태
  count: 0,
  
  // 액션들
  increment: () => set((state) => ({ 
    count: state.count + 1 
  })),
  decrement: () => set((state) => ({ 
    count: state.count - 1 
  })),
  reset: () => set({ count: 0 }), // 상태 초기화
}))

// Counter.js
function Counter() {
  // 필요한 상태와 액션만 선택적으로 가져오기
  const { count, increment, decrement, reset } = useCounterStore()
  
  return (
    <div>
      <h1>카운트: {count}</h1>
      <button onClick={decrement}>감소</button>
      <button onClick={increment}>증가</button>
      <button onClick={reset}>초기화</button>
    </div>
  )
}

중급: Todo 리스트

// stores/todoStore.js
import create from 'zustand'

const useTodoStore = create((set, get) => ({
  // 상태
  todos: [],
  isLoading: false,
  error: null,
  
  // 액션: 할 일 추가
  addTodo: (todo) => set((state) => ({
    todos: [...state.todos, {
      id: Date.now(),
      text: todo,
      completed: false
    }]
  })),
  
  // 액션: 할 일 삭제
  removeTodo: (id) => set((state) => ({
    todos: state.todos.filter(todo => todo.id !== id)
  })),
  
  // 액션: 할 일 토글
  toggleTodo: (id) => set((state) => ({
    todos: state.todos.map(todo =>
      todo.id === id 
        ? { ...todo, completed: !todo.completed }
        : todo
    )
  })),
  
  // 비동기 액션: 할 일 목록 가져오기
  fetchTodos: async () => {
    set({ isLoading: true })
    try {
      const response = await fetch('https://api.example.com/todos')
      const todos = await response.json()
      set({ todos, isLoading: false })
    } catch (error) {
      set({ error: error.message, isLoading: false })
    }
  },
  
  // Computed 값: 완료된 할 일 개수
  get completedCount() {
    return get().todos.filter(todo => todo.completed).length
  }
}))

고급: 미들웨어와 영구 저장소

// stores/advancedStore.js
import create from 'zustand'
import { persist, devtools } from 'zustand/middleware'

// 커스텀 미들웨어 예시
const log = (config) => (set, get, api) =>
  config(
    (...args) => {
      console.log('이전 상태:', get())
      set(...args)
      console.log('다음 상태:', get())
    },
    get,
    api
  )

const useStore = create(
  devtools(  // Redux DevTools 연동
    persist(  // localStorage 연동
      log(  // 커스텀 로깅 미들웨어
        (set, get) => ({
          // 상태
          user: null,
          theme: 'light',
          settings: {},
          
          // 액션: 사용자 로그인
          login: async (credentials) => {
            const user = await authService.login(credentials)
            set({ user })
          },
          
          // 액션: 테마 토글
          toggleTheme: () => set((state) => ({
            theme: state.theme === 'light' ? 'dark' : 'light'
          })),
          
          // 액션: 설정 업데이트
          updateSettings: (newSettings) => set((state) => ({
            settings: { ...state.settings, ...newSettings }
          })),
        })
      ),
      {
        name: 'app-storage', // localStorage 키 이름
        whitelist: ['theme', 'settings'] // 저장할 상태 선택
      }
    )
  )
)

실전 팁과 트릭

1. 상태 선택적 구독하기

// 필요한 상태만 구독하여 불필요한 리렌더링 방지
const todos = useTodoStore(state => state.todos)
const completedCount = useTodoStore(state => 
  state.todos.filter(todo => todo.completed).length
)

2. 상태 초기화

// 전체 상태 초기화
const resetStore = useStore.setState(initialState)

// 특정 상태만 초기화
const resetCount = useStore.setState({ count: 0 })

3. 비동기 상태 관리

const useAsyncStore = create((set) => ({
  data: null,
  isLoading: false,
  error: null,
  
  fetchData: async () => {
    set({ isLoading: true })
    try {
      const response = await fetch('...')
      const data = await response.json()
      set({ data, isLoading: false })
    } catch (error) {
      set({ error, isLoading: false })
    }
  }
}))

4. TypeScript 지원

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

const useTodoStore = create<TodoState>((set) => ({
  todos: [],
  addTodo: (text) => set((state) => ({
    todos: [...state.todos, { id: Date.now(), text, completed: false }]
  })),
  removeTodo: (id) => set((state) => ({
    todos: state.todos.filter(todo => todo.id !== id)
  }))
}))

마무리

Zustand는 간단하면서도 강력한 상태 관리 라이브러리입니다. 특히:

  • 간단한 API로 빠른 학습이 가능
  • 필요한 기능만 선택적으로 사용 가능
  • TypeScript 지원이 우수
  • 미들웨어를 통한 확장성이 뛰어남

이러한 특징들 때문에 많은 개발자들이 Redux나 Recoil 대신 Zustand를 선택하고 있습니다.

특히 작은 규모의 프로젝트부터 중간 규모의 프로젝트까지 Zustand는 매우 효과적인 선택이 될 수 있습니다. 복잡한 상태 관리가 필요하지 않은 프로젝트에서는 과도한 보일러플레이트 없이 깔끔한 코드를 유지할 수 있습니다.

0개의 댓글