📘

3.2 Redux Toolkit 동작 원리

 
리덕스 툴킷은 기존의 리덕스를 사용할 때 작성하던 코드를 더 간결하게 작성할 수 있도록 해줍니다. 또한 내부적으로 Immer를 사용하여 자동으로 상태의 불변성을 유지할 수 있으며, 이로 인해 리듀서 함수를 더 간결하게 작성할 수 있습니다. 그뿐만 아니라 기존 리덕스에서 리듀서를 하나로 통합하고, 통합한 리듀서를 스토어에 연결하기 위해 스토어를 생성하는 번거로운 과정을 리덕스 툴킷에서는 한 번에 처리할 수 있습니다.
 
이번에는 2장에서 리덕스를 활용해 만든 계산기와 투두리스트 프로젝트를 리덕스 툴킷을 활용하여 구현해 보겠습니다. 코드를 작성하기 전에 리덕스 툴킷을 사용하기 위한 설치 및 환경 설정을 진행하겠습니다.
 

3.2.1 Redux Toolkit 사용 환경 세팅하기

리액트 프로젝트 생성 후 프로젝트 디렉토리에 리덕스 툴킷에 필요한 라이브러리 및 의존성을 설치해 줍니다.
npm install react-redux npm install @reduxjs/toolkit
 
설치가 제대로 되었다면 package.json 파일에 ‘@reduxjs/toolkit’과 ‘react-redux’가 추가 되었음을 확인할 수 있습니다.
// package.json "dependencies": { "@reduxjs/toolkit": "^1.9.7", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "eslint-plugin-jsx-a11y": "^6.7.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^8.1.3", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" },
 

3.2.2 Components 생성

기능 없이 계산기와 투두리스트의 틀만 잡아주는 컴포넌트를 생성하고 App.js에서 렌더링을 해줍니다.

1) Calculator 컴포넌트 만들기

// components/Calculator.jsx import { useState } from "react"; export default function Calculator() { const [inputValue, setInputValue] = useState(""); return ( <> <div>결과: </div> <input type="number" value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> <div> <button onClick={() => {}}>+</button> <button onClick={() => {}}>-</button> <button onClick={() => {}}>/</button> <button onClick={() => {}}>*</button> <button onClick={() => {}}>C</button> </div> <br /> </> ); }
 

2) TodoList 컴포넌트 만들기

// components/TodoList.jsx import { useState } from "react"; export default function TodoList() { const [inputValue, setInputValue] = useState(""); const todos = []; return ( <> <form onSubmit={(e) => e.preventDefault()}> <input type="text" value={inputValue} placeholder="할 일을 작성해 주세요" onChange={(e) => setInputValue(e.target.value)} /> <button type="submit" onClick={() => {}}> 추가 </button> </form> {todos.map((todo) => ( <section key={todo.id}> <input type="checkbox" onClick={() => {}} /> <div style={{ textDecoration: todo.isDone ? "line-through" : "none", }} > {todo.content} </div> <button onClick={() => {}}>❌</button> </section> ))} </> ); }
 

3) App에서 렌더링하기

// App.js import Calculator from "./components/Calculator"; import TodoList from "./components/TodoList"; function App() { return ( <div> <Calculator /> <TodoList /> </div> ); } export default App;
여기까지 진행하면 다음과 같은 화면이 렌더링 됩니다.
notion imagenotion image
 

3.2.3 createSlice

앞서 리덕스에서는 액션과 액션 생성자, 리듀서 함수를 수동으로 작성해야 했습니다. 액션과 액션 생성 함수는 일일이 정의하고 관리해 줘야 했으며, 리듀서 함수도 마찬가지로 이전 상태와 액션을 받아 새로운 상태를 반환하는 로직을 포함해서 따로 생성해 줘야 했습니다. 하지만 이러한 복잡한 과정을 리덕스 툴킷의 createSlice를 사용하면 코드 작성 및 관리를 단순화하고 효율적으로 만들 수 있습니다.
 
