Zustand๋ ๋ฌด์์ธ๊ฐ
ํ๊ต ์ฌ๋ฌผํจ์ ์๊ฐํด๋ณด์.
๊ฐ ํ์์ด ์๊ธฐ ์ฌ๋ฌผํจ์ ๋ฌผ๊ฑด์ ๋ฃ๊ณ ๋นผ๋ฏ์ด,
Zustand๋ ์ฑ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ณ ๊บผ๋ด๋ ์ฌ๋ฌผํจ์ด๋ค.
Redux๋ ๊ฑฐ๋ํ ์ฐฝ๊ณ ๊ฐ๋ค.
๋ฌผ๊ฑด ํ๋ ๋ฃ์ผ๋ ค๋ฉด ์๋ฅ ์์ฑํ๊ณ , ์น์ธ๋ฐ๊ณ , ๊ธฐ๋กํด์ผ ํ๋ค.
Zustand๋ ๊ฐ์ธ ์ฌ๋ฌผํจ ๊ฐ๋ค.
์ด์ ๋ง ์์ผ๋ฉด ๋ฐ๋ก ๋ฃ๊ณ ๋บ ์ ์๋ค.
๊ฐ๋จํ๊ณ , ๋น ๋ฅด๊ณ , ํธํ๋ค.
์ Zustand๋ฅผ ๋ฐฐ์์ผ ํ ๊น?
์ด์ 1: ๊ฐ๋จํจ
Redux ๋๋น 80% ์ ์ ์ฝ๋
์ด์ 2: ๋น ๋ฅธ ์ฑ๋ฅ
ํ์ํ ์ปดํฌ๋ํธ๋ง ๋ฆฌ๋ ๋๋ง
์ด์ 3: TypeScript ์นํ์
ํ์
์ถ๋ก ์ด ์๋์ผ๋ก ๋์
์ด์ 4: ๋ฒ๋ค ํฌ๊ธฐ
๋จ 1KB (gzipped)
๊ธฐ๋ณธ ๊ฐ๋ ์์ฝ
๐ท๏ธ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋น๊ต
| ๊ตฌ๋ถ | Redux | Zustand | Recoil | Jotai |
|---|---|---|---|---|
| ๋ณด์ผ๋ฌํ๋ ์ดํธ | ๋ง์ | ์ ์ | ์ค๊ฐ | ์ ์ |
| ๋ฒ๋ค ํฌ๊ธฐ | 7KB | 1KB | 20KB | 3KB |
| ํ์ต ๊ณก์ | ๋์ | ๋ฎ์ | ์ค๊ฐ | ๋ฎ์ |
| DevTools | ์ง์ | ์ง์ | ์ง์ | ์ง์ |
| ๋ฏธ๋ค์จ์ด | ํ์ | ์ ํ | ์์ | ์์ |
| React ์ธ๋ถ ์ฌ์ฉ | ๊ฐ๋ฅ | ๊ฐ๋ฅ | ๋ถ๊ฐ | ๋ถ๊ฐ |
๐ท๏ธ Zustand ํต์ฌ ๊ฐ๋
Store (์ ์ฅ์)
๊ฐ๋
: ์ํ์ ์ก์
์ ๋ด๋ ์ปจํ
์ด๋
// ์ฌ๋ฌผํจ = Store
// ์ฌ๋ฌผํจ ์์ ๋ฌผ๊ฑด = State
// ๋ฌผ๊ฑด ๋ฃ๊ธฐ/๋นผ๊ธฐ = Actions
State (์ํ)
๊ฐ๋
: ์ ์ฅ๋ ๋ฐ์ดํฐ
์์:
- ๋ก๊ทธ์ธํ ์ฌ์ฉ์ ์ ๋ณด
- ์ฅ๋ฐ๊ตฌ๋ ์ํ ๋ชฉ๋ก
- ๋คํฌ๋ชจ๋ ์ค์
Actions (์ก์ )
๊ฐ๋
: ์ํ๋ฅผ ๋ณ๊ฒฝํ๋ ํจ์
์์:
- ๋ก๊ทธ์ธ/๋ก๊ทธ์์
- ์ํ ์ถ๊ฐ/์ญ์
- ํ ๋ง ๋ณ๊ฒฝ
๐ท๏ธ Zustand vs Redux ์ฝ๋ ๋น๊ต
Redux ๋ฐฉ์ (๋ณต์กํจ)
// 1. Action Types
const INCREMENT = "INCREMENT";
const DECREMENT = "DECREMENT";
// 2. Action Creators
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });
// 3. Reducer
const counterReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case INCREMENT:
return { count: state.count + 1 };
case DECREMENT:
return { count: state.count - 1 };
default:
return state;
}
};
// 4. Store
const store = createStore(counterReducer);
// 5. Component (connect ๋๋ useSelector/useDispatch)
const Counter = () => {
const count = useSelector((state) => state.count);
const dispatch = useDispatch();
return (
<div>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
</div>
);
};
Zustand ๋ฐฉ์ (๊ฐ๋จํจ)
// 1. Store (์ํ + ์ก์
ํ ๋ฒ์)
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
// 2. Component
const Counter = () => {
const { count, increment } = useCounterStore();
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
};
์ค์ ์์
๐ท๏ธ ์ค์น ๋ฐ ๊ธฐ๋ณธ ์ค์
# ์ค์น
npm install zustand
# TypeScript ํ๋ก์ ํธ
npm install zustand
๐ท๏ธ ๊ธฐ๋ณธ Store ์์ฑ
// src/stores/counter-store.ts
import { create } from "zustand";
// ํ์
์ ์
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
setCount: (value: number) => void;
}
// Store ์์ฑ
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 }),
setCount: (value) => set({ count: value }),
}));
// src/components/Counter.tsx
import { useCounterStore } from "../stores/counter-store";
export const Counter = () => {
// ํ์ํ ์ํ์ ์ก์
๋ง ์ ํ
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
const decrement = useCounterStore((state) => state.decrement);
const reset = useCounterStore((state) => state.reset);
return (
<div>
<h1>์นด์ดํฐ: {count}</h1>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>์ด๊ธฐํ</button>
</div>
);
};
๐ท๏ธ ์ค์ : ์ฅ๋ฐ๊ตฌ๋ Store
// src/types/cart.ts
export interface Product {
id: string;
name: string;
price: number;
image: string;
}
export interface CartItem extends Product {
quantity: number;
}
// src/stores/cart-store.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { CartItem, Product } from "../types/cart";
interface CartState {
// ์ํ
items: CartItem[];
// ๊ณ์ฐ๋ ๊ฐ (Getter)
totalItems: () => number;
totalPrice: () => number;
// ์ก์
addItem: (product: Product) => void;
removeItem: (productId: string) => void;
updateQuantity: (productId: string, quantity: number) => void;
clearCart: () => void;
}
export const useCartStore = create<CartState>()(
persist(
(set, get) => ({
items: [],
// ์ด ์ํ ์
totalItems: () => {
return get().items.reduce((sum, item) => sum + item.quantity, 0);
},
// ์ด ๊ฐ๊ฒฉ
totalPrice: () => {
return get().items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
},
// ์ํ ์ถ๊ฐ
addItem: (product) => {
set((state) => {
const existingItem = state.items.find(
(item) => item.id === product.id
);
if (existingItem) {
// ์ด๋ฏธ ์์ผ๋ฉด ์๋ ์ฆ๊ฐ
return {
items: state.items.map((item) =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
),
};
}
// ์์ผ๋ฉด ์๋ก ์ถ๊ฐ
return {
items: [...state.items, { ...product, quantity: 1 }],
};
});
},
// ์ํ ์ ๊ฑฐ
removeItem: (productId) => {
set((state) => ({
items: state.items.filter((item) => item.id !== productId),
}));
},
// ์๋ ๋ณ๊ฒฝ
updateQuantity: (productId, quantity) => {
set((state) => ({
items: state.items.map((item) =>
item.id === productId ? { ...item, quantity } : item
),
}));
},
// ์ฅ๋ฐ๊ตฌ๋ ๋น์ฐ๊ธฐ
clearCart: () => set({ items: [] }),
}),
{
name: "cart-storage", // localStorage ํค
}
)
);
// src/components/Cart.tsx
import { useCartStore } from "../stores/cart-store";
export const Cart = () => {
const items = useCartStore((state) => state.items);
const totalItems = useCartStore((state) => state.totalItems);
const totalPrice = useCartStore((state) => state.totalPrice);
const removeItem = useCartStore((state) => state.removeItem);
const updateQuantity = useCartStore((state) => state.updateQuantity);
const clearCart = useCartStore((state) => state.clearCart);
return (
<div className="cart">
<h2>์ฅ๋ฐ๊ตฌ๋ ({totalItems()}๊ฐ)</h2>
{items.length === 0 ? (
<p>์ฅ๋ฐ๊ตฌ๋๊ฐ ๋น์ด์์ต๋๋ค.</p>
) : (
<>
<ul>
{items.map((item) => (
<li key={item.id}>
<img src={item.image} alt={item.name} />
<span>{item.name}</span>
<span>{item.price.toLocaleString()}์</span>
<input
type="number"
min="1"
value={item.quantity}
onChange={(e) =>
updateQuantity(item.id, parseInt(e.target.value))
}
/>
<button onClick={() => removeItem(item.id)}>์ญ์ </button>
</li>
))}
</ul>
<div className="cart-summary">
<p>์ด ๊ธ์ก: {totalPrice().toLocaleString()}์</p>
<button onClick={clearCart}>์ฅ๋ฐ๊ตฌ๋ ๋น์ฐ๊ธฐ</button>
<button>๊ฒฐ์ ํ๊ธฐ</button>
</div>
</>
)}
</div>
);
};
// src/components/ProductCard.tsx
import { useCartStore } from "../stores/cart-store";
import { Product } from "../types/cart";
interface ProductCardProps {
product: Product;
}
export const ProductCard = ({ product }: ProductCardProps) => {
const addItem = useCartStore((state) => state.addItem);
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.price.toLocaleString()}์</p>
<button onClick={() => addItem(product)}>์ฅ๋ฐ๊ตฌ๋ ๋ด๊ธฐ</button>
</div>
);
};
๐ท๏ธ ์ค์ : ์ธ์ฆ Store
// src/types/auth.ts
export interface User {
id: string;
email: string;
name: string;
avatar?: string;
}
export interface AuthState {
user: User | null;
token: string | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
}
// src/stores/auth-store.ts
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import { User } from "../types/auth";
interface AuthState {
// ์ํ
user: User | null;
token: string | null;
isLoading: boolean;
error: string | null;
// ๊ณ์ฐ๋ ๊ฐ
isAuthenticated: () => boolean;
// ์ก์
login: (email: string, password: string) => Promise<void>;
logout: () => void;
updateProfile: (data: Partial<User>) => void;
clearError: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set, get) => ({
user: null,
token: null,
isLoading: false,
error: null,
isAuthenticated: () => !!get().token,
login: async (email, password) => {
set({ isLoading: true, error: null });
try {
// API ํธ์ถ
const response = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error("๋ก๊ทธ์ธ์ ์คํจํ์ต๋๋ค.");
}
const data = await response.json();
set({
user: data.user,
token: data.token,
isLoading: false,
});
} catch (error) {
set({
error: error instanceof Error ? error.message : "์ค๋ฅ ๋ฐ์",
isLoading: false,
});
}
},
logout: () => {
set({
user: null,
token: null,
error: null,
});
},
updateProfile: (data) => {
set((state) => ({
user: state.user ? { ...state.user, ...data } : null,
}));
},
clearError: () => set({ error: null }),
}),
{
name: "auth-storage",
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
user: state.user,
token: state.token,
}),
}
)
);
// src/components/LoginForm.tsx
import { useState } from "react";
import { useAuthStore } from "../stores/auth-store";
export const LoginForm = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const login = useAuthStore((state) => state.login);
const isLoading = useAuthStore((state) => state.isLoading);
const error = useAuthStore((state) => state.error);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await login(email, password);
};
return (
<form onSubmit={handleSubmit}>
{error && <div className="error">{error}</div>}
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="์ด๋ฉ์ผ"
disabled={isLoading}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="๋น๋ฐ๋ฒํธ"
disabled={isLoading}
/>
<button type="submit" disabled={isLoading}>
{isLoading ? "๋ก๊ทธ์ธ ์ค..." : "๋ก๊ทธ์ธ"}
</button>
</form>
);
};
// src/components/Header.tsx
import { useAuthStore } from "../stores/auth-store";
export const Header = () => {
const user = useAuthStore((state) => state.user);
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const logout = useAuthStore((state) => state.logout);
return (
<header>
<nav>
{isAuthenticated() ? (
<>
<span>์๋
ํ์ธ์, {user?.name}๋</span>
<button onClick={logout}>๋ก๊ทธ์์</button>
</>
) : (
<a href="/login">๋ก๊ทธ์ธ</a>
)}
</nav>
</header>
);
};
๐ท๏ธ ๋ฏธ๋ค์จ์ด ํ์ฉ
DevTools ์ฐ๋
// src/stores/counter-store.ts
import { create } from "zustand";
import { devtools } from "zustand/middleware";
interface CounterState {
count: number;
increment: () => void;
}
export const useCounterStore = create<CounterState>()(
devtools(
(set) => ({
count: 0,
increment: () =>
set(
(state) => ({ count: state.count + 1 }),
false,
"increment" // ์ก์
์ด๋ฆ (DevTools์ ํ์)
),
}),
{ name: "CounterStore" } // Store ์ด๋ฆ
)
);
์ฌ๋ฌ ๋ฏธ๋ค์จ์ด ์กฐํฉ
// src/stores/app-store.ts
import { create } from "zustand";
import { devtools, persist, subscribeWithSelector } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
interface AppState {
theme: "light" | "dark";
language: "ko" | "en";
notifications: boolean;
setTheme: (theme: "light" | "dark") => void;
setLanguage: (lang: "ko" | "en") => void;
toggleNotifications: () => void;
}
export const useAppStore = create<AppState>()(
devtools(
persist(
subscribeWithSelector(
immer((set) => ({
theme: "light",
language: "ko",
notifications: true,
setTheme: (theme) =>
set((state) => {
state.theme = theme;
}),
setLanguage: (lang) =>
set((state) => {
state.language = lang;
}),
toggleNotifications: () =>
set((state) => {
state.notifications = !state.notifications;
}),
}))
),
{ name: "app-settings" }
),
{ name: "AppStore" }
)
);
// ์ํ ๋ณํ ๊ตฌ๋
useAppStore.subscribe(
(state) => state.theme,
(theme) => {
document.documentElement.setAttribute("data-theme", theme);
}
);
๐ท๏ธ ์ฌ๋ผ์ด์ค ํจํด (๋๊ท๋ชจ ์ฑ)
์ฌ๋ผ์ด์ค ํจํด์ด๋?
ํผ์๋ฅผ ์๊ฐํด๋ณด์.
ํผ์ ํ ํ์ ์ฌ๋ฌ ์กฐ๊ฐ์ผ๋ก ๋๋๋ฏ์ด,
ํ๋์ ํฐ Store๋ฅผ ์ฌ๋ฌ ๊ฐ์ ์์ ์กฐ๊ฐ(Slice)์ผ๋ก ๋๋๋ ํจํด์ด๋ค.
์ ์ฌ๋ผ์ด์ค ํจํด์ ์ฌ์ฉํ ๊น?
๋ฌธ์ ์ : ํ๋์ ๊ฑฐ๋ํ Store
// โ ๋์ ์: ๋ชจ๋ ์ํ๊ฐ ํ ํ์ผ์
const useStore = create((set) => ({
// ์ฌ์ฉ์ ๊ด๋ จ
user: null,
setUser: () => {},
logout: () => {},
// ์ฅ๋ฐ๊ตฌ๋ ๊ด๋ จ
items: [],
addToCart: () => {},
removeFromCart: () => {},
// UI ๊ด๋ จ
isSidebarOpen: false,
isModalOpen: false,
toggleSidebar: () => {},
// ์ค์ ๊ด๋ จ
theme: "light",
language: "ko",
setTheme: () => {},
// ... ์๋ฐฑ ์ค์ ์ฝ๋
}));
ํด๊ฒฐ์ฑ
: ์ฌ๋ผ์ด์ค๋ก ๋ถ๋ฆฌ
src/stores/
โโโ index.ts # ๋ชจ๋ ์ฌ๋ผ์ด์ค ํฉ์น๊ธฐ
โโโ slices/
โ โโโ user-slice.ts # ์ฌ์ฉ์ ๊ด๋ จ
โ โโโ cart-slice.ts # ์ฅ๋ฐ๊ตฌ๋ ๊ด๋ จ
โ โโโ ui-slice.ts # UI ๊ด๋ จ
โ โโโ settings-slice.ts # ์ค์ ๊ด๋ จ
โโโ types/
โโโ store.ts # ํ์
์ ์
์ฌ๋ผ์ด์ค ํจํด์ ์ฅ์ :
| ์ฅ์ | ์ค๋ช |
|---|---|
| ๊ด์ฌ์ฌ ๋ถ๋ฆฌ | ๊ธฐ๋ฅ๋ณ๋ก ์ฝ๋ ๋ถ๋ฆฌ |
| ์ ์ง๋ณด์ | ์์ ํ ํ์ผ ์ฐพ๊ธฐ ์ฌ์ |
| ํ์ | ํ์๋ณ๋ก ๋ค๋ฅธ ์ฌ๋ผ์ด์ค ๋ด๋น |
| ํ ์คํธ | ์ฌ๋ผ์ด์ค๋ณ ๋ ๋ฆฝ ํ ์คํธ |
| ์ฌ์ฌ์ฉ | ๋ค๋ฅธ ํ๋ก์ ํธ์์ ์ฌ๋ผ์ด์ค ์ฌ์ฌ์ฉ |
๊ธฐ๋ณธ ์ฌ๋ผ์ด์ค ๊ตฌํ
// src/stores/slices/user-slice.ts
import { StateCreator } from "zustand";
export interface UserSlice {
user: { id: string; name: string } | null;
setUser: (user: { id: string; name: string } | null) => void;
}
export const createUserSlice: StateCreator<UserSlice> = (set) => ({
user: null,
setUser: (user) => set({ user }),
});
// src/stores/slices/cart-slice.ts
import { StateCreator } from "zustand";
export interface CartSlice {
items: Array<{ id: string; quantity: number }>;
addToCart: (id: string) => void;
removeFromCart: (id: string) => void;
}
export const createCartSlice: StateCreator<CartSlice> = (set) => ({
items: [],
addToCart: (id) =>
set((state) => ({
items: [...state.items, { id, quantity: 1 }],
})),
removeFromCart: (id) =>
set((state) => ({
items: state.items.filter((item) => item.id !== id),
})),
});
// src/stores/slices/ui-slice.ts
import { StateCreator } from "zustand";
export interface UISlice {
isSidebarOpen: boolean;
isModalOpen: boolean;
toggleSidebar: () => void;
openModal: () => void;
closeModal: () => void;
}
export const createUISlice: StateCreator<UISlice> = (set) => ({
isSidebarOpen: false,
isModalOpen: false,
toggleSidebar: () =>
set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
openModal: () => set({ isModalOpen: true }),
closeModal: () => set({ isModalOpen: false }),
});
// src/stores/index.ts
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import { UserSlice, createUserSlice } from "./slices/user-slice";
import { CartSlice, createCartSlice } from "./slices/cart-slice";
import { UISlice, createUISlice } from "./slices/ui-slice";
// ๋ชจ๋ ์ฌ๋ผ์ด์ค ํฉ์น๊ธฐ
type StoreState = UserSlice & CartSlice & UISlice;
export const useStore = create<StoreState>()(
devtools((...args) => ({
...createUserSlice(...args),
...createCartSlice(...args),
...createUISlice(...args),
}))
);
์ฌ๋ผ์ด์ค ๊ฐ ์ํ ๊ณต์
์ฌ๋ผ์ด์ค๋ผ๋ฆฌ ์๋ก์ ์ํ์ ์ ๊ทผํด์ผ ํ ๋๊ฐ ์๋ค.
์: ์ฅ๋ฐ๊ตฌ๋์์ ๋ก๊ทธ์ธ ์ฌ๋ถ ํ์ธ
// src/stores/types/store.ts
// ์ ์ฒด Store ํ์
์ ์
import { UserSlice } from "../slices/user-slice";
import { CartSlice } from "../slices/cart-slice";
export type StoreState = UserSlice & CartSlice;
// src/stores/slices/cart-slice.ts
import { StateCreator } from "zustand";
import { StoreState } from "../types/store";
export interface CartSlice {
items: Array<{ id: string; quantity: number }>;
addToCart: (id: string) => void;
checkout: () => Promise<void>;
}
// ๋ ๋ฒ์งธ ์ ๋ค๋ฆญ์ผ๋ก ์ ์ฒด Store ํ์
์ ๋ฌ
export const createCartSlice: StateCreator<
StoreState, // ์ ์ฒด Store ํ์
[],
[],
CartSlice // ์ด ์ฌ๋ผ์ด์ค ํ์
> = (set, get) => ({
items: [],
addToCart: (id) => {
// ๋ค๋ฅธ ์ฌ๋ผ์ด์ค(UserSlice)์ ์ํ ์ ๊ทผ
const user = get().user;
if (!user) {
alert("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค!");
return;
}
set((state) => ({
items: [...state.items, { id, quantity: 1 }],
}));
},
checkout: async () => {
const user = get().user;
const items = get().items;
if (!user) {
throw new Error("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค.");
}
// ๊ฒฐ์ API ํธ์ถ
await fetch("/api/checkout", {
method: "POST",
body: JSON.stringify({ userId: user.id, items }),
});
// ์ฅ๋ฐ๊ตฌ๋ ๋น์ฐ๊ธฐ
set({ items: [] });
},
});
์ฌ๋ผ์ด์ค + ๋ฏธ๋ค์จ์ด ์กฐํฉ
// src/stores/index.ts
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
import { UserSlice, createUserSlice } from "./slices/user-slice";
import { CartSlice, createCartSlice } from "./slices/cart-slice";
import { UISlice, createUISlice } from "./slices/ui-slice";
type StoreState = UserSlice & CartSlice & UISlice;
export const useStore = create<StoreState>()(
devtools(
persist(
(...args) => ({
...createUserSlice(...args),
...createCartSlice(...args),
...createUISlice(...args),
}),
{
name: "app-storage",
// ํน์ ์ฌ๋ผ์ด์ค๋ง ์ ์ฅ
partialize: (state) => ({
user: state.user,
items: state.items,
// UI ์ํ๋ ์ ์ฅ ์ ํจ
}),
}
),
{ name: "AppStore" }
)
);
์ปดํฌ๋ํธ์์ ์ฌ์ฉ
// src/components/Header.tsx
import { useStore } from "../stores";
export const Header = () => {
// ํ์ํ ์ํ๋ง ์ ํ (์ฑ๋ฅ ์ต์ ํ)
const user = useStore((state) => state.user);
const itemCount = useStore((state) => state.items.length);
const toggleSidebar = useStore((state) => state.toggleSidebar);
return (
<header>
<button onClick={toggleSidebar}>๋ฉ๋ด</button>
<span>์ฅ๋ฐ๊ตฌ๋ ({itemCount})</span>
{user ? <span>{user.name}๋</span> : <a href="/login">๋ก๊ทธ์ธ</a>}
</header>
);
};
// src/components/Cart.tsx
import { useStore } from "../stores";
import { shallow } from "zustand/shallow";
export const Cart = () => {
// ์ฌ๋ฌ ์ํ๋ฅผ ํ ๋ฒ์ ์ ํ (shallow ๋น๊ต)
const { items, addToCart, checkout } = useStore(
(state) => ({
items: state.items,
addToCart: state.addToCart,
checkout: state.checkout,
}),
shallow
);
return (
<div>
{items.map((item) => (
<div key={item.id}>
์ํ ID: {item.id}, ์๋: {item.quantity}
</div>
))}
<button onClick={checkout}>๊ฒฐ์ ํ๊ธฐ</button>
</div>
);
};
์ฌ๋ผ์ด์ค ํจํด Best Practices
| ๊ท์น | ์ค๋ช |
|---|---|
| ๋จ์ผ ์ฑ ์ | ํ๋์ ์ฌ๋ผ์ด์ค๋ ํ๋์ ๊ธฐ๋ฅ๋ง |
| ํ์ ๋ถ๋ฆฌ | ํ์ ์ ๋ณ๋ ํ์ผ์ ์ ์ |
| ๋ค์ด๋ฐ | create[๊ธฐ๋ฅ]Slice ํ์ |
| ์์กด์ฑ ์ต์ํ | ์ฌ๋ผ์ด์ค ๊ฐ ์์กด์ฑ ์ค์ด๊ธฐ |
| ์ ํ์ ์ฌ์ฉ | ํ์ํ ์ํ๋ง ๊ตฌ๋ |
๐ท๏ธ React ์ธ๋ถ์์ ์ฌ์ฉ
// src/stores/counter-store.ts
import { create } from "zustand";
interface CounterState {
count: number;
increment: () => void;
}
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
// React ์ธ๋ถ์์ ์ํ ์ ๊ทผ
const count = useCounterStore.getState().count;
// React ์ธ๋ถ์์ ์ก์
ํธ์ถ
useCounterStore.getState().increment();
// ์ํ ๋ณํ ๊ตฌ๋
const unsubscribe = useCounterStore.subscribe((state) => {
console.log("์ํ ๋ณ๊ฒฝ:", state.count);
});
// ๊ตฌ๋
ํด์
unsubscribe();
// src/utils/api.ts
import { useAuthStore } from "../stores/auth-store";
// API ์ธํฐ์
ํฐ์์ ํ ํฐ ์ฌ์ฉ
export const apiClient = {
fetch: async (url: string, options: RequestInit = {}) => {
const token = useAuthStore.getState().token;
const response = await fetch(url, {
...options,
headers: {
...options.headers,
Authorization: token ? `Bearer ${token}` : "",
},
});
// 401 ์๋ฌ ์ ์๋ ๋ก๊ทธ์์
if (response.status === 401) {
useAuthStore.getState().logout();
}
return response;
},
};
์ค์ ์ฒดํฌ๋ฆฌ์คํธ
โ Store ์ค๊ณ
- ๊ด๋ จ ์ํ๋ผ๋ฆฌ ๊ทธ๋ฃนํ
- ํ์ ์ ์ ๋ช ํํ
- ์ก์ ์ด๋ฆ ์ง๊ด์ ์ผ๋ก
- ๋ถํ์ํ ์ํ ์ต์ํ
โ ์ฑ๋ฅ ์ต์ ํ
- ์ ํ์(selector) ์ฌ์ฉ
- ํ์ํ ์ํ๋ง ๊ตฌ๋
- ํฐ ๊ฐ์ฒด๋ ๋ถ๋ฆฌ
- ๋ฉ๋ชจ์ด์ ์ด์ ํ์ฉ
โ ๋ฏธ๋ค์จ์ด
- persist๋ก ์์์ฑ ๊ด๋ฆฌ
- devtools๋ก ๋๋ฒ๊น
- immer๋ก ๋ถ๋ณ์ฑ ๊ด๋ฆฌ
- subscribeWithSelector๋ก ๊ตฌ๋
โ ๊ตฌ์กฐํ
- ์ฌ๋ผ์ด์ค ํจํด ์ ์ฉ
- ํ์ ํ์ผ ๋ถ๋ฆฌ
- Store ํ์ผ ๋ถ๋ฆฌ
- ํ ์คํธ ์์ฑ
์์ฝ
Zustand๋ ๊ฐ๋จํ๊ณ ๊ฐ๋ ฅํ React ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค.
๐ ํต์ฌ ํฌ์ธํธ:
- create: Store ์์ฑ
- set: ์ํ ๋ณ๊ฒฝ
- get: ํ์ฌ ์ํ ์กฐํ
- selector: ํ์ํ ์ํ๋ง ๊ตฌ๋
- persist: localStorage ์ ์ฅ
- devtools: Redux DevTools ์ฐ๋
๐ ์ธ์ Zustand๋ฅผ ์ฌ์ฉํ ๊น?:
| ์ํฉ | ๊ถ์ฅ |
|---|---|
| ๊ฐ๋จํ ์ ์ญ ์ํ | Zustand โ |
| ๋ณต์กํ ๋น๋๊ธฐ ๋ก์ง | Zustand + React Query |
| ์๋ฒ ์ํ ๊ด๋ฆฌ | React Query / SWR |
| ํผ ์ํ | React Hook Form |
| ๋๊ท๋ชจ ์ฑ | Zustand (์ฌ๋ผ์ด์ค ํจํด) |
๐ Best Practices:
- ์ ํ์๋ก ํ์ํ ์ํ๋ง ๊ตฌ๋
- ๊ด๋ จ ์ํ๋ ํ๋์ Store์
- persist๋ก ์๋ก๊ณ ์นจ ๋์
- devtools๋ก ๋๋ฒ๊น
- TypeScript ํ์ ์ ์ ํ์
- ์ฌ๋ผ์ด์ค ํจํด์ผ๋ก ํ์ฅ
Zustand๋ฅผ ์ฌ์ฉํ๋ฉด,
Redux์ ๋ณต์กํจ ์์ด
๊ฐ๋ ฅํ ์ํ ๊ด๋ฆฌ๋ฅผ ๊ตฌํํ ์ ์๋ค.