📘

3.1 Redux Toolkit 개념

3.1.1 Redux Toolkit

앞에서 살펴봤던 리덕스의 코드들은 복잡하고 반복적인 코드가 많이 필요했습니다.
하나의 상태를 관리하기 위해 해당 액션을 생성하는 액션 생성자 함수를 작성하고, 액션 유형에 대한 액션 타입 상수를 만들고, 초기 상태를 설정하는 리듀서 함수를 작성해야 하는 등 기본적인 코드 양이 많았습니다. 또한, 리덕스를 잘 활용하기 위해서 여러 패키지를 추가해야 했으며, 상태 업데이트 시 불변성을 유지하기 위한 로직이 따로 필요했습니다.
이러한 리덕스의 몇 가지 문제를 개선하기 위해 리덕스 툴킷(Redux Toolkit)이 등장하게 되었습니다.
리덕스 툴킷은 리덕스 팀에서 만들어진 공식적으로 권장되는 헬퍼 라이브러리로서, 리덕스 로직을 작성하기 위한 표준 방식이 되도록 만들어졌습니다. 리덕스만 사용할 때와 비교하여, 리덕스 툴킷을 사용하면 여러 가지 이점이 있습니다. 특히, 액션 타입이나 액션 생성자, 그리고 리듀서 등을 따로 정의할 필요가 없다는 점은 매우 유용합니다.
 
아직 낯설겠지만 간단하게 코드를 통해 비교해 보겠습니다.
// 액션 타입 정의 const INCREMENT = "INCREMENT"; // 액션 생성자 함수 정의 function increment() { return { type: INCREMENT }; } // 초기 상태 정의 const initialState = { count: 0, }; // 리듀서 함수 정의 function counterReducer(state = initialState, action) { switch (action.type) { case INCREMENT: return { ...state, count: state.count + 1 }; default: return state; } } // 스토어 생성 const store = createStore(counterReducer);
이 코드는 리덕스를 이용해 카운터 값을 증가시키는 간단한 기능을 구현하고 있습니다. 리덕스만 사용할 때는 액션 타입과 액션 생성자 함수를 따로 정의해야 하며, 리듀서에서 상태를 불변성을 유지하면서 업데이트해야 합니다.
 
import { createSlice, configureStore } from "@reduxjs/toolkit"; const counterSlice = createSlice({ name: "counter", initialState: { count: 0 }, reducers: { increment(state) { state.count += 1; }, }, }); export const { increment } = counterSlice.actions; const store = configureStore({ reducer: counterSlice.reducer, });
반면에 리덕스 툴킷에서는 createSlice라는 함수를 통해 액션 타입과 액션 생성자 함수, 그리고 리듀서를 한 번에 정의할 수 있습니다. 또한 내부적으로 Immer 라이브러리가 동작하기 때문에 상태를 마치 직접 수정하는 것처럼 코드를 작성할 수 있습니다. 실제로는 불변성을 지키면서 새로운 상태를 생성해 주기 때문에 안정성도 보장됩니다. 리덕스 툴킷 사용법은 다음 장에서 자세히 다룰 예정이니 지금은 구조적인 차이점만 이해하시면 됩니다.
 
예시에 등장한 Immer 라이브러리처럼 리덕스 툴킷은 리덕스에서 사용자마다 다르게 설치하던 패키지나 라이브러리를 아예 내장시켜 작업을 단순화시키고 실수를 방지하게끔 해줍니다. 비동기 로직 처리 역시 간단해집니다. 비동기 api 통신을 위한 미들웨어인 redux-thunk가 내장되어 있어 npm이나 yarn으로 따로 설치할 필요가 없이 제공되는 createAsyncThunk라는 함수로 비동기 작업 처리 패턴을 단순화시킬 수 있습니다.
 
마지막으로 모든 애플리케이션에서 같은 구조와 패턴을 재사용함으로써 유지 보수 및 협업 시 이점을 제공합니다. 리덕스 툴킷 사용이 필수는 아니지만 가져다주는 이점이 분명한 만큼, 리덕스 공식 문서에서 말하듯 리덕스 앱에 리덕스 툴킷을 적용하는 것을 적극 권장합니다! 🎉
 

3.1.2 리덕스 툴킷의 주요 구성요소

1) configureStore

