← 상태 관리 비교로 돌아가기

🔍 라이브러리 상세 비교

동일한 기능을 3가지 라이브러리로 구현한 코드 비교

1️⃣ 기본 상태 정의

🐻 Zustand

import { create } from 'zustand';

const useStore = create((set) => ({
  count: 0,
  increment: () => 
    set((state) => ({ 
      count: state.count + 1 
    })),
}));

// 사용
const count = useStore(
  (state) => state.count
);

⚛️ Recoil

import { atom } from 'recoil';

const countState = atom({
  key: 'countState',
  default: 0,
});

// 사용
const [count, setCount] = 
  useRecoilState(countState);
  
setCount(count + 1);

👻 Jotai

import { atom } from 'jotai';

const countAtom = atom(0);

// 사용
const [count, setCount] = 
  useAtom(countAtom);
  
setCount(count + 1);

2️⃣ 파생 상태 (Derived State)

🐻 Zustand

const useStore = create((set) => ({
  todos: [],
  // 수동으로 계산
}));

// 컴포넌트에서 계산
const completedCount = 
  useStore(state => 
    state.todos.filter(
      t => t.completed
    ).length
  );

⚠️ 수동 계산 필요 (useMemo 권장)

⚛️ Recoil

const completedCountState = 
  selector({
    key: 'completedCount',
    get: ({ get }) => {
      const todos = 
        get(todosState);
      return todos.filter(
        t => t.completed
      ).length;
    },
  });

// 자동 메모이제이션
const count = useRecoilValue(
  completedCountState
);

✅ Selector로 자동 계산

👻 Jotai

const completedCountAtom = 
  atom((get) => {
    const todos = 
      get(todosAtom);
    return todos.filter(
      t => t.completed
    ).length;
  });

// 자동 메모이제이션
const count = useAtomValue(
  completedCountAtom
);

✅ 파생 Atom으로 자동 계산

3️⃣ 비동기 데이터 로딩

🐻 Zustand

const useStore = create((set) => ({
  user: null,
  loading: false,
  fetchUser: async (id) => {
    set({ loading: true });
    const res = await fetch(
      `/api/users/${id}`
    );
    const user = await res.json();
    set({ 
      user, 
      loading: false 
    });
  },
}));

⚠️ 로딩/에러 상태 수동 관리

⚛️ Recoil

const userQuery = selector({
  key: 'userQuery',
  get: async ({ get }) => {
    const userId = 
      get(userIdState);
    const res = await fetch(
      `/api/users/${userId}`
    );
    return res.json();
  },
});

// Suspense와 함께
<Suspense fallback="Loading">
  <UserComponent />
</Suspense>

✅ Suspense 완벽 지원

👻 Jotai

const userAtom = atom(
  async (get) => {
    const userId = 
      get(userIdAtom);
    const res = await fetch(
      `/api/users/${userId}`
    );
    return res.json();
  }
);

// Suspense와 함께
<Suspense fallback="Loading">
  <UserComponent />
</Suspense>

✅ Suspense 완벽 지원

4️⃣ 미들웨어 & 유틸리티

🐻 Zustand

import { 
  devtools, 
  persist 
} from 'zustand/middleware';

const useStore = create()(
  devtools(
    persist(
      (set) => ({
        todos: [],
      }),
      { name: 'todos' }
    )
  )
);
  • ✅ devtools
  • ✅ persist
  • ✅ immer
  • ✅ subscribeWithSelector

⚛️ Recoil

const todosState = atom({
  key: 'todos',
  default: [],
  effects: [
    ({ setSelf, onSet }) => {
      // localStorage 동기화
      const saved = 
        localStorage.getItem('todos');
      if (saved) {
        setSelf(JSON.parse(saved));
      }
      onSet((newValue) => {
        localStorage.setItem(
          'todos', 
          JSON.stringify(newValue)
        );
      });
    },
  ],
});
  • ✅ Atom Effects
  • ✅ DevTools
  • ✅ Time Travel

👻 Jotai

import { 
  atomWithStorage,
  atomFamily,
  selectAtom,
} from 'jotai/utils';

const todosAtom = 
  atomWithStorage(
    'todos', 
    []
  );

const userFamily = 
  atomFamily((id) =>
    atom(async () => {
      const res = await fetch(
        `/api/users/${id}`
      );
      return res.json();
    })
  );
  • ✅ atomWithStorage
  • ✅ atomFamily
  • ✅ selectAtom
  • ✅ atomWithDefault

5️⃣ TypeScript 지원

🐻 Zustand

interface TodoStore {
  todos: Todo[];
  addTodo: (text: string) 
    => void;
}

const useStore = 
  create<TodoStore>()(
    (set) => ({
      todos: [],
      addTodo: (text) =>
        set((state) => ({
          todos: [
            ...state.todos,
            { id: 1, text }
          ],
        })),
    })
  );

✅ 명시적 타입 정의 필요

⚛️ Recoil

const todosState = 
  atom<Todo[]>({
    key: 'todos',
    default: [],
  });

const completedCount = 
  selector<number>({
    key: 'completedCount',
    get: ({ get }) => {
      // 타입 자동 추론
      const todos = 
        get(todosState);
      return todos.length;
    },
  });

✅ 제네릭으로 타입 지정

👻 Jotai

// 타입 자동 추론 우수
const todosAtom = 
  atom<Todo[]>([]);

const completedAtom = 
  atom((get) => {
    const todos = 
      get(todosAtom);
    // number로 자동 추론
    return todos.filter(
      t => t.completed
    ).length;
  });

// 타입 자동 추론
const [count] = 
  useAtom(completedAtom);

✅ 타입 추론 가장 우수

📊 종합 비교

항목🐻 Zustand⚛️ Recoil👻 Jotai
번들 사이즈~3KB ✅~21KB ⚠️~3KB ✅
러닝 커브낮음 ✅중간 ⚠️중간 ⚠️
파생 상태수동 계산 ⚠️Selector ✅파생 Atom ✅
비동기 처리수동 ⚠️내장 ✅내장 ✅
Provider 필요불필요 ✅필요 ⚠️선택적 ✅
DevToolsRedux DevTools ✅전용 ✅제한적 ⚠️
TypeScript우수 ✅우수 ✅최고 ✅✅
React 외부 사용가능 ✅불가 ❌불가 ❌

💡 선택 가이드

🐻 Zustand 추천

  • ✅ 간단한 전역 상태 관리
  • ✅ Redux 패턴에 익숙한 경우
  • ✅ Provider 래핑 싫은 경우
  • ✅ React 외부에서도 사용
  • ✅ 작은 번들 사이즈 중요

⚛️ Recoil 추천

  • ✅ 복잡한 파생 상태 많음
  • ✅ 비동기 데이터 의존성
  • ✅ Suspense 적극 활용
  • ✅ Facebook 생태계
  • ✅ Time Travel Debugging

👻 Jotai 추천

  • ✅ TypeScript 프로젝트
  • ✅ Bottom-up 설계 선호
  • ✅ 세밀한 리렌더링 최적화
  • ✅ 작은 번들 + Atom 패턴
  • ✅ Recoil보다 가벼운 대안