리덕스(Redux), 리덕스 툴킷(Redux Toolkit)
들어가며
- 리액트(React.js)의 전역 상태 관리 라이브러리 중의 하나인 리덕스(Redux)와 리덕스 툴킷(Redux Toolkit)에 대해 정리해본다.
![]() |
![]() |
리덕스(Redux)
개념
- 자바스크립트 애플리케이션에서 상태 관리를 위해 사용되는 상태 컨테이너
- 애플리케이션의 상태를 중앙에서 관리함으로써 상태 변화를 쉽게 추적하고, 디버깅을 단순화하며, 다양한 컴포넌트 간에 상태를 공유하는 것을 쉽게 해준다.
- 리덕스를 사용하기 위해서는 아래의 명령을 실행하여 관련 패키지를 설치해준다.
$ yarn add redux # npm install redux $ yarn add react-redux # npm install react-redux
리덕스(Redux)는 리액트(React.js) 뿐만 아니라 다양한 자바스크립트 패키지에서도 사용할 수 있다.

액션(Action)
- 애플리케이션에서 일어나는 사건을 설명하는 객체
type
속성을 필수로 가지며, 상태 변경을 트리거하는 역할을 한다.- 예)
{ type: 'INCREMENT' }
,{ type: 'ADD_TODO', text: 'Learn Redux' }
리듀서(Reducer)
- 액션을 처리하여 새로운 상태를 반환하는 함수
- 이전 상태와 액션 객체를 인자로 받아 새로운 상태 객체를 반환한다.
- 순수 함수여야 하며, 입력이 같으면 출력도 항상 같아야 한다.
- 순수 함수(Pure Function)
- 동일한 인자가 주어졌을 때 항상 동일한 결과를 변환
- 함수의 실행이 외부 상태에 의존하지 않음
- 외부 상태를 변경하지 않는 함수
- 순수 함수(Pure Function)
function counter(state = 0, action) { switch (action.type) { case 'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
스토어(Store)
- 애플리케이션의 상태를 담고 있는 객체
createStore
함수를 사용하여 스토어를 생성한다.- 스토어는 3가지 메소드를 가진다:
getState()
: 현재 상태 반환합dispatch(action)
: 상태를 변경하는 액션 보내기subscribe(listener)
: 상태가 변경될 때마다 호출되는 리스너를 등록하기
미들웨어(Middleware)
- 액션이 리듀서에 도달하기 전에 처리할 수 있는 기능을 제공한다.
- 로깅, 에러 보고, 비동기 작업 처리 등에 사용된다.
사용 방법
- 예제 코드와 함께 Redux를 사용하는 방법을 간단하게 정리해본다.
./src/store/index.js
- 프로젝트의
src
폴더 내부에store
폴더를 생성한 후, 안에index.js
파일을 생성한다.
import { createStore } from 'redux'; const counterReducer = (state = initialState, action) => { if (action.type === "increment") { return { counter: state.counter + 1, showCounter: state.showCounter, }; } if (action.type === "increase") { return { counter: state.counter + action.amount, showCounter: state.showCounter, }; } if (action.type === "decrement") { return { counter: state.counter - 1, showCounter: state.showCounter, }; } if (action.type === "toggle") { return { showCounter: !state.showCounter, counter: state.counter, }; } return state; }; // 중앙 스토어 생성 및 리듀서 함수 연결 const store = createStore(counterReducer); export default store;
./src/index.js
- 생성한
store.js
파일에서store
를 불러온다. react-redux
패키지에서Provider
컴포넌트를 불러온다.Provider
컴포넌트로<App />
컴포넌트를 감싸준다.store
prop의 값을store.js
에서 불러온store
로 설정해준다.
import React from "react"; import ReactDOM from "react-dom/client"; import "./index.css"; import App from "./App"; // Provider, store 불러오기 import { Provider } from "react-redux"; import store from "./store/index"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( <Provider store={store}> {/* <App/>을 <Provider>로 감싸준 후, store prop 설정 *} <App /> </Provider> );
./src/components/counter.js
- 실제 컴포넌트에서 사용하는 예제이다.
useSelecter
와 전역 상태 보관소(store.js
)의 값에 접근한다.useDispatch
를 사용하여 컴포넌트에서 전역 상태 보관소(store.js
)에 액션(Action)을 전달하여 상태를 변경한다.
import { useSelector, useDispatch } from "react-redux"; import classes from "./Counter.module.css"; const Counter = () => { const dispatch = useDispatch(); const counter = useSelector((state) => state.counter); const show = useSelector((state) => state.showCounter); const incrementHandler = () => { dispatch({ type: "increment" }); }; const increaseHandler = () => { dispatch({ type: "increase", amount: 5 }); }; const decrementHandler = () => { dispatch({ type: "decrement" }); }; const toggleCounterHandler = () => { dispatch({ type: "toggle" }); }; return ( <main className={classes.counter}> <h1>Redux Counter</h1> {show && <div className={classes.value}>{counter}</div>} <div> <button onClick={incrementHandler}>Increment</button> <button onClick={increaseHandler}>Increase by 5</button> <button onClick={decrementHandler}>Decrement</button> </div> <button onClick={toggleCounterHandler}>Toggle Counter</button> </main> ); }; export default Counter;
사용 시 주의 사항
- 리덕스를 사용할 때 절대로 다음과 같이 변수 자체를 변형(Mutation)시키지 않는다.
if (action.type === "increment") { state.counter++; return state; }
- 그 대신에 다음과 같이 객체로 오버라이딩(Overriding)한다.
if (action.type === "increment") { return { counter: state.counter + 1, showCounter: state.showCounter, }; }
상태 업데이트 시 일부 상태만 설정하면 나머지 상태는 undefined로 처리되어 예상치 못한 결과를 초래할 수 있다. 따라서 상태 업데이트를 할 때는 모든 기존 상태를 포함하도록 새로운 상태 객체를 반환해야 한다.
리덕스 툴킷(Redux Toolkit, RTK)
개념
- 리덕스를 더 쉽고 효율적으로 사용하기 위해 리덕스 툴킷(Redux Toolkit)을 사용할 수 있다.
$ npm install @reduxjs/toolkit # yarn add @reduxjs/toolkit $ npm install react-redux # yarn add react-redux
⇒ 리덕스 툴킷은 아래와 같은 라이브러리로 구성되어 있다.
✅ redux
: 상태 관리
✅ immer
: 상태 불변성 관리
✅ redux-thunk
: 비동기 액션 처리
✅ reselect
: 셀렉터 최적화
- 리덕스 툴킷은 리덕스의 일반적인 보일러플레이트(반복되는) 코드를 줄이고, 직관적이며 간결한 코드 작성을 가능하게 한다.
- 기본 리덕스를 사용할 때는 많은 설정과 보일러플레이트 코드가 필요하다.
- 예를 들어, 액션 생성자, 리듀서, 스토어 설정 등을 별도로 작성해야 하는데 리덕스 툴킷은 이러한 설정을 단순화하여 개발자의 작업을 줄여준다.
- 기본 리덕스를 사용할 때는 많은 설정과 보일러플레이트 코드가 필요하다.
- 리덕스 툴킷은
configureStore
함수를 제공하여 스토어를 쉽게 설정할 수 있다.- 이 함수는 기본적인 미들웨어 설정과 디버깅 도구 설정을 자동으로 처리해준다.
import { configureStore } from '@reduxjs/toolkit'; import rootReducer from './reducers'; const store = configureStore({ reducer: rootReducer, });
- 리덕스 툴킷의
createSlice
함수는 액션 생성자와 리듀서를 한 번에 생성할 수 있게 해준다.- 리덕스의 핵심 개념을 더 직관적이고 간결하게 구현할 수 있다.
import { createSlice } from '@reduxjs/toolkit'; const counterSlice = createSlice({ name: 'counter', initialState: 0, reducers: { increment: (state) => state + 1, decrement: (state) => state - 1, }, }); export const { increment, decrement } = counterSlice.actions; export default counterSlice.reducer;
- 리덕스 툴킷은 리덕스 개발자 도구(Redux DevTools)와 통합하여 상태 변화를 시각적으로 추적하고 디버깅하는 데 큰 도움을 준다.
- 리덕스 툴킷은 타입 스크립트(TypeScript)와의 호환성을 강화하여, 타입 안전성을 유지하면서도 리덕스를 간편하게 사용할 수 있게 해준다.
- 리덕스 툴킷은
immer
를 사용하여 불변성 관리를 자동으로 처리해주며, 기본적으로 Redux Thunk 미들웨어를 포함한다.
- 이러한 설정들은 개발자가 직접 최적화할 필요 없이 기본적으로 제공되어 개발 속도를 빠르게 해준다.
Redux Toolkit은 Redux를 더욱 편리하게 사용할 수 있도록 만들어진 도구로, Redux에서 자주 사용되는 기능들을 포함한 '배터리가 장착된' Redux라고 할 수 있다.
Immer와 상태 불변성(Immutability)
- 리덕스를 사용할 때 절대로 변수 자체를 변형(Mutation)시키면 안된다.
- 하지만, 리덕스 툴킷에서는
createSlice
를 사용할 때Immer
라는 라이브러리가 자동으로 포함되어 있어, 불변성을 직접 관리할 필요 없이 더 직관적인 방식으로 상태를 변경할 수 있다. - 즉, 상태를 직접 변형하는 것처럼 보이는 코드를 작성해도
Immer
가 내부적으로 상태를 불변성으로 유지해 준다.
const counterSlice = createSlice({ name: 'counter', initialState: { counter: 0, showCounter: true }, reducers: { increment(state) { state.counter++; } } });
⇒ state.counter++
는 상태를 직접 변형하는 것처럼 보이지만, Immer
가 이 코드를 감싸서 실제로는 불변성을 유지한 상태로 새로운 상태를 반환하게 해준다.
리덕스 툴킷에서는 상태를 명시적으로 오버라이딩(Overriding)하는 방식도 사용할 수 있지만, Immer 라이브러리 때문에 상태를 변경하는 코드를 작성할 수도 있다.
createAsyncThunk() 함수
- 리덕스 툴킷은 비동기 작업을 간편하게 처리할 수 있는
createAsyncThunk
함수를 제공한다.- 비동기 작업의 상태 관리(로딩(
pending
), 성공(fulfilled
), 실패(rejected
) 등)를 쉽게 처리할 수 있다.
- 비동기 작업의 상태 관리(로딩(
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import axios from 'axios'; export const fetchUser = createAsyncThunk('users/fetchUser', async (userId) => { const response = await axios.get(`/api/users/${userId}`); return response.data; }); const userSlice = createSlice({ name: 'user', initialState: { user: null, loading: false, error: null, }, reducers: {}, extraReducers: (builder) => { builder .addCase(fetchUser.pending, (state) => { state.loading = true; }) .addCase(fetchUser.fulfilled, (state, action) => { state.loading = false; state.user = action.payload; }) .addCase(fetchUser.rejected, (state, action) => { state.loading = false; state.error = action.error.message; }); }, }); export default userSlice.reducer;
- 관련 내용은 아래의 글을 참고한다.
[React.js] Thunk API (Redux Toolkit)
Thunk API (Redux Toolkit)들어가며리덕스 툴킷(Redux Toolkit)의 Thunk API에 대해 정리해본다. Thunk API개념비동기 작업을 간편하게 처리할 수 있는 도구로, 여러가지 기능을 제공한다.주로 API 호출과 같은
dev-astra.tistory.com
사용 방법
- 실제 제작했었던 예제 코드를 바탕으로 사용 방법을 정리해본다.
슬라이스 생성하기
./src/features/cart/cartSlice.jsx
- 슬라이스 파일을 생성해준다.
./src
경로에features
폴더를 생성한 후, 전역 상태를 관리할 항목의 범주에 따라 폴더(cart
)와 슬라이스 파일(cartSlice.jsx
)을 생성해준다.- 슬라이스 파일(
cartSlice.jsx
)에createSlice
함수를 이용하여 슬라이스를 생성한다.- 슬라이스 이름(
name
) :'cart'
- 상태 초기값(
initialState
) :initialState
- 리듀서(
reducers
) :{ action: (state, { payload }) => {}, ..., action: (state, { payload }) => {} }
- 리듀서 안에 액션(Action)들을 정의해준다.
- 슬라이스 이름(
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import axios from 'axios'; import { openModal } from '../modal/modalSlice'; const url = 'https://www.example-api.com/cart-project'; // 상태 초기값 설정하기 const initialState = { cartItems: [], amount: 0, total: 0, isLoading: true, }; // API 통신 후 데이터 가져오기 export const getCartItems = createAsyncThunk( 'cart/getCartItems', async (name, thunkAPI) => { try { // console.log(name); // thunkAPI를 이용하여 다양한 정보를 얻을 수 있다. (state 등) // console.log(thunkAPI); // console.log(thunkAPI.getState()); // thunkAPI.dispatch(openModal()); const resp = await axios(url); return resp.data; } catch (error) { return thunkAPI.rejectWithValue('something went wrong'); } } ); // 슬라이스 생성하기 const cartSlice = createSlice({ // (1) 슬라이스 이름 설정 name: 'cart', // (2) 상태 초기값 설정 initialState, // (3) 리듀서 설정 reducers: { // 액션1 : 카트 비우기 clearCart: (state) => { state.cartItems = []; }, // 액션2 : 아이템 삭제하기 removeItem: (state, action) => { const itemId = action.payload; state.cartItems = state.cartItems.filter((item) => item.id !== itemId); }, // 액션3 : 아이템 개수 증가시키기 // payload에는 dispatch를 수행하는 외부 컴포넌트에서 보내는 값이 들어있다. increase: (state, { payload }) => { const cartItem = state.cartItems.find((item) => item.id === payload.id); cartItem.amount = cartItem.amount + 1; }, // 액션4 : 아이템 개수 감소시키기 decrease: (state, { payload }) => { const cartItem = state.cartItems.find((item) => item.id === payload.id); cartItem.amount = cartItem.amount - 1; }, // 액션5 : 전체 아이템 개수 및 가격 계산하기 calculateTotals: (state) => { let amount = 0; let total = 0; state.cartItems.forEach((item) => { amount += item.amount; total += item.amount * item.price; }); state.amount = amount; state.total = total; }, }, // (4) 추가 리듀서 설정 // RTK 2.0부터 객체 형태가 아닌, '빌더 콜백' 형태로 사용한다. extraReducers: (builder) => { builder // pending 상태일 경우 (pending) .addCase(getCartItems.pending, (state) => { state.isLoading = true; }) // 모두 불러와졌을 경우 (fulfilled) .addCase(getCartItems.fulfilled, (state, action) => { // console.log(action); state.isLoading = false; state.cartItems = action.payload; // 불러와진 데이터로 cartItem 업데이트 }) // 거절되었을 경우 (rejected) .addCase(getCartItems.rejected, (state, action) => { console.log(action); state.isLoading = false; }); }, }); // <1> 리듀서 내보내기 // - 기본 내보내기를 했기 때문에, 외부에서 불러올 때는 아무 이름으로 불러올 수 있다. // 예) import cartReducer from './features/cart/cartSlice'; export default cartSlice.reducer; // <2> 액션 내보내기 // 외부에서 사용할 수 있는 액션을 내보낸다. export const { clearCart, removeItem, increase, decrease, calculateTotals } = cartSlice.actions;
./src/features/modal/modalSlice.jsx
import { createSlice } from '@reduxjs/toolkit'; const initialState = { isOpen: false, }; const modalSlice = createSlice({ name: 'modal', initialState, reducers: { // 액션1 : 모달 열기 openModal: (state, action) => { state.isOpen = true; }, // 액션2 : 모달 닫기 closeModal: (state, action) => { state.isOpen = false; }, }, }); // 리듀서 내보내기 export default modalSlice.reducer; // 액션 내보내기 export const { openModal, closeModal } = modalSlice.actions;
스토어(Store) 생성하기
./src/store/index.js
./src
경로에store
폴더를 만들고, 안에index.js
파일을 생성한다.- 그리고 안에서 위에서 생성했던 슬라이스 파일을 불러온 후,
configureStore()
를 이용하여 리듀서를 설정해준다.- 이제 외부 컴포넌트에서에서 이 저장소를 통해 슬라이스의 값(
cart
,modal
)에 접근할 수 있게 된다.
- 이제 외부 컴포넌트에서에서 이 저장소를 통해 슬라이스의 값(
import { configureStore } from '@reduxjs/toolkit'; // (참고) export default로 내보내기 했기 때문에, import 할 때는 아무런 이름을 지정하여 가져올 수 있다. import cartReducer from '../features/cart/cartSlice'; import modalReducer from '../features/modal/modalSlice'; export const store = configureStore({ reducer: { // 외부에서 접근할 때 다음과 같이 사용한다. // -> const { value } = useSelector((state) => state.cart); cart: cartReducer, modal: modalReducer, }, });
최상위 경로의 파일(main.jsx)
에 스토어 적용하기
./src/main.jsx
- 최상위 경로의 파일(
./src/main.jsx
)에 스토어(Store,./src/store/index.js
)를 적용시켜준다.react-redux
패키지에서Provider
컴포넌트를 불러온다../store/index.js
파일에서store
를 불러온다.- 최상위 컴포넌트인
<App />
컴포넌트를<Provider
컴포넌트로 감싸 준다.store
prop의 값을./store/index.js
파일에서 불러온store
로 설정해준다.
import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import App from './App.jsx'; import './index.css'; // 리덕스 사용하기 import { Provider } from 'react-redux'; import { store } from './store/index.js'; createRoot(document.getElementById('root')).render( <StrictMode> <Provider store={store}> <App /> </Provider> </StrictMode> );
사용하기
- 이제 전역 상태 관리를 위한 구성을 끝냈다.
- 전역 상태 변수와 액션에 접근하는 방법을 정리해본다.
전역 상태 변수에 접근하기
react-redux
패키지의useSelector
함수를 사용하여 전역 상태 변수에 접근할 수 있다.
import { CartIcon } from '../icons'; import { useSelector } from 'react-redux'; const Navbar = () => { // 전역 상태 변수에 접근하기 const { amount } = useSelector((state) => state.cart); return ( <nav> <div className="nav-center"> <h3>Cart with Redux Toolkit</h3> <div className="nav-container"> <CartIcon /> <div className="amount-container"> <p className="total-amount">{amount}</p> </div> </div> </div> </nav> ); }; export default Navbar;
액션에 디스패치 하기
react-redux
패키지의useDispatch
함수를 이용하여 액션에 디스패치할 수 있다.- 이때, 디스패치하기 위해 필요한 함수들을 슬라이스 파일에서 불러와준다.
import { removeItem, increase, decrease } from '../features/cart/cartSlice';
- 이렇게 디스패치를 함으로써 전역적으로 상태를 변화시킬 수 있다.
import { useDispatch } from 'react-redux'; import { ChevronDown, ChevronUp } from '../icons'; import { removeItem, increase, decrease } from '../features/cart/cartSlice'; const CartItem = ({ id, img, title, price, amount }) => { const dispatch = useDispatch(); return ( <article className="cart-item"> <img src={img} alt={title} /> <div> <h4>{title}</h4> <h4 className="item-price">${price}</h4> {/* remove button */} <button className="remove-btn" onClick={() => dispatch(removeItem(id))}> remove </button> </div> <div> {/* increase amount */} <button className="amount-btn" onClick={() => dispatch(increase({ id }))} > <ChevronUp /> </button> {/* amount */} <p className="amount">{amount}</p> {/* decrease amount */} <button className="amount-btn" onClick={() => { if (amount === 1) { dispatch(removeItem(id)); return; } dispatch(decrease({ id })); }} > <ChevronDown /> </button> </div> </article> ); }; export default CartItem;
프로젝트 구조 정리
/src ├─ /features ─ /cart/cartSlice.js, /modal/modalSlice → 슬라이스 파일 보관 ├─ /store ─ index.js → 스토어 파일 보관 ├─ /components → 전역 상태 변수 접근 및 액션 디스페치 ├─ App.jsx └─ index.jsx → 스토어 적용
사용 시 주의 사항
- RTK 2.0부터는
createReducer
와createSlice.extraReducers
에서 객체(Object) 형태의 사용을 더 이상 지원하지 않는다.
// RTK 2.0 이전의 방식 const cartSlice = createSlice({ // ... extraReducers: { [getCartItems.pending]: (state) => { state.isLoading = true; }, [getCartItems.fulfilled]: (state, action) => { console.log(action); state.isLoading = false; state.cartItems = action.payload; }, [getCartItems.rejected]: (state) => { state.isLoading = false; }, }, }
- 빌더(Builder) 콜백 형태가 간결하며, 타입스크립트(TypeScript)와의 통합에서도 더 효과적이기 때문이다.
// RTK 2.0 이후의 방식 // -> 빌더 콜백 형태로 사용한다. const cartSlice = createSlice({ // ... extraReducers: (builder) => { builder .addCase(getCartItems.pending, (state) => { state.isLoading = true; }) .addCase(getCartItems.fulfilled, (state, action) => { // console.log(action); state.isLoading = false; state.cartItems = action.payload; }) .addCase(getCartItems.rejected, (state, action) => { console.log(action); state.isLoading = false; }); }, }
함께 사용하면 좋은 것들
Redux DevTools
- 브라우저 확장 프로그램이다.
- 애플리케이션의 상태(
state
)를 쉽게 디버깅하고 추적할 수 있게 도와준다. - 애플리케이션의 상태 변화, 액션(
action
)의 흐름, 상태 변화 전후의 차이를 실시간으로 확인할 수 있다.
Redux DevTools - Chrome 웹 스토어
Redux DevTools for debugging application's state changes.
chromewebstore.google.com
참고 사이트
Redux - A JS library for predictable and maintainable global state management | Redux
A JS library for predictable and maintainable global state management
redux.js.org
Redux Toolkit | Redux Toolkit
The official, opinionated, batteries-included toolset for efficient Redux development
redux-toolkit.js.org
'Programming > React' 카테고리의 다른 글
[React.js] React Hook Form 라이브러리 (0) | 2024.11.23 |
---|---|
[React.js] 객체 표기법(Object Notation) 방식과 빌더 콜백 표기법(Builder Callback Notation) 방식 (0) | 2024.11.13 |
[React.js] .js 파일에서 Uncaught SyntaxError: Unexpected token '<' 오류 발생할 때 해결 방법 (Vite) (0) | 2024.11.13 |
[React.js] <Link> 컴포넌트와 <NavLink> 컴포넌트 비교 (React Router) (0) | 2024.11.13 |
[React.js] 모든 웹 브라우저에서 공통된 HTML 요소 스타일이 보여지도록 설정하는 방법 (normalize.css) (0) | 2024.11.07 |
[React.js] Recharts 라이브러리 (1) | 2024.11.06 |
[React.ts] PropsWithChildren (0) | 2024.11.05 |
[React.js] const Component vs. function Component (2) | 2024.11.04 |