createSlice를 살펴보기 전에 리덕스 툴킷에서 액션과 리듀서 함수를 만들어주는 createAction과 createReducer 함수를 간단한 예시 코드를 통해 먼저 알아보겠습니다.
• createAction
리덕스에서는 액션 타입을 문자열 상수로 정의하고, 액션 생성자 함수를 직접 작성해야 했습니다. 이로 인해 액션이 많아질 경우 코드가 더 길어지고 그 과정이 번거로웠습니다.
// Redux에서의 액션 타입과 액션 생성자 const INCREMENT = 'INCREMENT'; function incrementAction(payload) { return { type: INCREMENT, payload }; }
리덕스 툴킷의 createAction 함수를 통해 위의 액션과 액션 생성 함수의 정의가 매우 간결하게 작성됨을 알 수 있습니다. 이 createAction은 액션 생성자를 생성하는 함수로 액션 타입 문자열을 명시적으로 정의할 필요가 없으며, 액션 생성자가 액션 객체를 생성할 때 자동으로 타입과 페이로드를 설정합니다. 단일 액션을 생성하는 데 주로 사용됩니다.
// Redux Toolkit에서의 액션 타입과 액션 생성자 // @reduxjs/toolkit의 createAction을 import 합니다. import { createAction } from '@reduxjs/toolkit'; const increment = createAction('counter/increment'); // 자동으로 생성된 액션 타입: 'counter/increment' // 자동으로 생성된 액션 생성 함수: increment(payload)
 
  • createReducer
리덕스에서는 리듀서를 직접 작성해야 하며, 상태의 불변성을 유지하기 위해 새로운 상태 객체를 생성해야 했습니다. 또한 매번 이전 상태와 액션을 받아 새로운 상태를 반환하는 로직을 포함해야 했습니다.
// Redux에서의 리듀서 함수 function counterReducer(state = initialState, action) { switch (action.type) { case INCREMENT: return { ...state, count: state.count + action.payload }; default: return state; } }
리덕스 툴킷의 createReducer 함수를 사용하면 리듀서를 더 간단하게 생성할 수 있습니다. 리듀서 함수를 생성할 때 일반 객체의 프로퍼티를 변경하여 상태를 업데이트하기 때문에 불변성을 유지하면서도 코드를 간소화할 수 있습니다.
// Redux Toolkit에서의 리듀서 함수 // @reduxjs/toolkit의 createReducer를 import 합니다. import { createReducer } from '@reduxjs/toolkit'; const initialState = { count: 0 }; const counterReducer = createReducer(initialState, { // 일반 객체의 프로퍼티를 변경하는 방식 [increment]: (state, action) => { state.count += action.payload; }, });
 
  • createSlice
Redux Toolkit의 createAction 및 createReducer는 리덕스에서의 일반적인 액션 및 리듀서 작성에 비해 코드를 줄이고, 액션 타입 관리를 간편화하며 불변성을 보다 쉽게 유지할 수 있게 도와줍니다. 이로써 리덕스 코드를 더 효율적으로 작성하고 유지 보수하기 편하게 만들어줍니다. 하지만 기존의 리덕스 코드와 같이 매번 액션과 리듀서 함수를 따로 작성하고 관리해 줘야 하기 때문에 크게 효율성이 증가했다고 보기는 어렵습니다.
 
리덕스 툴킷에는 이러한 과정을 한 번에 처리해 주는 createSlice 함수가 있습니다. createSlice 함수는 리듀서와 액션 생성 함수를 함께 생성합니다. createAction과 마찬가지로 액션 타입 문자열과 액션 생성 함수가 자동으로 생성되며, 초기 상태(initialState)를 자동으로 추정하므로 별도로 정의할 필요가 없습니다. 또한 createReducer와 마찬가지로 객체의 프로퍼티를 변경하는 방식으로 상태를 업데이트 하므로 불변성을 유지하기 쉽습니다.
// Redux Toolkit의 createSlice를 이용한 액션 및 리듀서 함수 // @reduxjs/toolkit의 createSlice를 import 합니다. import { createSlice } from '@reduxjs/toolkit'; const counterSlice = createSlice({ name: 'counter', initialState: { count: 0, }, reducers: { increment: (state, action) => { state.count += action.payload; }, }, }); // 액션 타입: 'counter/increment' // 액션 생성자: increment() -> { type: 'counter/increment', payload: undefined }
일반적으로 createSlice를 사용하는 것이 리덕스 툴킷을 더 효율적으로 활용하는 방법입니다. createSlice는 액션과 리듀서를 한 번에 정의하고, 액션 타입 문자열(name)과 초기 상태(initialState)를 자동으로 처리해 주기 때문에 코드 작성과 유지 보수가 더 쉬워집니다. 그러나 프로젝트의 복잡성과 요구 사항에 따라 createAction 또는 createReducer를 선택할 수도 있습니다.
이 책에서는 createAction과 createReducer를 이용한 방법은 생략하고 createSlice를 통해 예제의 액션과 리듀서를 작성해 보겠습니다.

