Redux vs Recoil vs Zustand
작성일 : 2025.01.19
React 생태계에서는 다양한 상태관리 라이브러리들이 경쟁하고 있는데, 가장 많이 사용되는 Redux, Recoil, Zustand를 비교해보려고 합니다.
Redux: 견고한 아키텍처의 대명사
Redux는 React 생태계에서 가장 오랫동안 사랑받아온 상태 관리 라이브러리입니다. 예측 가능한 상태 흐름과 강력한 개발자 도구를 제공하는 것이 특징입니다.
Redux의 주요 특징
-
중앙 집중식 스토어
- 단일 스토어에서 모든 상태 관리
- 예측 가능한 단방향 데이터 흐름
- 시간 여행 디버깅 가능
-
강력한 미들웨어 시스템
- redux-saga, redux-thunk 등 다양한 미들웨어
- 비동기 작업 처리를 위한 풍부한 생태계
- 로깅, 라우팅 등 다양한 미들웨어 지원
-
DevTools 지원
- 강력한 개발자 도구 제공
- 상태 변화 추적 용이
- 디버깅이 쉬움
-
큰 커뮤니티
- 풍부한 레퍼런스와 문서
- 안정적인 생태계
- 많은 기업에서 사용중
기본 사용 예시
// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
비동기 작업 예시
// userSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
);
const userSlice = createSlice({
name: 'user',
initialState: {
entities: {},
loading: 'idle',
error: null
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUserById.pending, (state) => {
state.loading = 'pending';
})
.addCase(fetchUserById.fulfilled, (state, action) => {
state.loading = 'idle';
state.entities[action.payload.id] = action.payload;
})
.addCase(fetchUserById.rejected, (state, action) => {
state.loading = 'idle';
state.error = action.error;
});
},
});
실제 애플리케이션 예시
// store.js
import { configureStore } from '@reduxjs/toolkit';
import logger from 'redux-logger';
import userReducer from './userSlice';
import cartReducer from './cartSlice';
export const store = configureStore({
reducer: {
user: userReducer,
cart: cartReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(logger),
});
// CartComponent.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addItem, removeItem, updateQuantity } from './cartSlice';
function CartComponent() {
const dispatch = useDispatch();
const { items, total } = useSelector((state) => state.cart);
const { user } = useSelector((state) => state.user);
return (
<div>
<h2>Welcome, {user.name}</h2>
<div className="cart-items">
{items.map((item) => (
<div key={item.id} className="cart-item">
<span>{item.name}</span>
<span>${item.price}</span>
<input
type="number"
value={item.quantity}
onChange={(e) =>
dispatch(updateQuantity({
id: item.id,
quantity: parseInt(e.target.value)
}))
}
/>
<button onClick={() => dispatch(removeItem(item.id))}>
Remove
</button>
</div>
))}
</div>
<div className="cart-total">
Total: ${total}
</div>
</div>
);
}
Redux vs 다른 라이브러리들
-
vs Recoil
- Redux는 더 체계적인 상태 관리
- 더 큰 커뮤니티와 생태계
- 더 많은 보일러플레이트 코드
-
vs Zustand
- Redux는 더 엄격한 패턴 제공
- 더 강력한 개발자 도구
- 더 큰 번들 사이즈
-
vs Jotai
- Redux는 중앙 집중식 상태 관리
- 더 복잡한 설정 필요
- 더 체계적인 상태 관리
언제 Redux를 사용하면 좋을까?
- 대규모 애플리케이션에서 체계적인 상태 관리가 필요할 때
- 복잡한 상태 로직과 비동기 작업이 많을 때
- 팀 규모가 크고 일관된 코드 패턴이 필요할 때
- 엄격한 아키텍처가 요구되는 프로젝트
- 디버깅과 상태 추 적이 중요한 경우
Redux는 확실한 패턴과 규칙을 제공하여 큰 규모의 프로젝트에서 안정적인 상태 관리를 가능하게 합니다. 특히 많은 개발자가 참여하는 프로젝트에서 그 진가를 발휘하며, 강력한 개발자 도구와 미들웨어 생태계는 복잡한 애플리케이션 개발에 큰 도움이 됩니다.
Recoil: React를 위한 새로운 패러다임
Recoil은 Facebook에서 개발한 상태 관리 라이브러리로, React의 철학과 가장 잘 어울리는 접근 방식을 제공합니다. atom과 selector를 통한 상태 관리가 특징입니다.
Recoil의 주요 특징
-
Atom 기반 상태 관리
- 가장 작은 상태 단위인 atom
- 컴포넌트 간 상태 공유 용이
- 상태의 모듈화가 쉬움
-
Selector를 통한 파생 상태
- 상태 간의 의존성 관리
- 비동기 데이터 처리 용이
- 계산된 값의 캐싱 지원
-
React 친화적 설계
- React Concurrent Mode 지원
- Suspense 통합
- Code Splitting 최적화
-
동시성 모드 지원
- 비동기 상태 처리에 최적화
- 비동기 셀렉터 지원
- 상태 충돌 방지
기본 사용 예시
import { atom, useRecoilState, selector, useRecoilValue } from 'recoil';
// 기본 atom 정의
const counterState = atom({
key: 'counterState',
default: 0,
});
// selector를 통한 파생 상태
const doubledState = selector({
key: 'doubledState',
get: ({get}) => {
const count = get(counterState);
return count * 2;
},
});
function Counter() {
const [count, setCount] = useRecoilState(counterState);
const doubledCount = useRecoilValue(doubledState);
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubledCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
비동기 데 이터 처리 예시
import { selector, selectorFamily, useRecoilValue } from 'recoil';
const userQuery = selectorFamily({
key: 'UserQuery',
get: (userId) => async () => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
});
const userStatsQuery = selector({
key: 'UserStatsQuery',
get: async ({get}) => {
const response = await fetch('/api/user-stats');
return response.json();
},
});
function UserProfile({userId}) {
const user = useRecoilValue(userQuery(userId));
const stats = useRecoilValue(userStatsQuery);
return (
<div>
<h2>{user.name}</h2>
<p>Posts: {stats.totalPosts}</p>
<p>Following: {stats.following}</p>
</div>
);
}
실제 애플리케이션 예시
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';
// 상태 정의
const todoListState = atom({
key: 'todoListState',
default: [],
});
const todoListFilterState = atom({
key: 'todoListFilterState',
default: 'Show All',
});
const filteredTodoListState = selector({
key: 'filteredTodoListState',
get: ({get}) => {
const list = get(todoListState);
const filter = get(todoListFilterState);
switch (filter) {
case 'Show Completed':
return list.filter((item) => item.isComplete);
case 'Show Uncompleted':
return list.filter((item) => !item.isComplete);
default:
return list;
}
},
});
function TodoList() {
const [todoList, setTodoList] = useRecoilState(todoListState);
const [filter, setFilter] = useRecoilState(todoListFilterState);
const filteredList = useRecoilValue(filteredTodoListState);
const addItem = (text) => {
setTodoList((oldList) => [
...oldList,
{
id: getId(),
text,
isComplete: false,
},
]);
};
return (
<div>
<TodoListStats />
<TodoListFilters
filter={filter}
setFilter={setFilter}
/>
<TodoItemCreator addItem={addItem} />
{filteredList.map((item) => (
<TodoItem key={item.id} item={item} />
))}
</div>
);
}
Recoil vs 다른 라이브러리들
-
vs Redux
- 더 적은 보일러플레이트
- React에 더 최적화된 API
- 비동기 처리가 더 간단
-
vs Zustand
- 더 세밀한 상태 관리
- Suspense 지원
- React 생태계와 더 긴밀한 통합
-
vs Jotai
- 더 많은 기능 제공
- Facebook의 공식 지원
- 더 큰 번들 사이즈
언제 Recoil을 사용하면 좋을까?
- React의 최신 기능을 최대한 활용하고 싶을 때
- 비동기 상태 관리가 많은 프로젝트
- 상태 간 의존성이 복잡한 경우
- Code Splitting이 중요한 프로젝트
- Concurrent Mode를 활용하고 싶은 경우
Recoil은 React의 철학을 깊이 이해하고 이를 상태 관리에 반영한 현대적인 라이브러리입니다. 특히 비동기 데이터 처리와 상태 의존성 관리가 중요한 프로젝트에서 큰 강점을 발휘하며, React의 최신 기능들과 잘 통합되어 있습니다.
Zustand: 심플함의 미학
Zustand는 미니멀리즘을 추구하는 상태 관리 라이브러리입니다. "단순하고 확장 가능한" 이라는 모토처럼, 최소한의 코드로 강력한 상태 관리 기능을 제공합니다.
Zustand의 주요 특징
-
단순한 설정
- Provider 없이 사용 가능
- 최소한의 보일러플레이트
- 직관적인 API
-
강력한 성능
- 작은 번들 사이즈 (~1KB)
- 불필요한 리렌더링 방지
- 메모리 효율적인 구현
-
유연한 확장성
- 미들웨어 시스템 지원
- DevTools 통합
- TypeScript 지원
-
편리한 상태 관리
- 훅 기반의 API
- 자동 상태 병합
- 선택적 상태 구독
기본 사용 예시
import create from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
function Counter() {
const { count, increment, decrement, reset } = useStore();
return (
<div>
<h1>{count}</h1>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>Reset</button>
</div>
);
}
미들웨어 사용 예시
import create from 'zustand';
import { persist, devtools } from 'zustand/middleware';
const useStore = create(
devtools(
persist(
(set) => ({
theme: 'light',
toggleTheme: () =>
set((state) => ({
theme: state.theme === 'light' ? 'dark' : 'light',
})),
}),
{
name: 'theme-storage',
getStorage: () => localStorage,
}
)
)
);
실제 애플리케이션 예시
import create from 'zustand';
import { devtools } from 'zustand/middleware';
interface User {
id: string;
name: string;
email: string;
}
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface AppState {
// 사용자 상태
user: User | null;
setUser: (user: User | null) => void;
// 장바구니 상태
cart: CartItem[];
addToCart: (item: Omit<CartItem, 'quantity'>) => void;
removeFromCart: (itemId: string) => void;
updateQuantity: (itemId: string, quantity: number) => void;
// 계산된 값
cartTotal: () => number;
}
const useStore = create<AppState>()(
devtools((set, get) => ({
// 사용자 상태 관리
user: null,
setUser: (user) => set({ user }),
// 장바구니 상태 관리
cart: [],
addToCart: (item) =>
set((state) => {
const existingItem = state.cart.find((i) => i.id === item.id);
if (existingItem) {
return {
cart: state.cart.map((i) =>
i.id === item.id
? { ...i, quantity: i.quantity + 1 }
: i
),
};
}
return {
cart: [...state.cart, { ...item, quantity: 1 }],
};
}),
removeFromCart: (itemId) =>
set((state) => ({
cart: state.cart.filter((item) => item.id !== itemId),
})),
updateQuantity: (itemId, quantity) =>
set((state) => ({
cart: state.cart.map((item) =>
item.id === itemId
? { ...item, quantity }
: item
),
})),
// 계산된 값
cartTotal: () => {
const cart = get().cart;
return cart.reduce(
(total, item) => total + item.price * item.quantity,
0
);
},
}))
);
// 사용 예시
function ShoppingCart() {
const { cart, cartTotal, removeFromCart, updateQuantity } = useStore();
return (
<div>
{cart.map((item) => (
<div key={item.id}>
<span>{item.name}</span>
<input
type="number"
value={item.quantity}
onChange={(e) =>
updateQuantity(item.id, parseInt(e.target.value, 10))
}
/>
<button onClick={() => removeFromCart(item.id)}>
Remove
</button>
</div>
))}
<div>Total: ${cartTotal()}</div>
</div>
);
}
Zustand vs 다른 라이브러리들
-
vs Redux
- 더 적은 보일러플레이트
- 더 간단한 설정
- 더 작은 번들 사이즈
-
vs Recoil
- Provider 불필요
- 더 단순한 API
- 더 빠른 학습 곡선
-
vs Jotai
- 중앙 집중식 상태 관리
- 더 명확한 상태 구조
- 더 간단한 API
언제 Zustand를 사용하면 좋을까?
- 빠른 개발과 간단한 설정이 필요할 때
- 작은 규모에서 중간 규모의 프로젝트
- 성능과 번들 사이즈가 중요한 경우
- Redux의 보일러플레이트를 피하고 싶을 때
- TypeScript와 함께 사용하고 싶은 경우
Zustand는 단순함과 유연성을 동시에 제공하는 현대적인 상태 관리 라이브러리입니다. 특히 작은 프로젝트부터 중간 규모의 프로젝트까지 빠르게 개발하면서도 안정적인 상태 관리가 필요한 경우에 탁월한 선택이 될 수 있습니다.
Jotai: React를 위한 원시적이고 유연한 상태 관리 라이브러리
Jotai는 Recoil에서 영감을 받았지만, 더 단순하고 원시적인 접근 방식을 취하는 상태 관리 라이브러리입니다. "primitive and flexible state management for React"라는 슬로건처럼, 매우 미니멀하면서도 강력한 기능을 제공합니다.
Jotai의 주요 특징
-
Bottom-up 접근방식
- 작은 단위(atom)에서 시작하여 상태를 구성
- 모듈러하고 재사용 가능한 상태 관리 가능
-
TypeScript 우선 설계
- 처음부터 TypeScript를 고려하여 설계됨
- 뛰어난 타입 추론과 타입 안정성
-
작은 번들 사이즈
- 기본 번들 크기가 약 2.4kB로 매우 작음
-
Provider 선택적 사용
- Provider 없이도 사용 가능
- 필요한 경우에만 Provider 사용
기본 사용 예시
import { atom, useAtom } from 'jotai'
// 기본 atom 생성
const countAtom = atom(0)
const doubleAtom = atom((get) => get(countAtom) * 2)
// 읽기/쓰기 가능한 파생 atom
const countryAtom = atom('Japan')
const citiesAtom = atom(
(get) => getCitiesOf(get(countryAtom)),
(get, set, newCity) => {
const country = get(countryAtom)
if (!getCitiesOf(country).includes(newCity)) {
set(countryAtom, findCountryOf(newCity))
}
}
)
// 컴포넌트에서 사용
function Counter() {
const [count, setCount] = useAtom(countAtom)
const [double] = useAtom(doubleAtom)
return (
<div>
Count: {count}
Double: {double}
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
)
}
고급 사용 예시
import { atom, useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
import { focusAtom } from 'jotai/optics'
// localStorage와 연동
const themeAtom = atomWithStorage('theme', 'light')
// 비동기 atom
const userAtom = atom(async (get) => {
const response = await fetch('/api/user')
return response.json()
})
// 중첩된 객체의 특정 필드에 대한 atom
const formAtom = atom({
name: '',
age: 0,
address: {
street: '',
city: '',
}
})
// Optics를 사용한 중첩 객체 업데이트
const streetAtom = focusAtom(formAtom, (optic) => optic.prop('address').prop('street'))
function UserProfile() {
const [user] = useAtom(userAtom)
const [theme, setTheme] = useAtom(themeAtom)
const [street, setStreet] = useAtom(streetAtom)
return (
<div>
<div>User: {user.name}</div>
<div>Theme: {theme}</div>
<input
value={street}
onChange={(e) => setStreet(e.target.value)}
/>
</div>
)
}
실제 애플리케이션 예시
import { atom, useAtom } from 'jotai'
import { atomWithQuery } from 'jotai/query'
// 쇼핑몰 장바구니 구현
interface CartItem {
id: number
name: string
price: number
quantity: number
}
const cartAtom = atom<CartItem[]>([])
const cartTotalAtom = atom((get) => {
const cart = get(cartAtom)
return cart.reduce((total, item) => total + item.price * item.quantity, 0)
})
// React Query와 통합
const productsAtom = atomWithQuery((get) => ({
queryKey: ['products'],
queryFn: async () => {
const response = await fetch('/api/products')
return response.json()
},
}))
function ShoppingCart() {
const [cart, setCart] = useAtom(cartAtom)
const [total] = useAtom(cartTotalAtom)
const [products] = useAtom(productsAtom)
const addToCart = (product) => {
setCart(prev => {
const existing = prev.find(item => item.id === product.id)
if (existing) {
return prev.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
)
}
return [...prev, { ...product, quantity: 1 }]
})
}
return (
<div>
<h2>Products</h2>
{products.map(product => (
<div key={product.id}>
<span>{product.name}</span>
<button onClick={() => addToCart(product)}>Add to Cart</button>
</div>
))}
<h2>Cart</h2>
{cart.map(item => (
<div key={item.id}>
{item.name} x {item.quantity}
</div>
))}
<div>Total: ${total}</div>
</div>
)
}
Jotai vs 다른 라이브러리들
-
vs Recoil
- Jotai는 더 작고 단순한 API를 제공
- Provider가 선택적
- 더 작은 번들 사이즈
-
vs Zustand
- Jotai는 더 React스러운 접근방식
- atom 기반의 세분화된 상태 관리
- 더 나은 코드 분할 지원
-
vs Redux
- 훨씬 적은 보일러플레이트
- 더 유연한 상태 구성
- 더 나은 성능 (필요한 부분만 리렌더링)
언제 Jotai를 사용하면 좋을까?
- React와 긴밀하게 통합된 상태 관리가 필요할 때
- 작은 단위의 상태를 효율적으로 관리하고 싶을 때
- TypeScript 프로젝트에서 타입 안정성이 중요할 때
- 성능 최적화가 중요한 프로젝트에서
- 코드 분할이 필요한 대규모 애플리케이션에서
Jotai는 React의 기본 원칙을 따르면서도 강력한 상태 관리 기능을 제공하는 현대적인 라이브러리입니다. 특히 TypeScript 프로젝트에서 그 진가를 발휘하며, 작은 번들 사이즈와 뛰어난 성능으로 많은 개발자들의 선택을 받고 있습니다.