TypeScript

[React] Zustand로 배우는 간단하고 강력한 상태 관리

코샵 2024. 12. 20. 10:59
반응형

소개

Zustand는 React를 위한 작고 빠른 상태 관리 라이브러리입니다. Redux의 복잡성을 줄이면서도 강력한 기능을 제공하는 Zustand의 사용법을 알아보겠습니다.

설치

# npm
npm install zustand

# yarn
yarn add zustand

# pnpm
pnpm add zustand

기본적인 스토어 생성과 사용

스토어 생성

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

interface CounterState {
  count: number;
  increment: () => void;
  decrement: () => void;
  reset: () => 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 })
}));

export default useCounterStore;

컴포넌트에서 사용

// components/Counter.tsx
import useCounterStore from '../stores/useCounterStore';

function Counter() {
  const { count, increment, decrement, reset } = useCounterStore();

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

비동기 액션 처리

// stores/useUserStore.ts
interface UserState {
  user: User | null;
  loading: boolean;
  error: string | null;
  fetchUser: (id: string) => Promise<void>;
}

const useUserStore = create<UserState>((set) => ({
  user: null,
  loading: false,
  error: null,
  fetchUser: async (id) => {
    try {
      set({ loading: true, error: null });
      const response = await fetch(`/api/users/${id}`);
      const user = await response.json();
      set({ user, loading: false });
    } catch (error) {
      set({ error: error.message, loading: false });
    }
  }
}));

상태 구독과 선택적 업데이트

// 특정 상태만 구독
function UserName() {
  const userName = useUserStore((state) => state.user?.name);
  return <div>{userName}</div>;
}

// 여러 상태 구독
function UserStatus() {
  const { loading, error } = useUserStore((state) => ({
    loading: state.loading,
    error: state.error
  }));

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return null;
}

미들웨어 활용

로깅 미들웨어

const logMiddleware = (config) => (set, get, api) =>
  config(
    (...args) => {
      console.log('이전 상태:', get());
      set(...args);
      console.log('다음 상태:', get());
    },
    get,
    api
  );

const useStore = create(
  logMiddleware((set) => ({
    // store 구현
  }))
);

영속성 미들웨어

import { persist } from 'zustand/middleware';

const useStore = create(
  persist(
    (set) => ({
      // store 구현
    }),
    {
      name: 'storage-key',
      getStorage: () => localStorage
    }
  )
);

타입스크립트와의 통합

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

interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

const useTodoStore = create<TodoState>((set) => ({
  todos: [],
  addTodo: (text) => 
    set((state) => ({
      todos: [...state.todos, { 
        id: Date.now().toString(), 
        text, 
        completed: false 
      }]
    })),
  removeTodo: (id) =>
    set((state) => ({
      todos: state.todos.filter(todo => todo.id !== id)
    })),
  toggleTodo: (id) =>
    set((state) => ({
      todos: state.todos.map(todo =>
        todo.id === id 
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    }))
}));

성능 최적화

메모이제이션 활용

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

  // 메모이제이션된 필터링
  const completedTodos = useMemo(
    () => todos.filter(todo => todo.completed),
    [todos]
  );

  return (
    <div>
      {completedTodos.map(todo => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </div>
  );
}

선택적 렌더링

const TodoCount = memo(() => {
  const count = useTodoStore((state) => state.todos.length);
  return <div>Total todos: {count}</div>;
});

실전 활용 예제

인증 상태 관리

interface AuthState {
  user: User | null;
  token: string | null;
  login: (credentials: Credentials) => Promise<void>;
  logout: () => void;
  updateUser: (userData: Partial<User>) => void;
}

const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      user: null,
      token: null,
      login: async (credentials) => {
        const response = await loginApi(credentials);
        set({ user: response.user, token: response.token });
      },
      logout: () => set({ user: null, token: null }),
      updateUser: (userData) =>
        set((state) => ({
          user: state.user ? { ...state.user, ...userData } : null
        }))
    }),
    {
      name: 'auth-storage'
    }
  )
);

마치며

Zustand는 단순하면서도 강력한 상태 관리 솔루션을 제공합니다. Redux의 보일러플레이트 없이도 복잡한 상태 관리를 할 수 있으며, TypeScript와의 통합도 매우 자연스럽습니다.

사용 시 주의사항

  1. 상태 업데이트는 항상 불변성을 지켜야 합니다.
  2. 큰 상태 객체는 적절히 분리하여 관리하세요.
  3. 성능 최적화를 위해 선택적 구독을 활용하세요.