Zustand vs Jotai React 상태관리 라이브러리 완벽 비교
React 프로젝트에서 Zustand와 Jotai 상태관리 라이브러리의 차이점을 실전 코드 예제와 함께 상세히 비교 분석합니다.
Zustand vs Jotai React 상태관리 라이브러리 완벽 비교
React 생태계에서 상태관리는 항상 중요한 고민거리입니다. Redux의 복잡함을 피하고자 하는 개발자들이 늘어나면서, 경량화된 상태관리 라이브러리인 Zustand와 Jotai가 주목받고 있습니다. 이 글에서는 두 라이브러리의 핵심 차이점과 실제 사용 시나리오를 코드 예제와 함께 상세히 비교해보겠습니다.
Zustand와 Jotai 개요
Zustand는 독일어로 "상태"를 의미하며, 간단하고 직관적인 API로 전역 상태를 관리할 수 있는 라이브러리입니다. 반면 Jotai는 일본어로 "상태"를 뜻하며, 원자적(atomic) 접근 방식을 통해 상태를 관리합니다.
두 라이브러리 모두 Redux의 복잡성을 해결하고자 만들어졌지만, 접근 방식에서 근본적인 차이를 보입니다.
기본 설치 및 설정
먼저 각 라이브러리의 설치 과정을 살펴보겠습니다.
# Zustand 설치
npm install zustand
# Jotai 설치
npm install jotai
Zustand 기본 사용법
Zustand는 스토어 기반의 접근 방식을 사용합니다. 하나의 중앙 집중식 스토어에서 모든 상태를 관리합니다.
// store/useCounterStore.ts
import { create } from 'zustand'
interface CounterState {
count: number
increment: () => void
decrement: () => void
reset: () => void
}
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}))
// components/Counter.tsx
import { useCounterStore } from '../store/useCounterStore'
export function Counter() {
const { count, increment, decrement, reset } = useCounterStore()
return (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
)
}
Jotai 기본 사용법
Jotai는 원자(atom) 기반의 접근 방식을 사용합니다. 각 상태를 독립적인 원자로 관리합니다.
// atoms/counterAtom.ts
import { atom } from 'jotai'
export const countAtom = atom(0)
export const incrementAtom = atom(
null,
(get, set) => set(countAtom, get(countAtom) + 1)
)
export const decrementAtom = atom(
null,
(get, set) => set(countAtom, get(countAtom) - 1)
)
export const resetAtom = atom(
null,
(get, set) => set(countAtom, 0)
)
// components/Counter.tsx
import { useAtom } from 'jotai'
import { countAtom, incrementAtom, decrementAtom, resetAtom } from '../atoms/counterAtom'
export function Counter() {
const [count] = useAtom(countAtom)
const [, increment] = useAtom(incrementAtom)
const [, decrement] = useAtom(decrementAtom)
const [, reset] = useAtom(resetAtom)
return (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
)
}
복잡한 상태 관리 비교
실제 애플리케이션에서는 더 복잡한 상태 관리가 필요합니다. 사용자 정보와 할 일 목록을 관리하는 예제를 통해 비교해보겠습니다.
Zustand 복잡한 상태 관리
// store/useAppStore.ts
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
interface User {
id: string
name: string
email: string
}
interface Todo {
id: string
text: string
completed: boolean
}
interface AppState {
user: User | null
todos: Todo[]
setUser: (user: User) => void
addTodo: (text: string) => void
toggleTodo: (id: string) => void
removeTodo: (id: string) => void
clearCompleted: () => void
}
export const useAppStore = create<AppState>()(
devtools(
persist(
(set, get) => ({
user: null,
todos: [],
setUser: (user) => set({ user }),
addTodo: (text) => {
const newTodo: Todo = {
id: Date.now().toString(),
text,
completed: false,
}
set((state) => ({ todos: [...state.todos, newTodo] }))
},
toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
})),
removeTodo: (id) => set((state) => ({
todos: state.todos.filter(todo => todo.id !== id)
})),
clearCompleted: () => set((state) => ({
todos: state.todos.filter(todo => !todo.completed)
})),
}),
{ name: 'app-storage' }
)
)
)Jotai 복잡한 상태 관리
// atoms/appAtoms.ts
import { atom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
interface User {
id: string
name: string
email: string
}
interface Todo {
id: string
text: string
completed: boolean
}
export const userAtom = atomWithStorage<User | null>('user', null)
export const todosAtom = atomWithStorage<Todo[]>('todos', [])
export const addTodoAtom = atom(
null,
(get, set, text: string) => {
const newTodo: Todo = {
id: Date.now().toString(),
text,
completed: false,
}
set(todosAtom, [...get(todosAtom), newTodo])
}
)
export const toggleTodoAtom = atom(
null,
(get, set, id: string) => {
const todos = get(todosAtom)
set(todosAtom, todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
))
}
)
export const removeTodoAtom = atom(
null,
(get, set, id: string) => {
const todos = get(todosAtom)
set(todosAtom, todos.filter(todo => todo.id !== id))
}
)
export const completedTodosAtom = atom((get) =>
get(todosAtom).filter(todo => todo.completed)
)
export const activeTodosAtom = atom((get) =>
get(todosAtom).filter(todo => !todo.completed)
)
성능 최적화 비교
Zustand의 선택적 구독
// 특정 상태만 구독하여 불필요한 리렌더링 방지
function TodoCount() {
const todoCount = useAppStore(state => state.todos.length)
return <span>총 {todoCount}개의 할 일</span>
}
function CompletedCount() {
const completedCount = useAppStore(
state => state.todos.filter(todo => todo.completed).length
)
return <span>완료된 할 일: {completedCount}개</span>
}
Jotai의 원자적 구독
// 각 원자는 독립적으로 구독되어 자동으로 최적화됨
function TodoCount() {
const [todos] = useAtom(todosAtom)
return <span>총 {todos.length}개의 할 일</span>
}
function CompletedCount() {
const [completedTodos] = useAtom(completedTodosAtom)
return <span>완료된 할 일: {completedTodos.length}개</span>
}
TypeScript 지원 비교
두 라이브러리 모두 뛰어난 TypeScript 지원을 제공합니다.
| 특징 | Zustand | Jotai |
|---|---|---|
| 타입 추론 | 우수 | 우수 |
| 제네릭 지원 | 완전 지원 | 완전 지원 |
| 타입 안전성 | 높음 | 높음 |
| IDE 지원 | 우수 | 우수 |
Next.js와의 통합
Zustand + Next.js
// pages/_app.tsx
import { useAppStore } from '../store/useAppStore'
function MyApp({ Component, pageProps }) {
// Zustand는 별도 Provider 불필요
return <Component {...pageProps} />
}
// SSR 지원
export async function getServerSideProps() {
const initialState = await fetchInitialData()
return {
props: {
initialState
}
}
}
Jotai + Next.js
// pages/_app.tsx
import { Provider } from 'jotai'
function MyApp({ Component, pageProps }) {
return (
<Provider>
<Component {...pageProps} />
</Provider>
)
}
// SSR 지원을 위한 하이드레이션
import { useHydrateAtoms } from 'jotai/utils'
function HydrateAtoms({ initialValues, children }) {
useHydrateAtoms(initialValues)
return children
}
언제 어떤 라이브러리를 선택해야 할까?
Zustand를 선택해야 하는 경우
- Redux와 유사한 중앙 집중식 상태 관리를 원하는 경우
- 간단한 API로 빠르게 개발하고 싶은 경우
- 기존 Redux 코드를 점진적으로 마이그레이션하려는 경우
- 미들웨어(devtools, persist 등)를 적극 활용하고 싶은 경우
Jotai를 선택해야 하는 경우
- 세밀한 상태 관리와 최적화가 필요한 경우
- 컴포넌트별로 독립적인 상태를 관리하고 싶은 경우
- 복잡한 상태 의존성 관계를 다뤄야 하는 경우
- 함수형 프로그래밍 패러다임을 선호하는 경우
마무리
Zustand와 Jotai는 각각 다른 철학과 접근 방식을 가진 우수한 상태관리 라이브러리입니다. Zustand는 간단함과 직관성을, Jotai는 세밀한 제어와 최적화를 강점으로 합니다. 프로젝트의 규모, 팀의 선호도, 성능 요구사항을 종합적으로 고려하여 적절한 라이브러리를 선택하시기 바랍니다. 두 라이브러리 모두 현대적인 React 개발에서 Redux의 훌륭한 대안이 될 수 있습니다.
관련 게시글
Vite Build Tool: Fast Frontend Development Guide
Vite는 현대적인 프론트엔드 개발을 위한 빠르고 효율적인 빌드 도구입니다. 이 가이드에서는 Vite의 핵심 기능, React 및 TypeScript 프로젝트 설정, 플러그인 활용법, 그리고 빌드 최적화 전략까지 완벽하게 다룹니다.
React Server Components (RSC) 심층 가이드: Next.js와 함께하는 Full-stack React
React Server Components (RSC)의 개념, 등장 배경, 동작 원리, 그리고 Next.js 13+ App Router에서의 활용법을 심층적으로 다룹니다. 클라이언트/서버 컴포넌트 분리 전략과 실전 코드 예제를 통해 RSC의 강력한 이점을 이해하고 웹 애플리케이션 성능을 최적화하는 방법을 알아봅니다.
Next.js Middleware: 강력한 요청 처리 활용법
Next.js Middleware를 활용하여 사용자 인증, 국제화, A/B 테스트 등 다양한 요청 처리 로직을 효율적으로 구현하는 방법을 심층적으로 알아봅니다. 실전 코드 예제를 통해 Next.js 애플리케이션의 프론트엔드 기능을 강화하세요.