1) calculatorSlice

calculator에 대해 리덕스 슬라이스를 생성하는 코드를 작성하겠습니다. createSlice의 인자로 설정 내용(name, initialState, reducers)이 담긴 객체로 넘겨 슬라이스를 생성합니다. 만들어진 리듀서와 액션 생성 함수들은 스토어를 만들고 컴포넌트에서 디스패치 되는 데 사용되기 때문에 내보내기를 해줘야 합니다.
// store/calculator.js import { createSlice } from '@reduxjs/toolkit'; // 초기 상태를 객체로 정의합니다. const initialState = { value: 0 }; // createSlice 함수를 사용하여 Redux 슬라이스를 생성합니다. export const calculatorSlice = createSlice({ name: 'calculator', // 슬라이스의 이름을 설정합니다. initialState, // 초기 상태를 설정합니다. (반드시 initialState로 설정) reducers: { // 액션 생성함수와 리듀서를 정의합니다. // 각 액션은 현재 state를 변경하는 데 사용됩니다. // 숫자를 더하는 액션 addNumber: (state, action) => { state.value += Number(action.payload); // action.payload에 전달된 숫자로 현재 상태를 더합니다. }, subtractNumber: (state, action) => { state.value -= Number(action.payload); }, multiplyNumber: (state, action) => { state.value *= Number(action.payload); }, divideNumber: (state, action) => { state.value /= Number(action.payload); }, // 상태를 초기화하는 액션 resetNumber: (state) => { state.value = 0; // 상태를 0으로 재설정합니다. }, }, }); // 생성된 액션 생성함수들을 내보냅니다. export const { addNumber, subtractNumber, multiplyNumber, divideNumber, resetNumber } = calculatorSlice.actions; // 슬라이스의 리듀서를 내보냅니다. export default calculatorSlice.reducer;
 

2) todoListSlice

위와 동일한 작업을 todoList에 대해서도 진행하겠습니다. todoListSlice도 마찬가지로 만들어진 리듀서와 액션 생성 함수들이 다른 컴포넌트에서 사용되기 때문에 내보내기를 해줘야 합니다.
// store/todoList.js import { createSlice } from '@reduxjs/toolkit'; // 초기 상태를 정의합니다. const initialState = { todos: [] }; // createSlice 함수를 사용하여 Redux 슬라이스를 생성합니다. export const todoListSlice = createSlice({ name: 'todoList', // 슬라이스의 이름을 설정합니다. initialState, // 초기 상태를 설정합니다. reducers: { // 액션 생성자와 리듀서를 정의합니다. // 각 액션은 현재 상태를 변경하는 데 사용됩니다. // 할 일을 추가하는 액션 addTodo: (state, action) => { const newTodo = { id: state.todos.length, content: action.payload, isDone: false, }; state.todos = [...state.todos, newTodo]; // 현재 할 일 목록에 새로운 할 일을 추가합니다. }, // 할 일을 삭제하는 액션 deleteTodo: (state, action) => { state.todos = [...state.todos].filter((todo) => todo.id !== action.payload); // 주어진 ID와 일치하지 않는 할 일을 유지합니다. }, // 할 일을 토글(완료/미완료 상태 변경)하는 액션 toggleTodo: (state, action) => { state.todos = [...state.todos].map((todo) => (todo.id === action.payload ? { ...todo, isDone: !todo.isDone } : todo)); // 주어진 ID와 일치하는 할 일의 상태를 토글합니다. }, }, }); // 생성된 액션 생성자들을 내보냅니다. export const { addTodo, deleteTodo, toggleTodo } = todoListSlice.actions; // 슬라이스의 리듀서를 내보냅니다. export default todoListSlice.reducer;
 

