Node 환경에서 리덕스 사용하기
Vanilla JavaScript로 시작해서 Redux, 그리고 최신 Redux Toolkit까지 단계별로 진행하며 각 방식의 차이점과 장단점을 이해해보겠습니다.
1. Project Setup
Vite를 사용하여 빠른 개발 환경을 구축합니다.
npx my-react-vite-app [프로젝트 이름]
npm install @reduxjs/toolkit react-redux
npm run dev
2. 리덕스 없이 Node환경에서 구현해보기
Redux의 필요성을 이해하기 위해 먼저 순수 JavaScript로 상태 관리를 구현해보겠습니다. 간단한 상품 수량 카운터를 만들어보겠습니다.

<!-- my-redux-app / index.html -->
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<title>React App</title>
</head>
<body>
<!-- <div id="root"></div> -->
<h1>Product Detail</h1>
<h2>딥러닝 개발자 무릎 담요</h2>
<span><strong>17,500</strong>원</span>
<div id="counter-box">
<button type="button" id="minus" disabled>MINUS</button>
<span id="number">0</span>
<button type="button" id="plus">PLUS</button>
</div>
<div>총 수량 <strong id="quantity">0</strong></div>
<div><strong id="total">0</strong>원</div>
<script>
const plus = document.getElementById('plus');
const minus = document.getElementById('minus');
const number = document.getElementById('number'); // 수량
const quantity = document.getElementById('quantity'); // 페이지 하단에 총 수량
const totalPrice = document.getElementById('total'); // 페이지 하단에 총 가격
const PRICE = 17500;
let count = 0;
// UI Update - text
const updateResult = (c) => {
number.innerText = count;
quantity.innerHTML = c;
totalPrice.innerHTML = c * PRICE;
};
// State Change
const addNumber = () => {
count += 1;
minus.disabled = false;
updateResult(count);
};
// State Change
const substractNumber = () => {
count -= 1;
if (count <= 0) {
minus.disabled = true;
}
updateResult(count);
};
// Init
number.innerHTML = count;
updateResult(count);
// Event
plus.addEventListener('click', addNumber);
minus.addEventListener('click', substractNumber);
</script>
</body>
</html>
Count를 Vanila JS로 구현해본다면 클릭 이벤트가 발생할 때마다 count의 상태값을 직접 수정한 후, HTML을 업데이트 해줍니다.
위 코드를 살펴보면 몇 가지 문제점을 발견할 수 있습니다.
- 전역 변수 남용:
count변수가 전역 스코프에 노출되어 어디서든 수정 가능합니다. - 상태와 UI 로직 혼재: 상태 변경과 DOM 조작이 한 곳에 섞여 있습니다.
- 예측 불가능한 상태 변화: 상태가 언제, 어디서, 어떻게 변경되는지 추적하기 어렵습니다.
- 재사용성 부족: 다른 컴포넌트에서 같은 로직을 사용하기 어렵습니다.
3. 리덕스로 구현해보기
위에서 구현한 기능을 리덕스를 사용해서 구현해 봅시다!
<!-- my-redux-app / index.html 원상복귀 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
앞서 수정한 파일을 원상복귀 합니다.
3.1 src/store/index.js - Redux의 핵심 로직
// src/store/index.js
// createStore에 줄이 쳐집니다.
// 다만 교육용으로는 createStore가 좋다 판단해 사용하도록 하겠습니다.
import { createStore } from 'redux';
// 상수
const PRICE = 17500;
// 액션 타입
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
// 액션 생성자
export const increment = () => ({ type: INCREMENT });
export const decrement = () => ({ type: DECREMENT });
// 초기 상태
const initialState = {
count: 0,
unitPrice: PRICE,
totalPrice: 0,
};
// Reducer
function counterReducer(state = initialState, action) {
console.log('Reducer 호출:', action.type, state);
switch (action.type) {
case INCREMENT:
const newCountInc = state.count + 1;
return {
...state,
count: newCountInc,
totalPrice: newCountInc * state.unitPrice,
};
case DECREMENT:
const newCountDec = Math.max(0, state.count - 1);
return {
...state,
count: newCountDec,
totalPrice: newCountDec * state.unitPrice,
};
default:
return state;
}
}
// Store 생성
const store = createStore(
counterReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
);
export default store;
이 파일은 Redux의 뇌 역할을 하는 곳입니다. 상태 관리의 모든 규칙과 로직이 들어있어요. 21년부터는 Redux Toolkit이라는 것을 권장하고 있어 createStore가 줄이 쳐집니다. 다만 이렇게 했을 경우 각 부분을 하나씩 이해하기 난해하여 createStore를 사용하도록 하겠습니다. 실무에서는 RTK(리덕스 툴킷)을 사용합니다. 변경된 코드는 뒤에 첨부합니다.
import { createStore } from 'redux';
Redux 라이브러리에서 Store를 만드는 함수를 가져옵니다. Store는 앱의 모든 상태를 보관하는 큰 창고라고 생각하면 됩니다.
const PRICE = 17500;
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
상수들을 정의합니다. PRICE는 상품 가격이고, INCREMENT와 DECREMENT는 액션 타입입니다. 액션 타입을 상수로 만드는 이유는 오타를 방지하고 나중에 수정하기 쉽게 하기 위해서예요.
export const increment = () => ({ type: INCREMENT });
export const decrement = () => ({ type: DECREMENT });
액션 생성자 함수들입니다. 이 함수들을 호출하면 Redux가 이해할 수 있는 액션 객체를 만들어줍니다. 예를 들어 increment()를 호출하면 { type: 'INCREMENT' } 객체가 만들어져요.
const initialState = {
count: 0,
unitPrice: PRICE,
totalPrice: 0,
};
앱이 처음 시작될 때의 초기 상태를 정의합니다. 수량은 0개, 단가는 17500원, 총 가격은 0원으로 시작하죠.
function counterReducer(state = initialState, action) {
console.log('Reducer 호출:', action.type, state);
// ...
}
Reducer 함수의 시작입니다. 이 함수는 Redux의 핵심이에요. 현재 상태와 액션을 받아서 새로운 상태를 만들어 반환합니다. state = initialState는 상태가 없을 때 초기값을 사용하겠다는 뜻입니다.
switch (action.type) {
case INCREMENT:
const newCountInc = state.count + 1;
return {
...state,
count: newCountInc,
totalPrice: newCountInc * state.unitPrice,
};
// ...
}
액션 타입이 INCREMENT일 때의 처리입니다. 기존 수량에서 1을 더하고, 총 가격도 새로 계산합니다. ...state는 스프레드 연산자로 기존 상태를 복사하는 겁니다. Redux에서는 기존 상태를 직접 수정하지 않고 새로운 객체를 만들어야 해요.
case DECREMENT:
const newCountDec = Math.max(0, state.count - 1)
return {
...state,
count: newCountDec,
totalPrice: newCountDec * state.unitPrice
}
DECREMENT 액션 처리입니다. Math.max(0, state.count - 1)로 수량이 0 밑으로 내려가지 않도록 막습니다.
const store = createStore(
counterReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
);
실제 Redux Store를 생성합니다. 첫 번째 인자는 reducer 함수, 두 번째는 브라우저의 Redux DevTools와 연결하는 코드예요.
3.2 src/main.jsx - Redux를 React에 연결
// src/main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './store';
import App from './App.jsx';
console.log('Redux Store 초기 상태:', store.getState());
createRoot(document.getElementById('root')).render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>,
);
이 파일은 Redux Store를 React 앱 전체에서 사용할 수 있도록 연결하는 역할을 합니다.
import { Provider } from 'react-redux';
import store from './store';
react-redux 라이브러리에서 Provider를 가져오고, 우리가 만든 store도 가져옵니다. Provider는 Redux Store를 React 컴포넌트들에게 전달해주는 특별한 컴포넌트예요.
console.log('Redux Store 초기 상태:', store.getState());
개발자 도구에서 Store의 초기 상태를 확인할 수 있도록 로그를 찍습니다. store.getState()는 현재 Store에 저장된 상태를 가져오는 함수입니다.
<Provider store={store}>
<App />
</Provider>
Provider로 App 컴포넌트를 감쌉니다. 이렇게 하면 App과 그 안의 모든 자식 컴포넌트들이 Redux Store에 접근할 수 있게 됩니다. 마치 Wi-Fi 공유기 같은 역할이라고 생각하면 돼요.
3.3 src/App.jsx - Redux를 실제로 사용
// src/App.jsx
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './store';
function App() {
// Redux Store에서 상태 가져오기
const { count, unitPrice, totalPrice } = useSelector((state) => state);
// 액션 디스패치 함수
const dispatch = useDispatch();
// 이벤트 핸들러
const handleIncrement = () => {
console.log(' + INCREMENT 액션 디스패치');
dispatch(increment());
};
const handleDecrement = () => {
console.log(' - DECREMENT 액션 디스패치');
dispatch(decrement());
};
console.log('App 렌더링, 현재 상태:', { count, totalPrice });
return (
<div>
<h1>Product Detail</h1>
<h2>딥러닝 개발자 무릎 담요</h2>
<span>
<strong>{unitPrice.toLocaleString()}</strong>원
</span>
<div>
<button type="button" disabled={count <= 0} onClick={handleDecrement}>
MINUS
</button>
<span>{count}</span>
<button type="button" onClick={handleIncrement}>
PLUS
</button>
</div>
<div>
총 수량 <strong>{count}</strong>
</div>
<div>
<strong>{totalPrice.toLocaleString()}</strong>원
</div>
</div>
);
}
export default App;
이 파일에서 Redux Store의 상태를 읽고, 액션을 보내서 상태를 변경합니다.
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './store';
react-redux에서 두 개의 중요한 Hook을 가져옵니다. useSelector는 Store에서 상태를 읽어오고, useDispatch는 액션을 보내는 역할을 해요. 그리고 우리가 만든 액션 생성자 함수들도 가져옵니다.
const { count, unitPrice, totalPrice } = useSelector((state) => state);
useSelector를 사용해서 Redux Store에서 필요한 상태들을 가져옵니다. state => state는 전체 상태를 그대로 가져오겠다는 뜻이에요. 이 Hook은 상태가 바뀔 때마다 컴포넌트를 자동으로 다시 렌더링해줍니다.
const dispatch = useDispatch();
액션을 보낼 수 있는 dispatch 함수를 가져옵니다. 이 함수를 통해 Redux Store에게 "이런 변화를 원해요"라고 알려줄 수 있어요.
const handleIncrement = () => {
console.log(' + INCREMENT 액션 디스패치');
dispatch(increment());
};
PLUS 버튼을 클릭했을 때 실행되는 함수입니다. dispatch(increment())를 호출하면 Redux Store에게 INCREMENT 액션을 보냅니다. 그러면 Store가 자동으로 reducer 함수를 호출해서 상태를 업데이트해요.
const handleDecrement = () => {
console.log(' - DECREMENT 액션 디스패치');
dispatch(decrement());
};
MINUS 버튼을 클릭했을 때 실행되는 함수입니다. DECREMENT 액션을 보내서 수량을 줄입니다.
console.log('App 렌더링, 현재 상태:', { count, totalPrice });
컴포넌트가 렌더링될 때마다 현재 상태를 콘솔에 출력합니다. 상태가 어떻게 변하는지 확인할 수 있어요.
<button type="button" disabled={count <= 0} onClick={handleDecrement}>
MINUS
</button>
MINUS 버튼입니다. disabled={count <= 0}로 수량이 0일 때는 버튼을 비활성화합니다. onClick={handleDecrement}로 클릭했을 때 감소 함수를 실행해요.
이렇게 Redux는 액션 → 디스패치 → 리듀서 → 새 상태 → 컴포넌트 리렌더링의 순환 구조로 동작합니다. 마치 주문서(액션)를 보내면 요리사(리듀서)가 요리(새 상태)를 만들어주는 것과 같습니다.
4. RTK 방식으로 변환
Redux Toolkit은 기존 Redux의 불편한 점들을 해결하기 위해 만들어진 ‘Redux를 더 쉽게 쓰는 도구’입니다. 우선 코드를 모두 수정하고 난 다음 변경이 된 것들에 대해 논해보도록 하겠습니다.
// src/store/index.js - Redux Toolkit 방식
import { configureStore, createSlice } from '@reduxjs/toolkit';
const PRICE = 17500;
// createSlice로 액션과 리듀서를 한 번에 생성
const counterSlice = createSlice({
name: 'counter',
initialState: {
count: 0,
unitPrice: PRICE,
totalPrice: 0,
},
reducers: {
increment: (state) => {
// RTK는 Immer를 내장해서 직접 수정하는 것처럼 쓸 수 있음
state.count += 1;
state.totalPrice = state.count * state.unitPrice;
},
decrement: (state) => {
if (state.count > 0) {
state.count -= 1;
state.totalPrice = state.count * state.unitPrice;
}
},
},
});
// 액션 생성자들을 자동으로 생성해줌
export const { increment, decrement } = counterSlice.actions;
// configureStore로 스토어 생성 (Redux DevTools 자동 설정됨)
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
});
export default store;
// src/App.jsx - RTK 버전
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './store';
function App() {
// RTK에서는 state.counter로 접근 (slice 이름)
const { count, unitPrice, totalPrice } = useSelector(
(state) => state.counter,
);
const dispatch = useDispatch();
const handleIncrement = () => {
console.log('+ INCREMENT 액션 디스패치');
dispatch(increment());
};
const handleDecrement = () => {
console.log('- DECREMENT 액션 디스패치');
dispatch(decrement());
};
console.log('App 렌더링, 현재 상태:', { count, totalPrice });
return (
<div>
<h1>Product Detail</h1>
<h2>딥러닝 개발자 무릎 담요</h2>
<span>
<strong>{unitPrice.toLocaleString()}</strong>원
</span>
<div>
<button type="button" disabled={count <= 0} onClick={handleDecrement}>
MINUS
</button>
<span>{count}</span>
<button type="button" onClick={handleIncrement}>
PLUS
</button>
</div>
<div>
총 수량 <strong>{count}</strong>
</div>
<div>
<strong>{totalPrice.toLocaleString()}</strong>원
</div>
</div>
);
}
export default App;
4.4.1 Redux Toolkit의 주요 개선사항
-
createSlice의 장점
- 보일러플레이트 코드 감소: 액션 타입, 액션 생성자, 리듀서를 한 번에 정의합니다.
- Immer 내장: 불변성을 유지하면서도 직관적인 코드 작성이 가능합니다.
-
Immer를 통한 불변성 관리 기존 Redux에서는 다음과 같이 작성해야 했습니다.
// 기존 Redux return { ...state, count: state.count + 1, totalPrice: (state.count + 1) * state.unitPrice, };RTK에서는 다음과 같이 직관적으로 작성할 수 있습니다.
// Redux Toolkit with Immer state.count += 1; state.totalPrice = state.count * state.unitPrice;내부적으로 Immer가 불변성을 유지해주므로 안전합니다.
-
configureStore의 개선사항
const store = configureStore({ reducer: { counter: counterSlice.reducer, }, });- configureStore의 장점
- Redux DevTools Extension이 자동으로 설정됩니다. (이전에는 직접 설정해주었어야 했습니다. 궁금하신 분들이나 필요하신 분들은 이전 강의 백업을 확인해주세요.)
- redux-thunk 미들웨어(비동기 작업을 위한 함수, 예를 들어 fetch)가 기본으로 포함됩니다.
- 개발 모드에서 불변성 검사와 직렬화 검사를 자동으로 수행합니다.
- configureStore의 장점
4.4.2 RTK 버전 React 컴포넌트
주요 변화는 상태 접근 방식입니다.
// 기존 Redux
const { count, unitPrice, totalPrice } = useSelector((state) => state);
// Redux Toolkit
const { count, unitPrice, totalPrice } = useSelector((state) => state.counter);
configureStore에서 counter: counterSlice.reducer로 설정했기 때문에 state.counter로 접근해야 합니다.