Jotai
조타이 (일본어로 '상태')
Jotai는 원자(atom) 기반의 React 상태 관리 라이브러리입니다. Recoil에서 영감을 받아 더 간단한 API와 작은 번들 크기로 설계되었으며, 보일러플레이트 없이 세밀한 상태 관리가 가능합니다.
조타이 (일본어로 '상태')
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 }]);
}
}
);
💬 상황: 새 프로젝트의 상태 관리 라이브러리 선택 논의