원문 : https://redux-toolkit.js.org/tutorials/intermediate-tutorial
주의 : 제가 이해하기 쉬운 방식으로 번역 및 첨언했습니다.
이전글 : https://develoger.local:80/redux-toolkit-basic-tutorial/
이번 글에서는 original Redux Todo App을 Redux-Toolkit을 사용한 Todo App으로 바꿔보면서 Redux-Toolkit의 사용법을 익혀볼것이다.
Reviewing the Redux Todos Example
original Redux Todo App을 보면 아래것들을 발견할 수 있다.
- todos reducer function 은 state의 불변성을 유지하기 위해서 "직접" object를 deep copy하고있다.
- actions file 에서 action 생성자 함수를 "직접" 작성하고 있다. 또한, aciton의 type을 string으로 "직접" 적어주고 있으며, 다른 파일에서 도 중복으로 "직접"적어서 사용하고 있다.
- 같은 context를 가진 action과 reducer를 한 파일에 두지 않고, 모든 action은 actions 파일에, 모든 reducer는 reducer파일에 보관한다. 상관없는 action들끼리 모여있으니 보기가 영 안좋다.
- "container/presentational" pattern에 따라서 화면에 어떻게 그려질지가 중요한 컴포넌트들(presentational)을 한 폴더에 넣고, 어떻게 작동(action dispatch)하는지가 중요한 컴포넌트들은 다른 폴더에 넣어놨다.
- 몇몇 코드들은 별로 좋은 pattern이 아니다. 이것도 찝어서 수정해 나갈것이다.
Initial Conversion Steps
original code 다운받기
redux github repository에서 redux를 다운받고, examples > todos 폴더만 남기고 다른건 다 지운다.
새 프로젝트 생성
original code의 module들은 좀 옛날꺼라서 아예 create-react-app으로 새 프로젝트를 만든 다음에 original code만 복사해오는 방식으로 한다.
npx create-react-app my-app --template redux
필요없는 코드들 지우기
src
폴더에 있는거 모두 지워준다. 그리고 public
폴더 안에있는것들도 지워준다.
코드 복붙
여기에 있는 소스코드들중 public과 src폴더를 복사해서 방금 만든 새 프로젝트에 붙여넣기한다. 그럼 다음과 같은 구조가 된다.

이제 npm run start
하면 잘 뜬다.

Converting the Store to Use configureStore()
index.js
에서 createStore
함수를 configureStore
로 대체해보자.
import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { configureStore } from '@reduxjs/toolkit';
import { Provider } from 'react-redux'
import App from './components/App'
import rootReducer from './reducers'
const store = configureStore({ reducer: rootReducer });
render(
<Provider store = { store } >
<App />
</Provider>,
document.getElementById('root'));
configureStore는 알아서 Redux DevTools Extension과 연동을 해주기 때문에 만약 크롬브라우저에 Redux DevTools Extension 이 설치되어 있다면, 아래와 같은 모습을 볼 수 있다.

Creating the Todos Slice
original todo는 action과 reducer를 떨어뜨려놓았다. 이거를 하나의 slice로 묶어보자
Understanding Slices
우리는 RTK(redux toolkit)의 createAction()
과 createReducer()
를 사용해서 original todo의 reducers/todos.js
와 actions/index.js
를 더 깔끔하게 작성할수있다. 하지만, 실질적으로 createSlice()
만 써도 된다. createSlice()
는 같은 맥락의 action과 reducer를 하나로 묶어주기 때문에 코드를 더 깔끔하고 이해하기 쉽게 작성할 수 있다. 그리고 createSlice()
내부적으로 createAction()
과 createReducer()
를 사용한다.
근데 'slice' 가 뭘 의미하는거지?
combineReducers()
state tree는 내 App의 상태를 보관하는 JS Object이다. 이 state tree는 reducer의 return값으로 계속 새걸로 교체가 되는데,

