개요

Redux의 복잡성에 지친 개발자들을 위해 등장한 Zustand와 Jotai는 모두 경량 상태관리 라이브러리입니다. 이 가이드에서는 두 라이브러리의 철학, 사용법, 장단점을 비교하여 프로젝트에 적합한 선택을 돕습니다.

Zustand: 간결한 중앙 집중식 상태관리

핵심 개념

  • 단일 스토어: Redux처럼 중앙 집중식 상태 관리
  • 보일러플레이트 최소화: Provider 없이 바로 사용 가능
  • 불변성 자동 처리: Immer 통합으로 편리한 상태 업데이트
  • 미들웨어 지원: Redux DevTools, persist 등 확장 가능

기본 사용법

// store.ts
import { create } from 'zustand';

interface TodoState {
  todos: Array<{ id: number; text: string; completed: boolean }>;
  addTodo: (text: string) => void;
  toggleTodo: (id: number) => void;
  removeTodo: (id: number) => void;
}

export const useTodoStore = create<TodoState>((set) => ({
  todos: [],

  addTodo: (text) =>
    set((state) => ({
      todos: [...state.todos, { id: Date.now(), text, completed: false }],
    })),

  toggleTodo: (id) =>
    set((state) => ({
      todos: state.todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      ),
    })),

  removeTodo: (id) =>
    set((state) => ({
      todos: state.todos.filter((todo) => todo.id !== id),
    })),
}));

// TodoList.tsx
import { useTodoStore } from './store';

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

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id} onClick={() => toggleTodo(todo.id)}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

Immer 미들웨어로 편리한 상태 업데이트

import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

interface UserState {
  user: {
    profile: {
      name: string;
      email: string;
    };
    settings: {
      theme: 'light' | 'dark';
    };
  };
  updateName: (name: string) => void;
  toggleTheme: () => void;
}

export const useUserStore = create<UserState>()(
  immer((set) => ({
    user: {
      profile: { name: '', email: '' },
      settings: { theme: 'light' },
    },

    // Immer로 직접 수정 가능 (불변성 자동 처리)
    updateName: (name) =>
      set((state) => {
        state.user.profile.name = name;
      }),

    toggleTheme: () =>
      set((state) => {
        state.user.settings.theme =
          state.user.settings.theme === 'light' ? 'dark' : 'light';
      }),
  }))
);

Persist 미들웨어로 로컬스토리지 연동

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export const useAuthStore = create(
  persist(
    (set) => ({
      token: null,
      user: null,
      login: (token, user) => set({ token, user }),
      logout: () => set({ token: null, user: null }),
    }),
    {
      name: 'auth-storage', // localStorage 키
      partialize: (state) => ({ token: state.token }), // 일부만 저장
    }
  )
);

Jotai: 원자적(Atomic) 상태관리

핵심 개념

  • 아톰 기반: Recoil처럼 작은 상태 단위(atom)로 분리
  • 상향식 접근: 필요한 만큼만 정의하고 조합
  • 파생 상태: 다른 아톰을 참조하는 computed 값 생성
  • 서스펜스 통합: 비동기 상태를 React Suspense로 처리

기본 사용법

// atoms.ts
import { atom } from 'jotai';

export const todosAtom = atom<Array<{ id: number; text: string; completed: boolean }>>([]);

export const addTodoAtom = atom(
  null, // read
  (get, set, text: string) => {
    const todos = get(todosAtom);
    set(todosAtom, [...todos, { id: Date.now(), text, completed: false }]);
  }
);

export const toggleTodoAtom = atom(
  null,
  (get, set, id: number) => {
    const todos = get(todosAtom);
    set(
      todosAtom,
      todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  }
);

// 파생 상태 (computed)
export const completedTodosAtom = atom((get) => {
  const todos = get(todosAtom);
  return todos.filter((todo) => todo.completed);
});

export const todoStatsAtom = atom((get) => {
  const todos = get(todosAtom);
  return {
    total: todos.length,
    completed: todos.filter((t) => t.completed).length,
    pending: todos.filter((t) => !t.completed).length,
  };
});

// TodoList.tsx
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { todosAtom, toggleTodoAtom, todoStatsAtom } from './atoms';

export function TodoList() {
  const todos = useAtomValue(todosAtom);
  const toggleTodo = useSetAtom(toggleTodoAtom);

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id} onClick={() => toggleTodo(todo.id)}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

export function TodoStats() {
  const stats = useAtomValue(todoStatsAtom);
  return <div>완료: {stats.completed}/{stats.total}</div>;
}

비동기 아톰과 Suspense

import { atom } from 'jotai';

// 비동기 데이터 페칭
export const userAtom = atom(async () => {
  const response = await fetch('/api/user');
  return response.json();
});

// 파생 비동기 아톰
export const postsAtom = atom(async (get) => {
  const user = await get(userAtom);
  const response = await fetch(`/api/users/${user.id}/posts`);
  return response.json();
});

// UserProfile.tsx
import { Suspense } from 'react';
import { useAtomValue } from 'jotai';

function UserProfile() {
  const user = useAtomValue(userAtom); // Suspense 경계 내에서 사용
  return <div>{user.name}</div>;
}

export function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <UserProfile />
    </Suspense>
  );
}

아톰 패밀리 (동적 아톰 생성)

import { atomFamily } from 'jotai/utils';

// ID별로 동적으로 아톰 생성
export const todoAtomFamily = atomFamily((id: number) =>
  atom({
    id,
    text: '',
    completed: false,
  })
);

// 사용
function TodoItem({ id }: { id: number }) {
  const [todo, setTodo] = useAtom(todoAtomFamily(id));

  return (
    <div>
      <input
        value={todo.text}
        onChange={(e) => setTodo({ ...todo, text: e.target.value })}
      />
    </div>
  );
}

비교 분석

번들 크기

  • Zustand: ~1.2KB (gzipped)
  • Jotai: ~2.9KB (gzipped)

학습 곡선

  • Zustand: 매우 낮음. Redux 경험이 있다면 즉시 사용 가능
  • Jotai: 낮음. 아톰 개념 이해 후 직관적

사용 사례별 추천

상황 Zustand Jotai
전역 상태가 많고 복잡한 앱 ⭐⭐⭐ ⭐⭐
작은 독립적인 상태들 ⭐⭐ ⭐⭐⭐
비동기 데이터 페칭 ⭐⭐ ⭐⭐⭐
Redux DevTools 필요 ⭐⭐⭐
서버 상태 관리 ⭐⭐ ⭐⭐⭐
최소 번들 크기 ⭐⭐⭐ ⭐⭐

활용 팁

  • Zustand는 중앙 집중식 상태가 필요한 대규모 앱에 적합합니다.
  • Jotai는 상태를 작은 단위로 분리하고 조합하기 좋습니다.
  • Zustand의 미들웨어 생태계를 활용하면 다양한 기능을 쉽게 추가할 수 있습니다.
  • Jotai는 React Suspense와 자연스럽게 통합되어 비동기 상태 관리가 편리합니다.
  • 두 라이브러리 모두 React Context보다 성능이 우수합니다.

마무리

Zustand와 Jotai는 각각의 철학과 강점을 가진 훌륭한 상태관리 라이브러리입니다. 프로젝트의 규모, 상태 구조, 팀의 선호도를 고려하여 선택하면 됩니다. 작은 프로젝트라면 둘 다 충분히 좋은 선택이며, 복잡도가 증가할수록 각자의 장점이 더 명확해집니다.