리액트 상태 관리 심화 #5 Redux Toolkit과 레거시 컨텍스트
지금까지 다룬 Zustand와 Jotai는 비교적 최근의 가벼운 도구들입니다. 이번 글의 Redux는 그 이전 시대를 지배한 도구입니다. 한동안 “리액트 전역 상태 = Redux"가 사실상 공식이었고, 그 시절에 만들어진 수많은 코드베이스에 지금도 남아 있습니다. 그래서 새로 쓸 일이 줄었더라도 읽고 유지보수할 일은 여전히 많습니다.
이 글은 옛 Redux의 장황한 코드가 아니라, 현재 공식 권장 방식인 Redux Toolkit(RTK) 기준으로 정리합니다.
Redux의 핵심 개념 #
Redux는 세 가지 규칙 위에 서 있습니다. 이 모델 자체는 리액트 빌트인 useReducer와 같은 뿌리입니다.
- 단일 스토어 — 앱의 상태가 하나의 큰 객체에 모입니다.
- 액션으로만 변경 — 상태를 직접 바꾸지 않고, “무슨 일이 일어났는지"를 기술한 액션을 디스패치합니다.
- 리듀서가 다음 상태를 계산 —
(현재 상태, 액션) => 다음 상태순수 함수가 변경을 책임집니다.
이 엄격함이 Redux의 장점이자 비용입니다. 상태 변화가 항상 액션을 거치므로 추적과 디버깅(시간 여행 디버깅 포함)이 강력합니다. 대신 그만큼 거쳐야 할 단계가 많습니다.
옛 Redux가 장황했던 이유 #
과거 Redux는 기능 하나를 추가하려면 액션 타입 상수, 액션 생성 함수, 리듀서를 각각 다른 파일에 손으로 써야 했습니다. 카운터 하나에 수십 줄이 들었고, 이 보일러플레이트가 Redux를 떠나게 만든 가장 큰 이유였습니다. Redux Toolkit은 바로 이 문제를 풀기 위해 공식 팀이 만든 도구입니다.
설치와 슬라이스 #
npm install @reduxjs/toolkit react-reduxRTK의 중심은 createSlice입니다. 상태, 리듀서, 액션을 한 곳에서 정의합니다.
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;state.value += 1처럼 상태를 직접 바꾸는 듯한 코드가 보입니다. 원래 리듀서는 상태를 불변으로 다뤄야 하는데, RTK는 내부에 Immer를 두어 이 “바꾸는 듯한” 코드를 안전한 불변 갱신으로 변환합니다. 덕분에 스프레드 연산자로 객체를 복사하던 번거로움이 사라졌습니다. 그리고 리듀서 이름(increment 등)에 맞는 액션 생성 함수가 자동으로 만들어집니다. 옛 Redux의 보일러플레이트 대부분이 여기서 사라집니다.
스토어 구성과 Provider #
여러 슬라이스를 모아 하나의 스토어로 만듭니다.
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./features/counterSlice";
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});그리고 #3 Zustand, #4 Jotai와 달리 Redux는 Provider로 트리를 감싸야 합니다.
import { Provider } from "react-redux";
import { store } from "./store";
createRoot(document.getElementById("root")).render(
<Provider store={store}>
<App />
</Provider>
);컴포넌트에서 읽고 쓰기 #
상태를 읽을 때는 useSelector, 액션을 보낼 때는 useDispatch를 씁니다.
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement, incrementByAmount } from "./features/counterSlice";
function Counter() {
const value = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(decrement())}>-</button>
<span>{value}</span>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
</div>
);
}useSelector의 셀렉터는 #3 Zustand의 셀렉터와 같은 발상입니다. 반환한 값이 바뀔 때만 컴포넌트가 리렌더됩니다. 액션은 반드시 dispatch를 거치므로, 상태가 언제 왜 바뀌었는지가 Redux DevTools에 액션 단위로 기록됩니다. 이 추적성이 큰 팀과 복잡한 상태에서 Redux가 여전히 선택되는 이유입니다.
새 프로젝트에서 Redux의 위치 #
Redux Toolkit은 옛 Redux보다 훨씬 간결해졌지만, 여전히 Zustand나 Jotai보다는 거쳐야 할 구조가 많습니다. 슬라이스, 스토어 구성, Provider가 기본으로 필요합니다. 그래서 도구를 고를 때 다음과 같이 정리할 수 있습니다.
| 상황 | 적합도 |
|---|---|
| 이미 Redux로 짜인 코드베이스 유지보수 | RTK가 자연스러운 현대화 경로 |
| 큰 팀, 엄격한 상태 변경 규약과 추적성이 중요 | Redux의 강점이 살아남 |
| 작은 앱, 가벼운 전역 상태만 필요 | Zustand,Jotai가 더 가벼움 |
| 상태가 대부분 서버 데이터 | TanStack Query 중심, 전역 저장소 최소화 |
즉 새로 시작하는 작은 프로젝트라면 굳이 Redux부터 꺼낼 이유는 줄었지만, 현장의 많은 코드가 Redux로 되어 있으므로 읽고 다룰 줄 아는 것은 여전히 중요합니다. 이 글을 “레거시 컨텍스트"라고 부른 이유입니다.
마무리 #
Redux Toolkit은 옛 Redux의 보일러플레이트를 걷어낸 공식 권장 형태입니다.
createSlice— 상태, 리듀서, 액션을 한곳에. Immer로 “바꾸는 듯한” 안전한 갱신configureStore— 슬라이스들을 모은 단일 스토어Provider— Redux는 트리를 감싸야 함 (Zustand,Jotai와 차이)useSelector/useDispatch— 읽기와 액션 디스패치- 강점은 추적성과 규약, 비용은 구조의 무게
이제 빌트인 도구부터 TanStack Query, Zustand, Jotai, Redux Toolkit까지 주요 선택지를 모두 봤습니다. 마지막 글인 “리액트 상태 관리 심화 #6 어떤 도구를 언제 — 결정 가이드"에서는 이 모두를 한 장의 결정 흐름으로 묶어, 실제 상황에서 무엇을 꺼낼지 정리하겠습니다.