프로젝트가 커지면 하나의 reducer안에 관련없는 것들이 주루룩 생기는게(switch - case) 단점이다.
// 자판기와 쓰레기통의 action에 반응하는 reducer
const BigReducer = (state = [], action) => {
switch (action.type) {
case '자판기에돈넣기':
return [...state, {
...
}];
case '음료뽑기':
return [...state, {
...
}];
case '쓰레기통문열기':
return [...state, {
...
}];
case '쓰레기통문닫기':
return [...state, {
...
}];
default:
return state;
}
}
그래서 만들어진것이 combineReducer()
이다. 이 함수를 사용하면 state tree안에서 관련이 있는 부분만 담당하는 reducer를 하나로 묶을 수 있다.
const 자판기리듀서 = (state = [], action) => {
switch (action.type) {
case '자판기에돈넣기':
return [...state, {
...
}];
case '음료뽑기':
return [...state, {
...
}];
default:
return state
}
}
const 쓰레기통리듀서 = (state = [], action) => {
switch (action.type) {
case '쓰레기통문열기':
return [...state, {
...
}];
case '쓰레기통문닫기':
return [...state, {
...
}];
default:
return state
}
}
const 종합리듀서 = combineReducers({
자판기관련상태: 자판기리듀서,
쓰레기통관련상태: 쓰레기통리듀서
});
const store = configureStore({
reducer: 종합리듀서
});
그래서 자판기리듀서
가 뿜어내는 state값은 state tree의 자판기관련상태
가지(?) 에만 들어가고, 쓰레기통리듀서
가 뿜어내는 state값은 state tree의 쓰레기통관련상태
가지에만 들어간다.
자 그래서, createSlice()
의 slice는 자판기상태
를 의미하는거고, slice reducer는 자판기리듀서
를 의미한다. 포괄적으로는 저 key-value를 하나의 slice로 봐도 될것같다.
Examining the Original Todos Reducer
const todos = (state = [], action) => {
switch (action.type) {
case "ADD_TODO":
return [
...state,
{
id: action.id,
text: action.text,
completed: false,
},
];
case "TOGGLE_TODO":
return state.map((todo) =>
todo.id === action.id
? {
...todo,
completed: !todo.completed,
}
: todo
);
default:
return state;
}
};
export default todos;
- 일반적인 reducer이다.
- immer 같은 상태 불변성 관리 라이브러리를 안쓰니, 직접 state를 deep copy하고있다.
- initial state는
[]
이다. - 어떤 action type도 switch-case에 맞지 않는 경우(default)에는 기존의 state를 돌려준다.
추가적으로, map함수의 사용법은 다음과 같다.
const days = ["Mon", "Tue", "Wed", "Thus", "Fri"];
const happyDay = days.map((day, index) => {
return `너무 좋은 ${day}`;
});
console.log(happyDay); // ["너무 좋은 Mon", "너무 좋은 Tue", "너무 좋은 Wed", "너무 좋은 Thus", "너무 좋은 Fri"]
Writing the Slice Reducer
위의 todos
reducer를 createSlice()
를 활용해 좀 더 심플하게 바꿔보자.
먼저는, features
폴더를 src
폴더 안에 만들고, /features/todos/todosSlice.js
파일을 생성한다. 그리고 아래와 같이 코드를 작성한다.
import { createSlice } from '@reduxjs/toolkit'
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo(state, action) {
const { id, text } = action.payload;
state.push({
id,
text,
completed: false
});
},
toggleTodo(state, action) {
const todo = state.find(todo => todo.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
}
}
});
export const { addTodo, toggleTodo } = todosSlice.actions;
export default todosSlice.reducer;
코드를 살펴보자.
slice
의name
은'todos'
이다. 이name
은state tree
의key
이다. 동시에 앞으로 들어올action
의 prefix이다. 예를들어서'todos/addTodo'
라는 액션이 들어올것이다.initialState
로[]
이 들어왔다.reducers
에는 두가지 함수를 넘겨준다.'todos/addTodo'
라는 action이 들어오면addTodo()
가 호출될것이고,'todos/toggleTodo'
라는 함수가 들어오면toggleTodo()
가 호출될것이다.- default handler는 없다. 맞지않는 action이 들어오면 별 반응 안하도록 내부적으로 세팅되어있다.
"Mutable" Update Logic
addTodo()
안에서 state.push({...})
를 하고있다. 원래 javascript의 Array.push를 해버리면 state의 불변성이 훼손되버리는데, RTK는 내부적으로 immer library를 사용하기 때문에 문제 없다.
toggleTodo() 도 마찬가지로 state의 한 slice인 todo를 찾아서(find) completed property를 변경했다. 이것또한 immer가 해당 객체의 변화를 감지하고 새로운 state를 만들어서 불변성을 유지해준다. 내부적으로 어떻게 이렇게 하는지는 모르겠지만, 아무튼 내가 막 바꿔도 그거는 새로운 객체를 바꾸는것이지, 원래의 객체를 바꾸는게 아니다.
Exporting the Slice Functions
createSlice()
는 대충 이렇게 생긴 객체를 리턴한다.
{
name: "todos",
reducer: (state, action) => newState,
actions: {
addTodo: (payload) => ({
type: "todos/addTodo",
payload
}),
toggleTodo: (payload) => ({
type: "todos/toggleTodo",
payload
})
},
caseReducers: {
addTodo: (state, action) => newState,
toggleTodo: (state, action) => newState,
}
}
주의할점은 createAction()
의 두번째 인자로 callback function이 들어가는데, 이 함수는 payload를 key로 가지는 객체를 리턴해야한다!
여기서 잠깐!
createSlice()함수를 보면 addTodo를 reducer로만 활용되는것처럼 보인다. 'todos/addTodo' action type을 받으면 같이 날라온 payload를 바탕으로 state를 업데이트한다.
import { createSlice } from '@reduxjs/toolkit'
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo(state, action) {
const { id, text } = action.payload
state.push({ id, text, completed: false })
},
toggleTodo(state, action) {
const todo = state.find(todo => todo.id === action.payload)
if (todo) {
todo.completed = !todo.completed
}
}
}
})
export const { addTodo, toggleTodo } = todosSlice.actions
export default todosSlice.reducer
근데, createAction()은 두번째 인자로 callback 함수를 넣어서 payload의 값을 내가 직접 만들 수 있다. 다시말해서 createSlice()는 creatAction으로 만들어진 action creator를 대체할 수 없다. 내가 직접 payload를 만들어서 action에 붙여서 dispatch해야하기 때문이다.
createSlice()는 이런 경우도 다~ 대비를 해놓았다. 아래처럼 쓰면 payload를 작성할 수 있는 callback함수를 사용할 수 있다.
import { createSlice } from '@reduxjs/toolkit'
let nextTodoId = 0
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: {
reducer(state, action) {
const { id, text } = action.payload
state.push({ id, text, completed: false })
},
prepare(text) {
return { payload: { text, id: nextTodoId++ } }
}
},
toggleTodo(state, action) {
const todo = state.find(todo => todo.id === action.payload)
if (todo) {
todo.completed = !todo.completed
}
}
}
})
Using the New Todos Slice
Updating the Root Reducer
rootReducer의 todos를 아래처럼 수정한다.
import { combineReducers } from 'redux'
import todosReducer from '../features/todos/todosSlice';
import visibilityFilter from './visibilityFilter'
export default combineReducers({
todos: todosReducer,
visibilityFilter
});
이것은 당연히 /features/todos/todosSlice
가 reducer를 export했기 때문이다.
const todosSlice = createSlice({
...
})
export const { addTodo, toggleTodo } = todosSlice.actions
export default todosSlice.reducer
Updating the Add Todo Component
지금까지 수정한거를 빌드해서 보면 화면에 그려지기는 하는데 AddTodo action이 제대로 작동을 안한다.

