Redux를 사용하는데 확실히 config 파일과 module 관련 파일을 작성하기 까다롭게 느껴졌다.
이 과정을 좀 더 쉽게 만들어주는 Redux toolkit이라는 패키지가 있다는 것을 알게 돼서 이번에 Redux만으로 만든 단순한 카운터 프로그램을 Redux toolkit으로 리팩토링하는 과정을 작성해볼 예정이다.
Redux toolkit은 기존 Redux의 ducks 패턴이 코드의 양도 많고 번거로워서 더 편하게 쓰기 위해 만들어진 패키지이다. Redux를 개량한 것이기 때문에 모듈 파일만 변화가 있고, 컴포넌트에서 useSelector를 통해 사용하는 것은 동일하다고 보면 된다.
따라서 Redux toolkit을 사용하려면 config 관련 파일, module 관련 파일만 다르게 작성하면 된다. index 파일에서 App 컴포넌트를 <Provider store={store}>
로 감싸는 것도 동일하다.
먼저 명령어를 통해 @reduxjs/toolkit
패키지를 설치한다.
yarn add react-redux @reduxjs/toolkit
Redux만으로 작성한 configStore 파일은 createStore
를 사용해서 단일 store를 만들어 모든 상태 tree를 관리하고, combineReducers
를 import해서 여러 개의 독립적인 reducer의 반환 값을 하나의 상태 객체로 만든다.
// Redux만으로 작성된 configStore.js
import { createStore } from "redux";
import { combineReducers } from "redux";
import counter from "../modules/counterSlice" // reducer 함수 이름
import todos from "../modules/todosSlice" // reducer 함수 이름
const rootReducer = combineReducers({
/* import한 module 파일들의 각 reducer 함수가 들어감 */
counter: counter,
todos: todos,
});
const store = createStore(rootReducer);
export default store;
위 코드에서 Redux Toolkit을 사용한다면, createStore
와 combineReducers
를 사용할 필요 없이 configureStore
만 import하여 바로 store에 할당하면 된다.
// Redux toolkit을 사용한 configStore.js
import { configureStore } from "@reduxjs/toolkit";
import counter from "../modules/counterSlice" // reducer 함수 이름
import todos from "../modules/todosSlice" // reducer 함수 이름
const store = configureStore({
/* import한 module 파일들의 각 reducer 함수가 reducer key의 value로 들어감 */
reducer: { counter: counter, todos: todos}
})
reducer 함수 import 파일명 뒤에 Slice가 붙은 이유는 Redux toolkit을 사용해 reducer 함수를 만들 때 createSlice
를 사용하여 Action Value
와 Action Creator
, Reducer
를 하나로 합쳐주기 때문에 파일명을 Slice를 붙이기도 한다고 한다. 근데 그냥 안붙여도 상관 없음!
일반적으로 Redux 모듈 파일은 human error를 방지하기 위한 Action Value
, Action Creator
, Reducer
로 이루어져 있다.
// Redux만으로 작성된 module 관련 파일 counter.js
// Action Value
const ADD_NUMBER = "ADD_NUMBER";
const MINUS_NUMBER = "MINUS_NUMBER";
// Action Creator
export const addNumber = (payload) => {
return {
type: ADD_NUMBER,
payload,
};
};
export const minusNumber = (payload) => {
return {
type: MINUS_NUMBER,
payload,
};
};
// Initial State
const initialState = {
number: 0,
};
// Reducer
const counter = (state = initialState, action) => {
switch (action.type) {
case ADD_NUMBER:
return {
number: state.number + action.payload,
};
case MINUS_NUMBER:
return {
number: state.number - action.payload,
};
default:
return state;
}
};
// export default reducer
export default counter;
Redux toolkit은 createSlice
라는 API를 활용해 이 세 가지를 하나로 합쳐준다.
// Redux toolkit을 이용한 module 관련 파일 counterSlice.js
import { createSlice } from "@reduxjs/toolkit"
const initialState = {
number: 0,
}
const counterSlice = createSlice({
name: "counter", // 필수 작성 - 모듈의 이름
initialState, // 필수 작성 - 모듈 초기 상태값
reducers: { // 필수 작성- 모듈의 reducer 로직
addNumber: (state, action) => {
state.number = state.number + action.payload;
},
minusNumber: (state, action) => {
state.number = state.number - action.payload;
},
},
});
// 컴포넌트에서 Action creator를 사용하기 위해 export
export const { addNumber, minusNumber } = counterSlice.actions;
// configStore에 등록하기 위해 export default reducer
export default counterSlice.reducer;
위 코드처럼 createSlice
인자 안에 설정 정보로 name
, initialState
, reducers
를 작성해주면, counterSlice reducer 객체 안에서 만들어주는 함수가 reducer의 로직이 되는 동시에 + Action Creator
가 되고 + Action Value
함수의 이름을 따서 자동으로 만들어지기 때문에 이 세 기능이 합쳐지게 된다. 따라서 Reducer만 만들어주면 되는 것!
여기서 export할 때 counterSlice.actions
처럼 .actions
를 붙여 Action creator
들(addNumber, minusNumber)을 각 컴포넌트에 export하고, configStore에 등록하는 .reducer
도 counterSlice
내의 reducers
가 아닌데, 이름이 비슷해서 순간 헷갈려서 찾아보니까 createSlice
가 반환하는 객체에서 꺼내오는 거였다.
createSlice
는 다음과 같은 객체를 반환한다.
{
name : string,
reducer : ReducerFunction,
actions : Record<string, ActionCreator>,
caseReducers: Record<string, CaseReducer>.
getInitialState: () => State
}
이 객체에서 reducer와 actions를 export하는 것!
확실히 세 가지 로직을 하나로 합쳐서 코드가 간결해졌고 가독성도 개선되었다! 그리고 Redux는 별도의 설정을 거쳐야 devtools를 사용 가능한데, toolkit을 사용하면 내장되어 있어서 별도 설정 없이 바로 사용이 가능해서 디버깅이 쉬울 듯 하다. 하지만 아직 익숙하지 않아서 그런가 틀을 계속 확인하면서 작성해야 하는 것 같다.