configureStore는 Redux Toolkit(리덕스 툴킷)에서 제공하는 함수로 Redux store를 생성하고  리덕스의 문제점(앞선 3.3.2 리덕스의 단점 파트에서 소개된 복잡한 스토어 설정, 추가되어야 하는 많은 패키지들, 보일러 플레이트 등)을 개선하여 추가로 설치할 필요 없이 기본 미들웨어(middleWare)와 리덕스 개발자 도구(Redux Devtools Extension)을 활성화하여 사용 가능하며 리덕스 코드를 간단하고 효율적으로 작성할 수 있게 도와줍니다. 현재 리덕스 버전에서는 configureStore의 사용을 권장하고 있습니다. configureStore에 전달하는 객체 내부에는 필수적으로 리듀서가 필요하며, 개발자가 선택적으로 middleware, devTools, preloadedState 등을 추가할 수 있습니다.
import { configureStore } from "@reduxjs/toolkit"; import authReducer from "./auth"; import counterReducer from "./counter"; import { logger } from "redux-logger"; //logger 사용 패키지 const store = configureStore({ reducer: { auth: authReducer, counter: counterReducer }, //선택 옵션들(middleware, devtools, preloadedState 등) middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger), //미들웨어 목록 devTools: process.env.NODE_ENV !== "production", // 리덕스 개발자 도구 on/off preloadedState: { counter: 0, auth: { isAuthenticated: false }, }, //리듀서 초기 상태 값 지정 }); export default store;
 
configureStore 함수 안 파라미터 객체들 중 reducer 정보는 필수로 작성해야 하며, 필요에 따라 middleware, devTools, preloadedState 정보를 사용할 수 있습니다. configureStore에 전달되는 각 객체 파라미터의 옵션들은 다음과 같습니다.
 
(1) reducer
개별 리듀서를 별도로 결합하는 작업(combineReducer) 없이도 여러 리듀서를 결합하여 하나의 루트 리듀서를 만들어 줍니다. 위와 같이 reducer: {auth : authReducer, counter : counterReducer} 이렇게 작성하면 자동으로 2 개의 슬라이스 리듀서(authReducer, counterReducer)를 내부적으로 combineReducers가 호출되어 1 개의 리듀서로 묶어주게 됩니다.
 
(2) middleware
리덕스 미들웨어는 디스패치된 액션과 리듀서 사이에서 개발자의 목적에 따라 동작하는 기능입니다. 리덕스 툴킷에 내장된 Redux Thunk와 함께 작동하며 미들웨어를 사용하는 방법은 미들웨어 프로퍼티에 getDefaultMiddleware()를 사용해서 미들웨어 배열에 concat 메서드를 추가하고 원하는 미들웨어를 추가해주면 됩니다. 예를 들어 콘솔에 로그를 출력 해주는 미들웨어 logger를 추가해 보겠습니다. 먼저 npm install redux-logger 하여 패키지를 설치해 주고 import를 하고 concat에 logger를 넣어주면 액션이 디스패치될 때마다 콘솔에 찍히게 됩니다. 또는  이런 식으로 개발자가 원하는 미들웨어를 추가하여 사용할 수 있습니다.
notion imagenotion image
getDefaultMiddleware 함수가 반환하는 값은 배열이기 때문에 배열메서드 concat을 사용하여 배열의 순서대로 미들웨어를 실행시켜 데이터를 가공해 리듀서로 넘기는 형식입니다. middleware 객체를 생략하게 되면, configureStore 함수가 자동으로 기본 미들웨어를 추가해 줍니다. 기본 미들웨어에는 3가지가 있습니다.
  • thunk : 비동기 작업을 처리하기 위한 미들웨어이며, 액션 생성자가 객체 대신 함수를 반환할 수 있게 합니다.
  • imnutable-state-invariant : 리덕스의 원칙 중 하나인 상태는 읽기 전용인데, 리듀서가 상태를 직접 수정하려고 하면 에러를 출력해 줍니다.
  • serializable-state-invariant : 액션과 상태의 값이 직렬화 가능한지 검사합니다. 직렬화가 가능하면 리덕스 개발자 도구는 액션과 상태 변화를 기록하여 시간 여행 디버깅을 지원하게 됩니다.
 
