2 분 소요

개요

Zustand는 가볍고 간단한 전역 상태 관리 라이브러리입니다. Context API보다 설정이 적고, Redux보다 훨씬 간단합니다.


설치

npm install zustand


기본 사용법

스토어 생성

// src/stores/useCounterStore.ts
import { create } from "zustand";

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

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 }),
  setCount: (value) => set({ count: value }),
}));

export default useCounterStore;

컴포넌트에서 사용

import useCounterStore from "../stores/useCounterStore";

function Counter() {
  // 필요한 값만 선택적으로 구독 (불필요한 리렌더링 방지)
  const count = useCounterStore((state) => state.count);
  const { increment, decrement, reset } = useCounterStore();

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

Provider 설정이 필요 없습니다. 바로 사용하면 됩니다.


Todo 앱 실전 예시

// src/stores/useTodoStore.ts
import { create } from "zustand";

interface Todo {
  id: number;
  text: string;
  done: boolean;
}

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

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

  addTodo: (text) =>
    set((state) => ({
      todos: [...state.todos, { id: Date.now(), text, done: false }],
    })),

  toggleTodo: (id) =>
    set((state) => ({
      todos: state.todos.map((t) =>
        t.id === id ? { ...t, done: !t.done } : t
      ),
    })),

  deleteTodo: (id) =>
    set((state) => ({
      todos: state.todos.filter((t) => t.id !== id),
    })),
}));

export default useTodoStore;
// src/components/TodoForm.tsx
import { useState } from "react";
import useTodoStore from "../stores/useTodoStore";

function TodoForm() {
  const [text, setText] = useState("");
  const addTodo = useTodoStore((state) => state.addTodo);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (text.trim()) {
      addTodo(text.trim());
      setText("");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={text} onChange={(e) => setText(e.target.value)} placeholder="할 일 입력" />
      <button type="submit">추가</button>
    </form>
  );
}

export default TodoForm;
// src/components/TodoList.tsx
import useTodoStore from "../stores/useTodoStore";

function TodoList() {
  const todos = useTodoStore((state) => state.todos);
  const toggleTodo = useTodoStore((state) => state.toggleTodo);
  const deleteTodo = useTodoStore((state) => state.deleteTodo);

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <span
            style=
            onClick={() => toggleTodo(todo.id)}
          >
            {todo.text}
          </span>
          <button onClick={() => deleteTodo(todo.id)}>삭제</button>
        </li>
      ))}
    </ul>
  );
}

export default TodoList;


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

// ❌ 전체 구독: count만 필요한데 다른 state 변경 시도 리렌더링
const state = useCounterStore();

// ✅ 선택적 구독: count가 변경될 때만 리렌더링
const count = useCounterStore((state) => state.count);

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


로컬 스토리지 영속화 (persist)

import { create } from "zustand";
import { persist } from "zustand/middleware";

interface SettingsState {
  theme: "light" | "dark";
  language: "ko" | "en";
  setTheme: (theme: "light" | "dark") => void;
}

const useSettingsStore = create<SettingsState>()(
  persist(
    (set) => ({
      theme: "light",
      language: "ko",
      setTheme: (theme) => set({ theme }),
    }),
    {
      name: "settings-storage",   // localStorage key 이름
    }
  )
);


관련 링크