개요

React 상태 관리 라이브러리는 Redux 이후 다양한 선택지가 등장했습니다. 2025년 현재 가장 주목받는 세 가지 라이브러리인 Zustand, Jotai, Recoil을 깊이 비교합니다. 각 라이브러리의 철학, 장단점, 적합한 사용 시나리오를 분석하여 프로젝트에 맞는 최적의 선택을 할 수 있도록 안내합니다.

핵심 개념

Zustand - Store 기반 단순함: Zustand는 Flux 아키텍처를 단순화한 스토어 기반 상태 관리 라이브러리입니다. 보일러플레이트가 극도로 적고, React 외부에서도 사용 가능하며, 번들 크기가 약 1.1KB로 매우 가볍습니다.

Jotai - 원자적 상태 관리: Jotai는 Recoil에서 영감을 받은 원자(atom) 기반 상태 관리 라이브러리입니다. 각 상태를 독립적인 atom으로 정의하고, 파생 상태를 선언적으로 구성합니다. Provider 없이도 동작하며, React Suspense와의 통합이 뛰어납니다.

Recoil - Facebook의 실험적 상태 관리: Recoil은 Facebook에서 개발한 실험적 상태 관리 라이브러리입니다. Atom과 Selector로 구성되며, 비동기 데이터 흐름을 기본적으로 지원합니다. 다만 2025년 현재 개발이 사실상 중단된 상태로, 새 프로젝트에서의 채택은 권장되지 않습니다.

실전 예제

동일한 카운터 + 비동기 데이터를 세 라이브러리로 구현해 봅니다.

// === Zustand ===
import { create } from 'zustand';

interface TodoStore {
  todos: Todo[];
  loading: boolean;
  fetchTodos: () => Promise<void>;
  addTodo: (title: string) => void;
}

const useTodoStore = create<TodoStore>((set) => ({
  todos: [],
  loading: false,
  fetchTodos: async () => {
    set({ loading: true });
    const res = await fetch('/api/todos');
    const todos = await res.json();
    set({ todos, loading: false });
  },
  addTodo: (title) =>
    set((state) => ({
      todos: [...state.todos, { id: Date.now(), title, done: false }],
    })),
}));

// 컴포넌트에서 사용 - 필요한 상태만 구독
function TodoList() {
  const todos = useTodoStore((s) => s.todos);
  const fetchTodos = useTodoStore((s) => s.fetchTodos);
  useEffect(() => { fetchTodos(); }, []);
  return todos.map(t => <div key={t.id}>{t.title}</div>);
}
// === Jotai ===
import { atom, useAtom } from 'jotai';

const todosAtom = atom<Todo[]>([]);
const loadingAtom = atom(false);

// 비동기 파생 atom
const fetchTodosAtom = atom(null, async (get, set) => {
  set(loadingAtom, true);
  const res = await fetch('/api/todos');
  const todos = await res.json();
  set(todosAtom, todos);
  set(loadingAtom, false);
});

// 읽기 전용 파생 atom
const completedCountAtom = atom(
  (get) => get(todosAtom).filter(t => t.done).length
);

function TodoList() {
  const [todos] = useAtom(todosAtom);
  const [, fetchTodos] = useAtom(fetchTodosAtom);
  useEffect(() => { fetchTodos(); }, []);
  return todos.map(t => <div key={t.id}>{t.title}</div>);
}

활용 팁

  • 소규모 프로젝트: Zustand를 추천합니다. 설정이 간단하고, 학습 곡선이 낮으며, 대부분의 상태 관리 요구를 충족합니다.
  • 복잡한 파생 상태: Jotai가 유리합니다. atom 간의 의존성을 선언적으로 표현할 수 있어, 복잡한 데이터 흐름에 적합합니다.
  • 서버 상태: Zustand나 Jotai와 함께 TanStack Query(React Query)를 사용하는 것이 가장 효과적입니다. 서버 상태를 클라이언트 상태 관리 라이브러리로 관리하는 것은 안티패턴입니다.
  • SSR 호환성: Jotai는 Provider를 사용하면 서버 렌더링에서 상태 격리가 가능합니다. Zustand도 createStore 패턴으로 SSR을 지원합니다.
  • DevTools: Zustand는 Redux DevTools를 지원하여 상태 디버깅이 용이합니다. Jotai도 전용 DevTools를 제공합니다.
  • Recoil 마이그레이션: Recoil을 사용 중이라면 Jotai로의 마이그레이션을 권장합니다. API가 유사하여 전환 비용이 낮습니다.

마무리

2025년 기준 Zustand와 Jotai가 가장 활발한 커뮤니티와 발전을 보여주고 있습니다. Zustand는 단순함과 유연성, Jotai는 원자적 구성과 파생 상태 표현력이 강점입니다. 프로젝트의 규모와 복잡성, 팀의 선호도에 따라 적절한 라이브러리를 선택하되, 서버 상태와 클라이언트 상태를 명확히 분리하는 것이 중요합니다.