React 요약 11 - 상태 관리 라이브러리
Updated:
Redux
// 1. 액션 타입 정의
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const DELETE_TODO = 'DELETE_TODO';
const SET_FILTER = 'SET_FILTER';
// 2. 액션 생성자
const addTodo = (text) => ({
type: ADD_TODO,
payload: {
id: Date.now(),
text,
completed: false
}
});
const toggleTodo = (id) => ({
type: TOGGLE_TODO,
payload: { id }
});
const deleteTodo = (id) => ({
type: DELETE_TODO,
payload: { id }
});
const setFilter = (filter) => ({
type: SET_FILTER,
payload: { filter }
});
// 3. 리듀서
const initialState = {
todos: [],
filter: 'all' // 'all', 'active', 'completed'
};
function todoReducer(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, action.payload]
};
case TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
)
};
case DELETE_TODO:
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload.id)
};
case SET_FILTER:
return {
...state,
filter: action.payload.filter
};
default:
return state;
}
}
// 4. 스토어 생성
import { createStore } from 'redux';
const store = createStore(todoReducer);
// 5. React 컴포넌트와 연결
import { Provider, useSelector, useDispatch } from 'react-redux';
function TodoApp() {
return (
<Provider store={store}>
<div className="todo-app">
<h1>Todo List</h1>
<AddTodoForm />
<FilterButtons />
<TodoList />
</div>
</Provider>
);
}
function AddTodoForm() {
const [text, setText] = useState('');
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
dispatch(addTodo(text));
setText('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add a new todo"
/>
<button type="submit">Add</button>
</form>
);
}
function FilterButtons() {
const filter = useSelector(state => state.filter);
const dispatch = useDispatch();
return (
<div className="filters">
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => dispatch(setFilter('all'))}
>
All
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => dispatch(setFilter('active'))}
>
Active
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => dispatch(setFilter('completed'))}
>
Completed
</button>
</div>
);
}
function TodoList() {
const { todos, filter } = useSelector(state => state);
const dispatch = useDispatch();
// 필터에 따라 할 일 목록 필터링
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true; // 'all'
});
return (
<ul className="todo-list">
{filteredTodos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<span onClick={() => dispatch(toggleTodo(todo.id))}>
{todo.text}
</span>
<button onClick={() => dispatch(deleteTodo(todo.id))}>
Delete
</button>
</li>
))}
</ul>
);
}
코멘트: Redux는 예측 가능한 상태 관리를 위한 인기 있는 라이브러리입니다. 주요 개념은 다음과 같습니다:
- 단일 스토어(Store): 애플리케이션의 전체 상태를 하나의 객체 트리에 저장
- 액션(Action): 상태 변경을 설명하는 일반 객체
- 리듀서(Reducer): 이전 상태와 액션을 받아 새 상태를 반환하는 순수 함수
- 디스패치(Dispatch): 액션을 스토어에 보내는 메서드
Redux는 다음과 같은 경우에 유용합니다:
- 여러 컴포넌트에서 공유되는 복잡한 상태
- 상태 변경 로직이 여러 곳에 분산된 경우
- 시간 여행 디버깅, 상태 지속성 등의 고급 기능이 필요한 경우
Redux Toolkit
import { createSlice, configureStore } from '@reduxjs/toolkit';
import { Provider, useSelector, useDispatch } from 'react-redux';
// 1. 슬라이스 생성 (액션 + 리듀서)
const todoSlice = createSlice({
name: 'todos',
initialState: {
items: [],
filter: 'all' // 'all', 'active', 'completed'
},
reducers: {
// 리듀서와 액션 생성자를 동시에 생성
addTodo: (state, action) => {
state.items.push({
id: Date.now(),
text: action.payload,
completed: false
});
},
toggleTodo: (state, action) => {
const todo = state.items.find(todo => todo.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
deleteTodo: (state, action) => {
state.items = state.items.filter(todo => todo.id !== action.payload);
},
setFilter: (state, action) => {
state.filter = action.payload;
}
}
});
// 2. 액션 생성자 내보내기
export const { addTodo, toggleTodo, deleteTodo, setFilter } = todoSlice.actions;
// 3. 스토어 생성
const store = configureStore({
reducer: todoSlice.reducer
});
// 4. React 컴포넌트
function TodoApp() {
return (
<Provider store={store}>
<div className="todo-app">
<h1>Todo List (Redux Toolkit)</h1>
<AddTodoForm />
<FilterButtons />
<TodoList />
</div>
</Provider>
);
}
function AddTodoForm() {
const [text, setText] = useState('');
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
dispatch(addTodo(text));
setText('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add a new todo"
/>
<button type="submit">Add</button>
</form>
);
}
function FilterButtons() {
const filter = useSelector(state => state.filter);
const dispatch = useDispatch();
return (
<div className="filters">
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => dispatch(setFilter('all'))}
>
All
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => dispatch(setFilter('active'))}
>
Active
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => dispatch(setFilter('completed'))}
>
Completed
</button>
</div>
);
}
function TodoList() {
const { items, filter } = useSelector(state => state);
const dispatch = useDispatch();
// 필터에 따라 할 일 목록 필터링
const filteredTodos = items.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true; // 'all'
});
return (
<ul className="todo-list">
{filteredTodos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<span onClick={() => dispatch(toggleTodo(todo.id))}>
{todo.text}
</span>
<button onClick={() => dispatch(deleteTodo(todo.id))}>
Delete
</button>
</li>
))}
</ul>
);
}
코멘트: Redux Toolkit은 Redux의 공식 권장 접근 방식으로, 보일러플레이트 코드를 줄이고 개발 경험을 개선합니다. 주요 기능은 다음과 같습니다:
- createSlice: 리듀서와 액션 생성자를 한 번에 생성
- Immer 통합: 불변성을 유지하면서도 “변경” 문법 사용 가능
- configureStore: 미들웨어와 개발자 도구 설정 간소화
- createAsyncThunk: 비동기 액션 처리 간소화
Redux Toolkit은 기존 Redux의 복잡성을 크게 줄이면서도 모든 이점을 유지합니다. 새 프로젝트에서는 일반 Redux보다 Redux Toolkit을 사용하는 것이 권장됩니다.
Zustand
import { create } from 'zustand';
// 1. 스토어 생성
const useTodoStore = create((set) => ({
// 초기 상태
todos: [],
filter: 'all', // 'all', 'active', 'completed'
// 액션
addTodo: (text) => set((state) => ({
todos: [...state.todos, {
id: Date.now(),
text,
completed: false
}]
})),
toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
})),
deleteTodo: (id) => set((state) => ({
todos: state.todos.filter(todo => todo.id !== id)
})),
setFilter: (filter) => set({ filter })
}));
// 2. React 컴포넌트
function TodoApp() {
return (
<div className="todo-app">
<h1>Todo List (Zustand)</h1>
<AddTodoForm />
<FilterButtons />
<TodoList />
</div>
);
}
function AddTodoForm() {
const [text, setText] = useState('');
const addTodo = useTodoStore((state) => state.addTodo);
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
addTodo(text);
setText('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add a new todo"
/>
<button type="submit">Add</button>
</form>
);
}
function FilterButtons() {
const filter = useTodoStore((state) => state.filter);
const setFilter = useTodoStore((state) => state.setFilter);
return (
<div className="filters">
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => setFilter('all')}
>
All
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => setFilter('active')}
>
Active
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => setFilter('completed')}
>
Completed
</button>
</div>
);
}
function TodoList() {
// 필요한 상태만 선택적으로 구독
const todos = useTodoStore((state) => state.todos);
const filter = useTodoStore((state) => state.filter);
const toggleTodo = useTodoStore((state) => state.toggleTodo);
const deleteTodo = useTodoStore((state) => state.deleteTodo);
// 필터에 따라 할 일 목록 필터링
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true; // 'all'
});
return (
<ul className="todo-list">
{filteredTodos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<span onClick={() => toggleTodo(todo.id)}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>
Delete
</button>
</li>
))}
</ul>
);
}
코멘트: Zustand는 간단하고 가벼운 상태 관리 라이브러리로, Redux의 복잡성 없이 유사한 기능을 제공합니다. 주요 특징은 다음과 같습니다:
- 간결한 API: 최소한의 보일러플레이트로 상태 관리
- Hook 기반: 컴포넌트에서
useTodoStore
와 같은 Hook으로 직접 상태에 접근- 선택적 구독: 필요한 상태만 선택적으로 구독하여 불필요한 리렌더링 방지
- 미들웨어 지원: Redux 개발자 도구, 지속성 등의 미들웨어 지원
Zustand는 작은 규모의 애플리케이션이나 Redux의 복잡성이 필요하지 않은 경우에 좋은 선택입니다. Context API보다 성능이 우수하면서도 사용이 간편합니다.
Jotai
import { atom, useAtom } from 'jotai';
// 1. 기본 아톰 정의
const todosAtom = atom([]);
const filterAtom = atom('all'); // 'all', 'active', 'completed'
// 2. 파생 아톰 (계산된 상태)
const filteredTodosAtom = atom((get) => {
const todos = get(todosAtom);
const filter = get(filterAtom);
if (filter === 'active') return todos.filter(todo => !todo.completed);
if (filter === 'completed') return todos.filter(todo => todo.completed);
return todos; // 'all'
});
// 3. 액션 아톰 (상태 업데이트 로직)
const addTodoAtom = atom(
null, // 읽기 값 없음
(get, set, text) => {
const todos = get(todosAtom);
set(todosAtom, [
...todos,
{
id: Date.now(),
text,
completed: false
}
]);
}
);
const toggleTodoAtom = atom(
null,
(get, set, id) => {
const todos = get(todosAtom);
set(todosAtom, todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
}
);
const deleteTodoAtom = atom(
null,
(get, set, id) => {
const todos = get(todosAtom);
set(todosAtom, todos.filter(todo => todo.id !== id));
}
);
// 4. React 컴포넌트
function TodoApp() {
return (
<div className="todo-app">
<h1>Todo List (Jotai)</h1>
<AddTodoForm />
<FilterButtons />
<TodoList />
</div>
);
}
function AddTodoForm() {
const [text, setText] = useState('');
const [, addTodo] = useAtom(addTodoAtom);
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
addTodo(text);
setText('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add a new todo"
/>
<button type="submit">Add</button>
</form>
);
}
function FilterButtons() {
const [filter, setFilter] = useAtom(filterAtom);
return (
<div className="filters">
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => setFilter('all')}
>
All
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => setFilter('active')}
>
Active
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => setFilter('completed')}
>
Completed
</button>
</div>
);
}
function TodoList() {
// 필터링된 할 일 목록 구독
const [filteredTodos] = useAtom(filteredTodosAtom);
const [, toggleTodo] = useAtom(toggleTodoAtom);
const [, deleteTodo] = useAtom(deleteTodoAtom);
return (
<ul className="todo-list">
{filteredTodos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<span onClick={() => toggleTodo(todo.id)}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>
Delete
</button>
</li>
))}
</ul>
);
}
코멘트: Jotai는 React를 위한 원자적(atomic) 상태 관리 라이브러리로, 상향식(bottom-up) 접근 방식을 취합니다. 주요 특징은 다음과 같습니다:
- 원자(Atom): 작은 상태 단위로, 다른 원자에서 파생될 수 있음
- 상향식 접근: 작은 상태 조각에서 시작하여 필요에 따라 조합
- React Suspense 지원: 비동기 상태 처리를 위한 내장 지원
- 최소한의 리렌더링: 사용된 원자가 변경될 때만 컴포넌트 리렌더링
Jotai는 특히 다음과 같은 경우에 적합합니다:
- 전역 상태와 지역 상태 간의 경계가 모호한 경우
- 상태 간의 의존성이 복잡한 경우
- React Suspense와 함께 사용하는 경우
- 리렌더링 최적화가 중요한 경우
Recoil
import { RecoilRoot, atom, selector, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
// 1. 아톰 정의 (기본 상태)
const todosAtom = atom({
key: 'todosState', // 고유 키
default: [] // 기본값
});
const filterAtom = atom({
key: 'todoFilterState',
default: 'all' // 'all', 'active', 'completed'
});
// 2. 선택자 정의 (파생 상태)
const filteredTodosSelector = selector({
key: 'filteredTodosState',
get: ({ get }) => {
const todos = get(todosAtom);
const filter = get(filterAtom);
if (filter === 'active') return todos.filter(todo => !todo.completed);
if (filter === 'completed') return todos.filter(todo => todo.completed);
return todos; // 'all'
}
});
const todoStatsSelector = selector({
key: 'todoStatsState',
get: ({ get }) => {
const todos = get(todosAtom);
const totalNum = todos.length;
const completedNum = todos.filter(todo => todo.completed).length;
const uncompletedNum = totalNum - completedNum;
const percentCompleted = totalNum === 0 ? 0 : Math.round((completedNum / totalNum) * 100);
return {
totalNum,
completedNum,
uncompletedNum,
percentCompleted
};
}
});
// 3. React 컴포넌트
function TodoApp() {
return (
<RecoilRoot>
<div className="todo-app">
<h1>Todo List (Recoil)</h1>
<TodoStats />
<AddTodoForm />
<FilterButtons />
<TodoList />
</div>
</RecoilRoot>
);
}
function TodoStats() {
const { totalNum, completedNum, uncompletedNum, percentCompleted } = useRecoilValue(todoStatsSelector);
return (
<div className="todo-stats">
<p>Total: {totalNum}</p>
<p>Completed: {completedNum}</p>
<p>Uncompleted: {uncompletedNum}</p>
<p>Percent Completed: {percentCompleted}%</p>
</div>
);
}
function AddTodoForm() {
const [text, setText] = useState('');
const setTodos = useSetRecoilState(todosAtom);
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
setTodos(oldTodos => [
...oldTodos,
{
id: Date.now(),
text,
completed: false
}
]);
setText('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add a new todo"
/>
<button type="submit">Add</button>
</form>
);
}
function FilterButtons() {
const [filter, setFilter] = useRecoilState(filterAtom);
return (
<div className="filters">
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => setFilter('all')}
>
All
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => setFilter('active')}
>
Active
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => setFilter('completed')}
>
Completed
</button>
</div>
);
}
function TodoList() {
// 필터링된 할 일 목록 구독
const filteredTodos = useRecoilValue(filteredTodosSelector);
const setTodos = useSetRecoilState(todosAtom);
const toggleTodo = (id) => {
setTodos(oldTodos =>
oldTodos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const deleteTodo = (id) => {
setTodos(oldTodos => oldTodos.filter(todo => todo.id !== id));
};
return (
<ul className="todo-list">
{filteredTodos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<span onClick={() => toggleTodo(todo.id)}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>
Delete
</button>
</li>
))}
</ul>
);
}
코멘트: Recoil은 Facebook에서 개발한 상태 관리 라이브러리로, React의 작동 방식과 잘 맞도록 설계되었습니다. 주요 특징은 다음과 같습니다:
- 아톰(Atom): 상태의 기본 단위, 컴포넌트가 구독할 수 있음
- 선택자(Selector): 아톰이나 다른 선택자에서 파생된 상태
- 비동기 지원: 비동기 데이터를 쉽게 처리할 수 있는 기능
- React Suspense 통합: 비동기 상태 로딩을 위한 내장 지원
Recoil은 특히 다음과 같은 경우에 적합합니다:
- 복잡한 상태 의존성이 있는 경우
- 비동기 상태 업데이트가 많은 경우
- React의 Concurrent Mode와 함께 사용하는 경우
- 상태 간의 관계를 명확하게 모델링해야 하는 경우
Leave a comment