Sam Baek, The Dev's Corner

๐Ÿป Zustand ์™„๋ฒฝ ๊ฐ€์ด๋“œ (React ์ƒํƒœ ๊ด€๋ฆฌ์˜ ์ƒˆ๋กœ์šด ํ‘œ์ค€)

14 Nov 2025

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 ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‹ค.

๐Ÿ’Ž ํ•ต์‹ฌ ํฌ์ธํŠธ:

  1. create: Store ์ƒ์„ฑ
  2. set: ์ƒํƒœ ๋ณ€๊ฒฝ
  3. get: ํ˜„์žฌ ์ƒํƒœ ์กฐํšŒ
  4. selector: ํ•„์š”ํ•œ ์ƒํƒœ๋งŒ ๊ตฌ๋…
  5. persist: localStorage ์ €์žฅ
  6. devtools: Redux DevTools ์—ฐ๋™


๐Ÿ“Œ ์–ธ์ œ Zustand๋ฅผ ์‚ฌ์šฉํ• ๊นŒ?:

์ƒํ™ฉ ๊ถŒ์žฅ
๊ฐ„๋‹จํ•œ ์ „์—ญ ์ƒํƒœ Zustand โœ…
๋ณต์žกํ•œ ๋น„๋™๊ธฐ ๋กœ์ง Zustand + React Query
์„œ๋ฒ„ ์ƒํƒœ ๊ด€๋ฆฌ React Query / SWR
ํผ ์ƒํƒœ React Hook Form
๋Œ€๊ทœ๋ชจ ์•ฑ Zustand (์Šฌ๋ผ์ด์Šค ํŒจํ„ด)


๐Ÿš€ Best Practices:

  • ์„ ํƒ์ž๋กœ ํ•„์š”ํ•œ ์ƒํƒœ๋งŒ ๊ตฌ๋…
  • ๊ด€๋ จ ์ƒํƒœ๋Š” ํ•˜๋‚˜์˜ Store์—
  • persist๋กœ ์ƒˆ๋กœ๊ณ ์นจ ๋Œ€์‘
  • devtools๋กœ ๋””๋ฒ„๊น…
  • TypeScript ํƒ€์ž… ์ •์˜ ํ•„์ˆ˜
  • ์Šฌ๋ผ์ด์Šค ํŒจํ„ด์œผ๋กœ ํ™•์žฅ


Zustand๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด,
Redux์˜ ๋ณต์žกํ•จ ์—†์ด
๊ฐ•๋ ฅํ•œ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.