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

👻 Jotai 예제

원시적이고 유연한 React 상태 관리

📦 패키지 설치 필요

npm install jotai

현재는 로컬 상태로 구현되어 있습니다. 패키지 설치 후 코드 주석을 해제하세요.

✨ Jotai의 핵심 특징

  • 원시적 Atom: 최소 단위의 상태 관리
  • Bottom-up 접근: 필요한 곳에서 Atom 조합
  • TypeScript 최적화: 타입 추론 자동화
  • 작은 번들: ~3KB (Zustand와 유사)
  • 유틸리티 풍부: atomWithStorage, atomFamily 등
  • React Suspense: 비동기 처리 완벽 지원

📝 Todo App 데모

할 일이 없습니다

📊 통계 (파생 Atom으로 계산됨)

0
전체
0
진행 중
0
완료

💻 기본 Atom 정의

import { atom, useAtom } from 'jotai';

// 원시 atom (primitive)
const todosAtom = atom<Todo[]>([]);
const filterAtom = atom<'all' | 'active' | 'completed'>('all');

// 파생 atom (derived) - 읽기 전용
const filteredTodosAtom = atom((get) => {
  const todos = get(todosAtom);
  const filter = get(filterAtom);

  if (filter === 'active') return todos.filter(t => !t.completed);
  if (filter === 'completed') return todos.filter(t => t.completed);
  return todos;
});

// 통계 atom (읽기 전용)
const todoStatsAtom = atom((get) => {
  const todos = get(todosAtom);
  return {
    total: todos.length,
    completed: todos.filter(t => t.completed).length,
    active: todos.filter(t => !t.completed).length,
  };
});

// 컴포넌트에서 사용
function TodoList() {
  const [todos, setTodos] = useAtom(todosAtom);
  const filteredTodos = useAtomValue(filteredTodosAtom);
  
  return <div>...</div>;
}

✍️ Write Atom (액션 패턴)

import { atom, useSetAtom } from 'jotai';

// 읽기/쓰기 atom
const addTodoAtom = atom(
  null, // 읽기는 사용 안 함
  (get, set, text: string) => {
    const todos = get(todosAtom);
    set(todosAtom, [
      ...todos,
      { id: Date.now(), text, completed: false }
    ]);
  }
);

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

// 컴포넌트에서 사용 (리렌더링 없음)
function TodoInput() {
  const addTodo = useSetAtom(addTodoAtom);
  
  const handleSubmit = (text: string) => {
    addTodo(text);
  };
  
  return <form onSubmit={...}>...</form>;
}

🛠️ 유틸리티 Atom

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

// localStorage 동기화
const todosAtom = atomWithStorage<Todo[]>('jotai-todos', []);

// 동적 atom 생성 (ID별 캐싱)
const userAtomFamily = atomFamily((userId: number) =>
  atom(async () => {
    const res = await fetch(`/api/users/${userId}`);
    return res.json();
  })
);

// 특정 필드만 구독
const userNameAtom = selectAtom(
  userAtom,
  (user) => user.name
);

// 컴포넌트에서 사용
function UserProfile({ userId }: { userId: number }) {
  const [user] = useAtom(userAtomFamily(userId));
  return <div>{user.name}</div>;
}

🌐 비동기 Atom (Suspense)

import { atom, useAtomValue } from 'jotai';
import { Suspense } from 'react';

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

// 의존적인 비동기 atom
const userPostsAtom = atom(async (get) => {
  const user = await get(userAtom);
  const response = await fetch(`/api/users/${user.id}/posts`);
  return response.json();
});

// Suspense와 함께 사용
function UserPosts() {
  const posts = useAtomValue(userPostsAtom);
  
  return (
    <div>
      {posts.map(post => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
}

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

🤔 Zustand vs Jotai

Zustand

  • • Redux 스타일 (중앙 스토어)
  • • 액션 함수 내장
  • • Provider 불필요
  • • React 외부에서도 사용

Jotai

  • • Recoil 스타일 (Atom)
  • • Write Atom으로 액션 정의
  • • Provider 선택적
  • • React에 최적화

✅ 장점

  • • 매우 작은 번들 사이즈 (~3KB)
  • • TypeScript 타입 추론 우수
  • • 원시적이고 유연한 API
  • • React Suspense 완벽 지원
  • • 풍부한 유틸리티 (atomWithStorage 등)
  • • 세밀한 리렌더링 최적화

⚠️ 단점

  • • 러닝 커브 (Atom 패턴 이해 필요)
  • • 액션 패턴이 Zustand보다 복잡
  • • DevTools 제한적
  • • 커뮤니티가 Recoil보다 작음
  • • React에만 사용 가능