우리가 createSlice()
로 만든 reducer는 'todos/addTodo'
를 기다리고 있는데, AddTodo.js
에서는 'ADD_TODO'
type의 action을 보내주고 있기 때문에 제대로 작동을 안하는것이다. 그래서 아래와 같이 변경해준다.
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../features/todos/todosSlice'
// todoSlice에서 addTodo action creator를 가져옴
const AddTodo = ({ dispatch }) => {
let input
return (
<div>
<form onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
dispatch(addTodo(input.value))
input.value = ''
}}>
<input ref={node => input = node} />
<button type="submit">
Add Todo
</button>
</form>
</div>
)
}
export default connect()(AddTodo)
todoSlice.js의 상태는 아래와 같다. (혹시 햇갈릴까봐)
import { createSlice } from '@reduxjs/toolkit'
let nextTodoId = 0
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: {
reducer(state, action) {
const { id, text } = action.payload
state.push({ id, text, completed: false })
},
prepare(text) {
return { payload: { text, id: nextTodoId++ } }
}
},
toggleTodo: (state, action) => {
const todo = state.find(todo => todo.id === action.payload)
if (todo) {
todo.completed = !todo.completed
}
}
}
})
export const { addTodo, toggleTodo } = todosSlice.actions
export default todosSlice.reducer
이렇게 action creator를 변경하긴 했지만, AddTodo Componenet는 요즘 React Style이 아니다. (1) 먼저는 Hook을 안썼다.
Hook을 써서 다시 코드를 재작성해보자.
import React, { useState } from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../features/todos/todosSlice'
const AddTodo = ({ dispatch }) => {
let input
const [todoText, setTodoText] = useState('');
return (
<div>
<form onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
dispatch(addTodo(input.value))
setTodoText(todoText)
}}>
<input ref={node => input = node} />
<button type="submit">
Add Todo
</button>
</form>
</div>
)
}
export default connect()(AddTodo)
대단히 바뀐건 없다. useState만 사용했을 뿐이다.
(2) ref를 사용했다. ref대신에 글자가 바뀔때마다 state를 업데이트 해주는게 요즘 react style이다.
import React, { useState } from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../features/todos/todosSlice'
const AddTodo = ({ dispatch }) => {
const [todoText, setTodoText] = useState('');
const onChange = e => setTodoText(e.target.value);
return (
<div>
<form onSubmit={e => {
e.preventDefault()
if (!todoText.trim()) {
return
}
dispatch(addTodo(todoText))
setTodoText(todoText)
}}>
<input value={todoText} onChange={onChange}/>
<button type="submit">
Add Todo
</button>
</form>
</div>
)
}
export default connect()(AddTodo)
(3) 마지막으로 mapDispatch를 안썻다. 기본적으로 컴포넌트와 redux store를 연결(connect)시키면 해당 컴포넌트는 props로 dispatch함수를 받아서 써도 문제는 없는데, 사실 connect()의 두번째 인자로 action creator를 넘겨주는게 정석이다.
import React, { useState } from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../features/todos/todosSlice'
const mapDispatch = { addTodo }
const AddTodo = ({ addTodo }) => {
const [todoText, setTodoText] = useState('');
const onChange = e => setTodoText(e.target.value);
return (
<div>
<form onSubmit={e => {
e.preventDefault()
if (!todoText.trim()) {
return
}
addTodo(todoText)
setTodoText(todoText)
}}>
<input value={todoText} onChange={onChange}/>
<button type="submit">
Add Todo
</button>
</form>
</div>
)
}
export default connect(null, mapDispatch)(AddTodo)
Updating the Todo List
VisibleTodoList.js 도 AddTodo.js와 마찬가지로 예전 방식의 action creator를 사용하고 있기 때문에 이거를 수정해주자.
import { connect } from 'react-redux'
import TodoList from '../components/TodoList'
import { toggleTodo } from '../features/todos/todosSlice'
import { VisibilityFilters } from '../actions'
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(t => t.completed)
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(t => !t.completed)
default:
throw new Error('Unknown filter: ' + filter)
}
}
const mapStateToProps = state => ({
todos: getVisibleTodos(state.todos, state.visibilityFilter)
})
const mapDispatchToProps = { toggleTodo }
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
Creating and Using the Filters Slice