(3) devTools
Redux DevTools는 상태 변화 및 디버깅을 위한 도구이며, 브라우저의 리덕스 개발자 도구와 연동하여 애플리케이션 상태를 디버깅합니다. 앞서 리덕스 한계점 파트에 Redux DevTools 사용법을 살펴보시면 devtools를 사용하기 위해 window.**REDUX_DEVTOOLS_EXTENSION**&&window.**REDUX_DEVTOOLS_EXTENSION**() 를 작성하거나 redux-devtools-extension 패키지를 따로 설치해야 했습니다. 하지만 configureStore를 사용하는 경우 자동으로 Redux DevTools가 있기 때문에 따로 설정할 필요가 없고, Redux DevTools 확장 프로그램을 통해 리덕스 애플리케이션의 상태와 로그를 확인할 수 있습니다. 기본값은 true이며 false로 설정하게 되면, 확장 프로그램이 비활성화됩니다.  코드에서 devTools: process.env.NODE_ENV !== "production" `조건을 통해 실행환경이 프로덕션 모드(최종적으로 사용자에게 배포되는 버전, 개발 버전의 대비되는 개념)인지 확인하고 프로덕션 모드가 아니라면 Reudx DevTools를 활성화합니다. devTools: true는 항상 Redux DevTools이 활성화되어 있기 때문에 프로덕션 환경에서 불필요한 성능 저하가 발생할 수 있기 때문에 프로덕션 모드에선 Redux DevTools를 비활성화하는 것은 좋은 습관입니다만, 필수적으로 작성해야 하는 코드는 아닙니다.
 
(4) preloadedState
리덕스 스토어의 초기 상태를 설정합니다. 이 값은 루트 리듀서가 처음 호출될 때만 사용됩니다. 일반적으로는 리듀서 함수에서 초기 상태를 정의하기 때문에 대부분 생략하지만 경우에 따라 애플리케이션 상태를 서버에서 미리 로드해야 할 일이 생길 때 사용합니다.
 

2) createSlice

createSlice는 식별자 이름과 상태의 초기값, 그리고 리듀서 함수들을 필수 객체로 받으며 액션 타입과 액션 함수를 자동으로 생성해 주는 역할을 합니다. 또한 redux-toolit의 createSlice에서는 객체의 불변성을 지켜줄 수 있도록 도와주는 Immer 라이브러리가 내장되어 있어 기존 상태의 복사본을 만들지 않고도 기존의 상태를 수정하는 로직을 구현할 수 있습니다.
(1) createSlice의 필수 객체
  • name : 해당 slice 상태의 식별자에 대해 문자열로 정의합니다. 중복된 값을 정의해서는 안 되며 고유한 값을 지정해 주어야 합니다.
  • initialState : 이 상태의 초기값에 대해서 할당을 해줍니다.
  • reducers : 상태를 변경하는 액션 함수를 정의합니다. toolkit에서 함수 내부에 받는 파라미터엔 상태 값을 불러오는 state와 전달받은 값을 처리하는 action.payload가 있습니다. 리덕스에서는 action의 타입에 따라 액션 함수를 불러와서 상태에 대한 불변성 처리를 했지만 toolkit에서는 함수 하나만 정의하는 것으로 해결할 수 있습니다.
 
(2) Immer
Immer는 불변성을 유지하면서 복잡한 배열이나 객체를 쉽게 업데이트하기 위한 라이브러리입니다. Redux Toolkit에서 Immer를 사용하면 리듀서에서 불변성을 유지하면서 상태를 업데이트하는데 코드 상으로는 객체나 배열을 직접 수정하는 것처럼 코드를 작성하지만, 내부적인 동작으로는 불변성을 유지하면서 새로운 상태를 생성합니다. 이를 통해 코드를 더 간단하고 쉽게 작성할 수 있으며 버그를 줄이며 유지 보수에 용이합니다.
// createSlice 예시 import { createSlice } from "@reduxjs/toolkit"; const counterReducer = createSlice({ // 식별자 name, initialState, 리듀서 함수 reducers를 key값으로 받기 name: "todoReducer", initialState: [{ id: 0, todo: "todo", isDone: false }], reducers: { // 리듀서 함수 // Immer를 통해 원본 상태를 복사하지 않고 상태를 수정 addTodo(state, action) { state.push(action.payload); }, toggleTodo(state, action) { const toggleTodo = state.find((todo) => todo.id === action.payload); if (toggleTodo) toggleTodo.isDone = !toggleTodo.isDone; }, deleteTodo(state, action) { state - state.filter((todo) => todo.id !== action.payload); }, }, });