🌐 웹개발

Jotai

조타이 (일본어로 '상태')

Jotai는 원자(atom) 기반의 React 상태 관리 라이브러리입니다. Recoil에서 영감을 받아 더 간단한 API와 작은 번들 크기로 설계되었으며, 보일러플레이트 없이 세밀한 상태 관리가 가능합니다.

📖 상세 설명

Jotai(조타이)는 일본어로 '상태'를 의미하며, 2021년 Daishi Kato가 개발한 React 상태 관리 라이브러리입니다. Recoil의 원자적(atomic) 접근 방식에서 영감을 받았지만, 더 작은 번들 크기(약 3KB gzip)와 간소화된 API를 제공합니다. Provider 없이도 작동하며 React Suspense와 Concurrent Mode를 네이티브로 지원합니다.

Jotai의 핵심 개념은 'atom'입니다. atom은 상태의 최소 단위로, 컴포넌트 외부에서 정의되며 여러 컴포넌트 간에 공유될 수 있습니다. 각 atom은 독립적으로 구독되므로, 특정 atom이 변경되면 해당 atom을 구독하는 컴포넌트만 리렌더링됩니다. 이를 통해 불필요한 리렌더링을 방지하고 성능을 최적화합니다.

파생 상태(derived state)는 atom 함수에 get 매개변수를 사용해 다른 atom들을 조합하여 생성합니다. 비동기 atom은 async 함수를 사용해 정의할 수 있으며, React Suspense와 자연스럽게 통합됩니다. atomWithStorage, atomWithObservable 등 다양한 유틸리티 atom을 통해 localStorage 연동, RxJS 연동 등 확장 기능을 제공합니다.

Jotai는 Zustand와 함께 poimandres 팀에서 관리하며, React 생태계에서 가장 인기 있는 경량 상태 관리 라이브러리 중 하나입니다. TypeScript를 완벽히 지원하고, devtools 확장도 제공합니다. 특히 컴포넌트 단위의 세밀한 상태 관리가 필요하거나, Context API의 불필요한 리렌더링 문제를 해결하고 싶을 때 적합합니다.

💻 코드 예제

import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';

// 1. 기본 atom 정의 (컴포넌트 외부에서)
const countAtom = atom(0);
const nameAtom = atom('');

// 2. 파생 atom (다른 atom 기반 계산)
const doubleCountAtom = atom((get) => get(countAtom) * 2);

// 3. 읽기/쓰기 atom (커스텀 로직)
const countWithLimitAtom = atom(
  (get) => get(countAtom),
  (get, set, newValue: number) => {
    // 0-100 사이로 제한
    const clampedValue = Math.max(0, Math.min(100, newValue));
    set(countAtom, clampedValue);
  }
);

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

// 5. localStorage 연동 atom
const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light');

// 6. 컴포넌트에서 사용
function Counter() {
  // 읽기 + 쓰기
  const [count, setCount] = useAtom(countAtom);

  // 읽기만 (리렌더링 최적화)
  const doubleCount = useAtomValue(doubleCountAtom);

  // 쓰기만 (리렌더링 최적화)
  const setName = useSetAtom(nameAtom);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Double: {doubleCount}</p>
      <button onClick={() => setCount(c => c + 1)}>
        Increment
      </button>
      <button onClick={() => setName('Alice')}>
        Set Name
      </button>
    </div>
  );
}

// 7. Suspense와 함께 비동기 atom 사용
function UserProfile() {
  const user = useAtomValue(userAtom); // 자동으로 Suspense 트리거

  return <div>Welcome, {user.name}</div>;
}

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

// 8. 실무 패턴: 기능별 atom 모듈화
// atoms/cart.ts
export const cartItemsAtom = atom<CartItem[]>([]);

export const cartTotalAtom = atom((get) => {
  const items = get(cartItemsAtom);
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
});

export const addToCartAtom = atom(
  null,
  (get, set, item: CartItem) => {
    const items = get(cartItemsAtom);
    const existing = items.find(i => i.id === item.id);

    if (existing) {
      set(cartItemsAtom, items.map(i =>
        i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
      ));
    } else {
      set(cartItemsAtom, [...items, { ...item, quantity: 1 }]);
    }
  }
);

🗣️ 실무 대화 예시

💬 상황: 새 프로젝트의 상태 관리 라이브러리 선택 논의

주니어 "상태 관리로 Redux를 쓰려고 하는데, 보일러플레이트가 너무 많은 것 같아요. 더 간단한 대안이 있을까요?"
시니어 "프로젝트 규모에 따라 다른데, 컴포넌트 단위 상태가 많다면 Jotai를 추천해요. atom 하나 정의하고 useAtom으로 바로 쓰면 돼요. Redux Toolkit보다 훨씬 적은 코드로 같은 결과를 얻을 수 있죠."
주니어 "Recoil이랑 비슷한 것 같은데, 차이점이 뭐예요?"
시니어 "Jotai가 번들 크기가 훨씬 작고(3KB vs 25KB), Provider 없이도 동작해요. Recoil은 key 문자열이 필수인데 Jotai는 atom 참조만으로 충분하고요. 둘 다 Facebook 출신이지만, Jotai가 더 미니멀하고 유지보수가 활발해요. 저희 팀은 Jotai + TanStack Query 조합으로 서버 상태와 클라이언트 상태를 분리해서 쓰고 있어요."

⚠️ 주의사항

🔗 관련 용어

📚 더 배우기