저 필터 로직도 createSlice()
를 활용해서 더 심플하게 표현해보자.
Writing the Filters Slice
퍼져있는 filter관련 action
과 reducer
를 하나로 묶기위해서 /features/filters/filterSlice.js
를 아래와 같이 생한다.
import { createSlice } from '@reduxjs/toolkit'
export const VisibilityFilters = {
SHOW_ALL: 'SHOW_ALL',
SHOW_COMPLETED: 'SHOW_COMPLETED',
SHOW_ACTIVE: 'SHOW_ACTIVE'
}
const filtersSlice = createSlice({
name: 'visibilityFilters',
initialState: VisibilityFilters.SHOW_ALL,
reducers: {
setVisibilityFilter(state, action) {
return action.payload
}
}
})
export const { setVisibilityFilter } = filtersSlice.actions
export default filtersSlice.reducer
/actions/index.js
에 있던 VisibilityFilters
를 지우고 여기로 가져왔다. 이렇게 하면 VisibilityFilters
를 사용하고 있는 파일에서 에러가 막 나올것이다. 이거를 먼저 수정해주자.
/containers/VisibleTodoList.js
에도 아래와 같이 수정해주고
...
import { VisibilityFilters } from '../features/filters/filterSlice'
...
/components/Footer.js
도 아래와 같이 수정해준다.
...
import { VisibilityFilters } from '../features/filters/filterSlice'
...
Using the Filters Slice
우선 filterSlice
의 reducer
를 combineReducers
에 적용해야 하니까 /reducers/index.js
를 아래와 같이 수정한다.
import { combineReducers } from 'redux'
import todosReducer from '../features/todos/todosSlice';
import visibilityFilterReducer from '../features/filters/filterSlice';
export default combineReducers({
todos: todosReducer,
visibilityFilter: visibilityFilterReducer
});
그리고 실제적으로 action
이 dispatch
되는 /containers/FilterLink.js
를 아래와 같이 수정해준다.
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../features/filters/filterSlice'
import Link from '../components/Link'
const mapStateToProps = (state, ownProps) => ({
active: ownProps.filter === state.visibilityFilter
})
const mapDispatchToProps = {
setVisibilityFilter
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Link)
그리고 /componenets/Link.js
는 다음과 같이 수정한다.
import React from 'react'
import PropTypes from 'prop-types'
const Link = ({ active, children, setVisibilityFilter, filter }) => (
<button
onClick={() => { setVisibilityFilter(filter) }}
disabled={active}
style={{
marginLeft: '4px',
}}
>
{children}
</button>
)
Link.propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
setVisibilityFilter: PropTypes.func.isRequired,
filter: PropTypes.string.isRequired,
}
export default Link
filter
는 /components/Footer.js
에서 props
로 전달된거고, setVisibilityFilter
는 /containers/FilterLink.js
에서 mapDispatchToProps
로 전달해 준거다.
Optimizing Todo Filtering
/containers/VisibleTodoList.js
파일을 보면
...
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(t => t.completed)
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(t => !t.completed)
default:
throw new Error('Unknown filter: ' + filter)
}
}
const mapStateToProps = state => ({
todos: getVisibleTodos(state.todos, state.visibilityFilter)
})
const mapDispatchToProps = { toggleTodo }
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
이렇게 되어있는데, 이게 아주 비효율적인 코드이다. 왜냐하면, TodoList
를 redux store
와 connect
해놨기 때문에, redux store에 어떤 action
이 와서 store
의 state
값이 바뀌면 TodoList
에 props
로 getVisibleTodos()
를 호출한 결과값인 todos
를 넘겨주기 때문이다. filtering과 아무 상관없는 action
때문에 getVisibleTodos()
가 계속 호출되는건 너무 비효율적이다. getVisibleTodos()
는 새로운 배열을 만들어서 return
해주기 때문이다. 그리고 이것은 TodoList
컴포넌트의 re-rendering을 야기한다.