3.2.4 configureStore

스토어를 설정하고 구성하는 함수인 configureStore는 리덕스 코어 라이브러리 표준 함수인 createStore를 추상화한 것입니다. 리덕스에서 createStore 사용 시 중앙선이 그어지며 deprecated 되었으니 리덕스 툴킷의 configureStore메서드 사용을 추천한다는 문구를 확인할 수 있는데, 그만큼 현시점 configureStore의 사용을 매우 권장하고 있습니다.

1) store 구성

configureStore를 사용하기 위해 리덕스 툴킷에서 제공하는 configureStore 함수를 import 해줘야합니다.
import { configureStore } from '@reduxjs/toolkit';
configureStore를 사용할 준비가 되었다면, 리덕스 툴킷의 createSlice로 생성한 슬라이스 객체의 reducer인 calculator와 todolist를 import 해줍니다. 그다음 configureStore 함수를 사용하여 스토어를 생성하고 아래와 같이 import한 슬라이스의 reducer를 등록하면 됩니다.
// store/configureStore.js // 스토어를 설정하기 위한 Redux Toolkit의 configureStore 함수 import { configureStore } from '@reduxjs/toolkit'; // 아래 import는 slice.reducer입니다. import calculator from './calculator'; import todoList from './todoList'; const store = configureStore({ // import한 slice의 리듀서입니다. // 내부 reducer에 s를 붙이지 않게 주의합니다. reducer: { calculator, todoList }, }); export default store;
configureStore를 통해 생성한 스토어를 App 컴포넌트 전역에서 사용하기 위해 Provider로 감싸 스토어의 value로 부여해 줍니다.
// index.js import React from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; import { Provider } from 'react-redux'; // 만들어 준 store를 import 합니다. import store from './store/configureStore'; const container = document.getElementById('root'); const root = createRoot(container); root.render( {/* Provider로 App컴포넌트를 감싸주고 configureStore로 생성한 store 값을 부여합니다. */} <Provider store={store}> <App /> </Provider> );
 

2) 스토어를 구성할 때 combineReducers 가 필요 없는 이유

위와 같이 리덕스 툴킷의 configureStore 함수는 리덕스 스토어 설정을 크게 단순화하고 개발자들에게 편의성을 제공합니다. 또한 configureStore는 여러 reducer를 rootReducer로 합치는 작업도 하는데, combineReducer를 사용할 필요없이 reducer 옵션에 객체 형태로 reducer를 전달하면 configureStore가 내부적으로 합쳐 사용합니다.
이 밖에도 개발환경에서 상태를 모니터링하고 디버깅에 용이한 도구인 redux devTools extentions을 자동으로 통합하고 있으며, 리덕스 스토어에 미들웨어를 추가하는 데 사용하는 applyMiddleware 또한 셋업 되어 redux-thunk나 redux-saga와 같은 미들웨어들을 간편하게 추가 할 수 있습니다.
 

3.2.5 Dispatch

리덕스 툴킷에서 디스패치를 사용하기 위해서는 리덕스에서 디스패치를 사용할 때처럼 컴포넌트 내부에서 react-redux 라이브러리 Hook 중 useDispatch를 가져오게 해줘야 합니다.
import { useDispatch } from 'react-redux';
 
또, 리덕스와 마찬가지로 useDispatch Hook을 사용하여 스토어의 디스패치 함수를 가져와 사용합니다.
const dispatch = useDispatch();
 
리덕스에서는 액션을 리듀서에 전달하기 위해 디스패치 함수에 직접 액션 객체를 인자로 넣거나 액션 생성 함수를 만들어 액션 타입을 입력해 줘야 했습니다.
//Redux에서의 dispatch dispatch(someActionCreator(someData));
 
리덕스 툴킷에서는 Slice에서 actions를 추출한 뒤, 액션의 종류를 전달해 사용합니다.
// Redux Toolkit에서의 dispatch // 1. slice에서 actions를 추출합니다. const actions = someSlice.actions; ... // 2. dispatch로 액션의 종류를 전달합니다. dispatch(actions.someAct(payload))
 

