Redux Toolkit은 마치 react-hook처럼, 기존 Redux의 불편함을 해소하고자 나온 또다른 redux 라이브러리입니다.
메인 화면에서도 볼 수 있듯, efficient Redux development입니다. 저는 React와 Redux를 프로덕션 레벨에서 사용해 본 경험이 없기 때문에 뭐라 말하기 그렇지만, Redux는 튜토리얼에서도 많은 코드들을 작성해야만 하는 불편함이 있습니다.
store
를 만듭니다.store
를 구성하는 state
들(혹은 reducer
)을 정의합니다.state
를 변경하는 action
을 정의합니다.action
을 구분하기 위한 action type constant
를 정의합니다.action creator
를 정의합니다.action
에따라 state
를 변경하는 reducer
를 정의합니다.즉, 어떤 하나의 행동을 위해서 action type constant
, action creator
, reducer
를 각각 다 만들어야 합니다.
물론 이런 기본 흐름은 Redux-Toolkit에서도 변하지 않았습니다. 하지만 이런 코드들을 좀 더 쉽고 빠르고 간단하게 효과적으로 도와주는 것이 Redux-Toolkit의 장점이라 할 수 있습니다.
저번 react-recoil-todo 시리즈에서 만들었던 똑같은 TodoApp을 또 만들어볼겁니다.
단, 이번엔 전역상태관리를 Redux-Toolkit을 이용해서 해볼 겁니다. 또한 recoil에서 이미 TodoApp의 기능적인 부분들은 설명을 했기 때문에, 이번 시리즈에서는 툴킷 API에 집중하도록 하겠습니다.
다음과 같이 개발 환경을 구성합니다.
$ npx create-react-app redux-toolkit-tutorial
$ cd redux-toolkit-tutorial
$ npm install react-redux @reduxjs/toolkit todomvc-app-css
$ npm start
src 디렉터리에서 App.js
, index.js
를 제외한 모든 파일을 삭제합니다.
그 다음, src/store.js
을 아래와 같이 생성합니다.
// src/store.js
import { configureStore } from '@reduxjs/toolkit'
export default configureStore({
reducer: {},
})
configureStore
는 기존 Redux의 store
를 쉽게 설정해주는 함수입니다. 우리는 간단한 TodoApp을 만들기 때문에 복잡한 설정은 필요 없습니다.
reducer
는 store
의 상태를 구성합니다. 이전 Recoil에서 atom
으로 상태를 만들었다면, Redux에서는 reducer
로 상태를 구성한다고 생각하셔도 무방합니다.
createSlice
우선 src/state/todos.js
를 아래와 같이 생성합니다.
// src/state/todos.js
import { createSlice } from '@reduxjs/toolkit'
let uniqId = 0
const todosSlice = createSlice({
name: 'todos',
initialState: {
filterType: 'all',
items: [],
},
reducers: {
add: {
reducer: (state, action) => {
state.items.push(action.payload)
},
prepare: text => {
return {
payload: {
id: ++uniqId,
done: false,
text,
},
}
},
},
},
})
export const { add } = todosSlice.actions
export default todosSlice.reducer
그리고 다시 src/store.js
에서 다음과 같이 코드를 추가해줍니다.
// src/store.js
import { configureStore } from '@reduxjs/toolkit'
// 👇👇👇
import todos from './state/todos'
export default configureStore({
reducer: {
// 👇👇👇
todos,
},
})
하나 하나 설명해보도록 하겠습니다.
createSlice
기존 Redux에서는 action type constant
, action creator
, reducer
를 다 따로 구현해야 했습니다. 이를 한방에 해주는게 createSlice
입니다.
예전에 작성했던 코드는 보통 이러했습니다.
// state/todos/action-types.js
export const ADD_TODO = 'todo/add'
export const REMOVE_TODO = 'todo/remove'
// state/todos/actions.js
import { ADD_TODO, REMOVE_TODO } from './action-types'
export const addTodo = text => ({
type: ADD_TODO,
payload: text,
})
export const removeTodo = id => ({
type: REMOVE_TODO,
payload: id,
})
// state/todos/index.js
import { ADD_TODO, REMOVE_TODO } from './action-types'
// reducer
const todos = (state = [], action) => {
switch (action.type) {
case ADD_TODO:
const text = action.payload
const todo = { id: uniqId(), done: false, text }
return [
...state,
todo,
]
case REMOVE_TODO:
const id = action.payload
return state.filter(todo => todo.id !== id)
default:
return state
}
}
export default todos
일반적으로 하나의 action이 하나의 reducer와 대응하는데, createSlice
에서는 reducer
만 정의하면 action type
, action creator
는 모두 아래와 같이 한번에 해결됩니다.
// src/state/todos.js
// 맨 하단
export const { add } = todosSlice.actions
여기서 add
는 action creator
이며, 이를 호출하면 기존에 Redux에서 만들었던 action creator
와 비슷하게 type
과 payload
로 이루어진 객체가 리턴됩니다.
이 때, type
은 createSlice
를 정의할 때 name
값과 reducers
의 key
값이 /
로 연결된 값이 됩니다. 즉, 이 경우 todos/add
입니다.
const todosSlice = createSlice({
// 👇 `name`
name: 'todos',
initialState: {
filterType: 'all',
items: [],
},
reducers: {
// 👇 `add`
add: {
reducer: (state, action) => {
state.items.push(action.payload)
},
prepare: text => {
return {
payload: {
id: ++uniqId,
done: false,
text,
},
}
},
},
},
})
export const { add } = todoSlice.actions
console.log(add('Hello')) // { type: 'todos/add', payload: { id: 1, done: false, text: 'Hello' }
initialState
reducer
의 기본 state
값을 정의합니다. reducer
에서 (state, action) => ...
의 state
가 바로 최초 initialState
입니다. action
이 dispatch
될 때마다 reducer
가 동작하여 계속 state
를 바꾸는 식으로 Redux는 작동합니다.
reducer
, prepare
prepare
는 payload
를 한 번 거치는 미들웨어 같은 함수입니다. action
을 dispatch
할 때, action
은 매개변수를 받는데, 이 매개변수의 값을 전처리 하는 역할을 합니다.
prepare
가 없는 형태가 일반적입니다.
reducers: {
add: {
reducer: (state, action) => {
// ...
},
prepare: text => {
// ...
},
},
filter: (state, action) => {
state.filterType = action.payload
},
}
action
의 매개변수를 전처리해야 한다면 reducer
와 prepare
를 정의하고, 그렇지 않다면 기본값이 reducer
이므로 그대로 정의하면 됩니다.
리액트 및 리덕스에서는 기본적으로 상태를 immutabilty
로 관리해야 합니다. 객체의 불변성에 관한 내용은 다른 좋은 포스팅 글인 링크를 참고하시면 되는데, 리액트와 Vue의 가장 큰 차이점이 바로 객체의 불변성을 관리하는 방식이었습니다.
리액트는 개발자가 직접 코드로 불변성을 관리해야합니다. 하지만 Vue는 객체의 setter
를 프록싱하여 라이브러리가 불변성을 관리해주었습니다. immer
가 하는 일이 바로 그런 일입니다.
redux-toolkit은 immer
가 내장되어 있으므로 객체 불변성 코드를 굳이 작성하지 않아도 됩니다. 현재 작성된 add
액션의 reducer
를 보면
reducers: {
add: {
reducer: (state, action) => {
// 👇👇👇
state.items.push(action.payload)
},
// ...
},
},
state.items
를 push
메서드로 변경하고 있습니다. 전통적인 redux에서는
const todo = { id: uniqId(), done: false, text }
return [
...state,
todo,
]
위와 같이 state
를 불변성으로 관리했습니다. immer
가 내장되어 있으므로 redux-toolkit에서는 코드를 좀 더 수월하게 작성할 수 있습니다.
redux-toolkit에 대해 간략히 정리해보았습니다. createSlice
를 이용하여 코드를 좀 더 쉽고 빠르게 작성할 수 있고, immer
의 내장기능으로 불변성 관리도 수월하게 할 수 있다는 점이 redux-toolkit의 가장 큰 장점이 아닐까 합니다.
다음 포스팅에서는 TodoApp에서 사용되는 action
들을 미리 정의하고, 기본 컴포넌트를 구현해보도록 하겠습니다.