[React] Zustand
개요
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 이름
}
)
);