1) Calculator 컴포넌트에서 action 전달하기

먼저 calculatorSlice에서 추출한 액션 생성 함수를 가져오기 합니다. 버튼 클릭 시 액션 생성 함수는 디스패치 함수에 전달되고, 리듀서 함수를 통해 해당 액션에 일치하는 값으로 스토어의 state를 업데이트 시킵니다.
// src/components/Calculator.jsx import React from "react"; import { useState } from "react"; import { useSelector, useDispatch } from "react-redux"; // calculatorSlice에서 추출한 actions를 import 합니다. import { addNumber, subtractNumber, multiplyNumber, divideNumber, resetNumber, } from "../store/calculator"; export default function Calculator() { const result = useSelector((state) => state.calculator.value); // useDispatch 훅을 사용하여 store의 dispatch 함수를 가져옵니다. const dispatch = useDispatch(); const [inputValue, setInputValue] = useState(""); return ( <> <div>결과: {result} </div> <input type="number" value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> <div> {/* inputValue가 담긴 액션을 전달합니다. */} {/* 전달된 값은 해당 리듀서를 통해 업데이트됩니다. */} <button onClick={() => { dispatch(addNumber(inputValue)); }} > + </button> <button onClick={() => { dispatch(subtractNumber(inputValue)); }} > - </button> <button onClick={() => { dispatch(divideNumber(inputValue)); }} > / </button> <button onClick={() => { dispatch(multiplyNumber(inputValue)); }} > * </button> <button onClick={() => { dispatch(resetNumber()); }} > C </button> </div> <br /> </> ); }
 

2) TodoList 컴포넌트에서 action 전달하기

TodoList 컴포넌트에서도 위와 동일한 작업을 진행합니다.
// components/TodoList.jsx import { useState } from "react"; import { useDispatch, useSelector } from "react-redux"; // todoListSlice에서 actions를 가져오기 합니다. import { addTodo, deleteTodo, toggleTodo } from "../store/todoList"; export default function TodoList() { const todos = useSelector((state) => state.todoList.todos); const dispatch = useDispatch(); const [inputValue, setInputValue] = useState(""); return ( <> <form onSubmit={(e) => e.preventDefault()}> <input type="text" value={inputValue} placeholder="할 일을 작성해 주세요" onChange={(e) => setInputValue(e.target.value)} /> {/* 추가 버튼 클릭 시, inputValue값이 담긴 addTodo 액션을 dispatch를 통해 보내 해당 리듀서에서 값을 처리합니다. */} <button type="submit" onClick={() => { dispatch(addTodo(inputValue)); }} > 추가 </button> </form> {todos.map((todo) => ( <section key={todo.id}> {/* toggleTodo, deleteTodo 액션은 todo.id를 인자로 받아 처리합니다. */} <input type="checkbox" onClick={() => { dispatch(toggleTodo(todo.id)); }} /> <div style={{ textDecoration: todo.isDone ? "line-through" : "none", }} > {todo.content} </div> <button onClick={() => { dispatch(deleteTodo(todo.id)); }} > ❌ </button> </section> ))} </> ); }
이렇게 리덕스 툴킷을 사용하는 방법에 대해 알아보았습니다. 기존의 리덕스와 비교했을 때 코드가 훨씬 더 간결해진 것을 확인할 수 있습니다. 하지만 리덕스 툴킷이 코드를 간소화하고 더 편리한 기능을 제공한다고 해서 리덕스의 모든 단점을 해결해 주진 못합니다. 리덕스 툴킷은 결국 리덕스를 간결하게 사용하기 위한 것인데 리덕스를 이해하지 못하면 툴킷의 사용이 어렵기 때문에 높은 러닝 커브가 요구될 수밖에 없습니다. 그래서 리덕스가 불편하다고 해서 리덕스 툴킷을 사용하는 건 좋지 않습니다. 따라서 프로젝트에 필요한 게 무엇인지 판단하고 그에 알맞은 상태 관리 라이브러리를 선택하는 게 중요합니다.