그래서 이 문제를 reselect
라는 라이브러리로 해결 할 수 있는데, RTK안에 이미 포함되어 있어서 바로 쓸 수있다.
import { connect } from 'react-redux'
import TodoList from '../components/TodoList'
import { toggleTodo } from '../features/todos/todosSlice'
import { VisibilityFilters } from '../features/filters/filterSlice'
import { createSelector } from '@reduxjs/toolkit'
const selectTodos = state => state.todos
const selectFilter = state => state.visibilityFilter
const selectVisibleTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(t => t.completed)
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(t => !t.completed)
default: throw new Error('Unknown filter: ' + filter)
}
}
)
const mapStateToProps = state => ({
todos: selectVisibleTodos(state)
})
const mapDispatchToProps = { toggleTodo }
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
createSelector()
의 핵심은, state.todos
나 state.visibilityFilter
중 하나라도 이전(cache 해놓음)과 다른게 있으면 (todos, filter) => { switch ... }
함수를 호출하고, 이전과 같다면 호출하지 않고 저장해놓은 값을 돌려준다는 것이다.
이렇게 해서 아무 상관없는 action
이 dispatch
되도 새로운 todos
배열을 만들지도 않고, 또 그래서 TodoList
는 re-rendering
되지 않는다.
Cleanup
이제 안쓰는 파일들을 지워야한다. actions/index.js
, reducers/todos.js
, visibilityFilter.js
는 이제 확실이 안쓰니까 지운자.
그리고 컴포넌트들도 관련있는 feature폴더에 넣어준다. 그래서 아래와 같은 폴더 구조를 갖게된다.

Summary
이번 글에서 우리는 다음과 같은것들을 알게되었다.
action
과reducer
를createSlice()
를 사용해 하나로 묶을 수 있다.reducer
가return
하는state
를 편하게 수정했다. (기본으로 포함되어있는immer.js
덕분에)prepare
를 통해서payload
를 dynamic하게 만들 수 있다.createSelector()
를 통해 불필요한state
의 변경을 줄일 수 있다. (re-render도 줄어든다)mapDispatch
를 통해action
을 좀 더 깔끔하게dispatch
할 수있다.- "feature folder" 구조로 파일을 관리하는게 기존의 방식(컴포넌트-action-reducer나누기) 보다 관리하기가 좋다.