리액트(React)는 컴포넌트 기반 UI 라이브러리로, 컴포넌트 내부 상태(state)를 다루는 기능(useState, useReducer 등)을 기본 제공합니다. 하지만 애플리케이션 규모가 커질수록, 컴포넌트 간 데이터를 효율적으로 공유하고 복잡도를 관리하기 위해 별도의 상태 관리 라이브러리를 도입하는 것이 일반적입니다.
이 글에서는 상태 관리의 기초 개념부터 언제, 왜, 어떻게 라이브러리를 쓰는지, 주요 원리와 종류, 마지막으로 Redux를 활용한 실전 예제까지 하나씩 살펴보겠습니다.
store: 중앙 저장소 slice/reducer: 상태 모듈화 Provider: React와 연결 useSelector, useDispatch) createAsyncThunk) const [count, setCount] = useState(0);
const [text, setText] = useState("");컴포넌트 간 데이터 공유가 필요할 때
props로 매번 전달하면 깊이가 깊어질수록 코드 복잡도↑ (prop drilling 문제)전역 또는 공통으로 사용되어야 할 데이터가 있을 때
복잡한 상태 로직을 모듈화/테스트하기 위해
useState만으로는 액션 흐름을 추적하기 어려움 비동기 작업 관리
| 라이브러리 | 특징 | 장단점 |
|---|---|---|
| Redux | Flux 패턴 기반, 큰 커뮤니티, 강력한 에코시스템 | ✅ 예측 가능성, DevTools ❌ 보일러플레이트 코드 |
| Zustand | 간결한 API, 작은 번들 크기 | ✅ 직관적, 러닝 커브 낮음 ❌ 커뮤니티·에코시스템 상대적 작음 |
| Recoil | Facebook 개발, atom/selector 개념 | ✅ React 전용, 성능 최적화 ❌ 아직 발전 중 |
| Jotai | 단일 원자(atom) 모델, 동시성 지원 | ✅ 간단, 모던 API ❌ 러닝 커브 약간 있음 |
| MobX | 관찰(observable) 기반, 자동 리렌더링 | ✅ 코드 간결, 반응형 ❌ 복잡한 디버깅 |
UI ──▶ Action ──▶ Reducer ──▶ Store ──▶ UI
// src/app/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
// user: userReducer,
// todo: todoReducer,
},
});
// src/features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = { value: 0 };
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => { state.value += 1; }, // Immer 덕분에 직접 수정처럼 보이지만, 불변성 유지
decrement: (state) => { state.value -= 1; },
incrementByAmount: (state, action) => { state.value += action.payload; },
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux';
import { store } from './app/store';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
const count = useSelector((state) => state.counter.value);const dispatch = useDispatch();
dispatch(increment());
dispatch(incrementByAmount(5));// features/todo/todoSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchTodos = createAsyncThunk(
'todos/fetchTodos',
async () => {
const res = await fetch('/api/todos');
return res.json();
}
);
const todoSlice = createSlice({
name: 'todos',
initialState: { items: [], status: 'idle', error: null },
reducers: { /* 동기 액션 */ },
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => { state.status = 'loading'; })
.addCase(fetchTodos.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
},
});
export default todoSlice.reducer;