Skip to Content
PatternsState Management

State Management

상태 관리 구현 패턴입니다.


목차

  1. 개요
  2. Zustand 기본 사용법
  3. 실전 예제
  4. Zustand 미들웨어
  5. Best Practices
  6. 대안적 방법

개요

권장 라이브러리

라이브러리권장도설명
Zustand기본 권장간단한 API, 보일러플레이트 최소화, TypeScript 지원
Context API단순 케이스Props 3단계 이상 drilling 시
useReducer복잡한 로직단일 컴포넌트 내 복잡한 상태

사용 사례

  • 전역 사용자 인증 상태
  • UI 테마/레이아웃 설정
  • 폼 상태 관리
  • 복잡한 비즈니스 로직

사용하지 말아야 할 때

  • 단일 컴포넌트 로컬 상태 → useState
  • 서버 상태 → React Query
  • URL 상태 → React Router

Zustand 기본 사용법

설치

pnpm add zustand

Store 생성

// stores/auth.ts import { create } from "zustand" interface User { id: string name: string email: string } interface AuthStore { user: User | null isAuthenticated: boolean login: (user: User) => void logout: () => void } export const useAuthStore = create<AuthStore>((set) => ({ user: null, isAuthenticated: false, login: (user) => set({ user, isAuthenticated: true }), logout: () => set({ user: null, isAuthenticated: false }), }))

컴포넌트에서 사용

import { useAuthStore } from "@/stores/auth" export function ProfilePage() { const { user, logout } = useAuthStore() return ( <div className="p-4"> <h1>프로필</h1> <p>{user?.name}</p> <button onClick={logout}>로그아웃</button> </div> ) }

Selector로 성능 최적화

// 전체 store 구독 (비권장) const { user } = useAuthStore() // 필요한 값만 구독 (권장) const user = useAuthStore((state) => state.user) const isAuthenticated = useAuthStore((state) => state.isAuthenticated)

실전 예제

Showcase Store (icignal-vite-demo)

📁 examples/icignal-vite-demo/src/stores/showcase.ts 참조

// stores/showcase.ts import { create } from "zustand" import { persist } from "zustand/middleware" export type Theme = "blue" | "cyan" | "purple" | "green" | "rose" type OverlayConfig = { type: "center" | "bottomsheet" | "fullscreen" | "sidepanel" dim: boolean blockInteraction: boolean toastPosition: | "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right" } type State = { layout: "default" | "top-nav" theme: Theme colorMode?: string overlayConfig: OverlayConfig } type Action = { setLayout: (layout: "default" | "top-nav") => void setTheme: (theme: Theme) => void setColorMode: (colorMode?: string) => void setOverlayConfig: (config: OverlayConfig) => void } export const useShowcase = create<State & Action>()( persist( (set) => ({ layout: "default", theme: "blue", colorMode: "light", overlayConfig: { type: "center", dim: true, blockInteraction: true, toastPosition: "top-right", }, setLayout: (layout) => set({ layout }), setTheme: (theme) => set({ theme }), setColorMode: (colorMode) => set({ colorMode }), setOverlayConfig: (overlayConfig) => set({ overlayConfig }), }), { name: "showcase-storage" }, ), )

파생 훅 패턴

Store의 특정 부분만 사용하는 커스텀 훅으로 관심사를 분리합니다.

// 오버레이 설정만 사용하는 훅 export const useOverlayConfig = () => { const config = useShowcase((state) => state.overlayConfig) const setConfig = useShowcase((state) => state.setOverlayConfig) return { config, setConfig } } // 테마만 사용하는 훅 export const useThemeConfig = () => { const theme = useShowcase((state) => state.theme) const colorMode = useShowcase((state) => state.colorMode) const setTheme = useShowcase((state) => state.setTheme) const setColorMode = useShowcase((state) => state.setColorMode) return { theme, colorMode, setTheme, setColorMode } }

컴포넌트에서 사용

import { useOverlayConfig } from "@/stores/showcase" export function SettingsPanel() { const { config, setConfig } = useOverlayConfig() return ( <div className="space-y-4"> <select value={config.type} onChange={(e) => setConfig({ ...config, type: e.target.value as OverlayConfig["type"], }) } > <option value="center">Center</option> <option value="fullscreen">Fullscreen</option> <option value="bottomsheet">Bottom Sheet</option> </select> <label> <input type="checkbox" checked={config.dim} onChange={(e) => setConfig({ ...config, dim: e.target.checked })} /> Dim 배경 </label> </div> ) }

Zustand 미들웨어

persist (localStorage 저장)

import { create } from "zustand" import { persist } from "zustand/middleware" export const useSettingsStore = create( persist( (set) => ({ theme: "light", setTheme: (theme) => set({ theme }), }), { name: "settings-storage" }, // localStorage key ), )

immer (불변성 자동 관리)

pnpm add immer
import { create } from "zustand" import { immer } from "zustand/middleware/immer" interface Todo { id: string text: string completed: boolean } export const useTodoStore = create( immer<{ todos: Todo[]; addTodo: (text: string) => void }>((set) => ({ todos: [], addTodo: (text) => set((state) => { state.todos.push({ id: Date.now().toString(), text, completed: false }) }), })), )

Best Practices

✅ 권장 사항

영역권장 사항
Selector필요한 값만 구독하여 리렌더링 최소화
파생 훅관심사별로 store 분리
persist사용자 설정 등 영속 필요 데이터에 사용
타입State와 Action 타입 분리 정의

⚠️ 피해야 할 것

  • 모든 상태를 전역으로 관리
  • 서버 상태를 Zustand로 관리 (→ React Query 사용)
  • 전체 store 구독 (const { user } = useStore())

대안적 방법

Zustand 외에도 다음 방법들을 사용할 수 있습니다.

Context API (단순 케이스)

import { createContext, useContext, useState, ReactNode } from "react" const ThemeContext = createContext<{ theme: string toggle: () => void } | null>(null) export function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState("light") const toggle = () => setTheme(theme === "light" ? "dark" : "light") return ( <ThemeContext.Provider value={{ theme, toggle }}> {children} </ThemeContext.Provider> ) } export const useTheme = () => { const ctx = useContext(ThemeContext) if (!ctx) throw new Error("useTheme must be used within ThemeProvider") return ctx }

사용 시점: Props drilling 3단계 이상, 단순한 전역 상태

useReducer (복잡한 로컬 상태)

import { useReducer } from "react" type State = { count: number } type Action = { type: "increment" } | { type: "decrement" } function reducer(state: State, action: Action): State { switch (action.type) { case "increment": return { count: state.count + 1 } case "decrement": return { count: state.count - 1 } } } function Counter() { const [state, dispatch] = useReducer(reducer, { count: 0 }) return ( <div> <span>{state.count}</span> <button onClick={() => dispatch({ type: "increment" })}>+</button> </div> ) }

사용 시점: 단일 컴포넌트 내 복잡한 상태 로직


관련 패턴

Last updated on