Redux 마스터하기 - 리덕스의 미들웨어에 대하여 알아보자.
Redux
는 Action
이 Reducer
에 전해지기 전 해야할 작업을 정의할 수 있는 미들웨어를 지원한다. Redux
의 비동기 작업들을 도와주는 미들웨어에 대하여 알아보자.
let todoId = 0;
export const addTodo = text =>
new Promise(resolve => {
window.setTimeout(() => {
resolve({
text: text,
id: todoId++,
completed: false
});
}, 2000);
});
setTimeout
을 걸어 Promise
를 반환하는 비동기 작업을 임시로 구현하였다. 위와 같은 비동기 작업이 있을때 Redux
의 Action
은 어떻게 처리해야 할까?
Async / Await
간단하게 Async / Await 를 이용하여 동기적으로 구현할 수 있을것 같아서 테스트해보았다. 코드는 다음과 같다.
import * as Api from "./api";
export const addTodo = async text => await Api.addTodo(text);
돌려보니 Actions must be plain objects. Use custom middleware for async actions.
라는 에러가 뜨면서 되질 않는다. 액션은 순수한 객체여야 하며 비동기 액션을 위해서는 커스텀 미들웨어를 쓰라는 뜻이다.
Redux Thunk
대표적으로 많이들 쓰는 redux-thunk에 대하여 알아보자.
npm install -S redux-thunk
기존 action -> addTodo
는 단순한 객체를 리턴했지만 이제 비동기를 처리하기 위해 dispatch를 인자로 받고 그에 대한 작업을 promise로 반환하는 함수를 리턴해야한다.
redux-thunk
는 단순히 action
에서 반환되어온 함수에 dispatch
를 인자로 넣어 실행할 뿐이다. redux-thunk
를 적용한 코드는 아래와 같다.
import { createStore, applyMiddleware } from "redux";
import { addTodo } from "./actions";
import reducers from "./reducers";
import ReduxThunk from "redux-thunk";
const store = createStore(reducers, applyMiddleware(ReduxThunk));
log("before", store.getState());
store.dispatch(addTodo("Hello world")).then(() => {
log("after", store.getState());
});
function log(label, json) {
const app = document.getElementById("app");
app.innerHTML += `<h1>${label}<h1>`;
app.innerHTML += `<pre>${JSON.stringify(json, undefined, 2)}</pre>`;
}
import * as Api from "./api";
export const addTodo = text => dispatch =>
Api.addTodo(text).then(todo => dispatch({ type: "ADD_TODO", newTodo: todo }));
action
이 Promise
를 반환한다는거 외에는 크게 달라진점이 없다. 나머지 getTodos
, editTodos
, removeTodos
도 액션과 리듀서에 추가해보자.
const todos = [
{
text: "First Todo",
id: 0,
completed: false
}
];
let todoId = 1;
export const getTodos = () =>
new Promise(resolve => {
window.setTimeout(() => {
resolve(todos);
}, 2000);
});
export const addTodo = text =>
new Promise(resolve => {
window.setTimeout(() => {
resolve({
text: text,
id: todoId++,
completed: false
});
}, 2000);
});
export const editTodo = newTodo =>
new Promise(resolve => {
window.setTimeout(() => {
const todoIndex = todos.find(todo => todo.id === newTodo.id);
todos[todoIndex] = { ...todos[todoIndex], ...newTodo };
resolve(todos[todoIndex]);
}, 2000);
});
export const removeTodo = todoId =>
new Promise(resolve => {
window.setTimeout(() => {
todos.splice(todos.findIndex(todo => todo.id === todoId));
resolve(todoId);
}, 2000);
});
import * as Api from "./api";
export const addTodo = text => dispatch =>
Api.addTodo(text).then(res => dispatch({ type: "ADD_TODO", newTodo: res }));
export const editTodo = newTodo => dispatch =>
Api.editTodo(newTodo).then(res =>
dispatch({ type: "EDIT_TODO", newTodo: res })
);
export const removeTodo = todoId => dispatch =>
Api.removeTodo(todoId).then(res =>
dispatch({ type: "REMOVE_TODO", todoId: res })
);
export const getTodos = () => dispatch =>
Api.getTodos().then(res => dispatch({ type: "GET_TODOS", todos: res }));
import { combineReducers } from "redux";
export default combineReducers({
todos: (todos = [], action) => {
switch (action.type) {
case "GET_TODOS":
return action.todos;
case "ADD_TODO":
return [...todos, action.newTodo];
case "REMOVE_TODO":
return todos.filter(todo => todo.id !== action.todoId);
case "EDIT_TODO":
const sliced = todos.slice();
const index = sliced.findIndex(todo => todo.id === action.newTodo.id);
if (index > -1) sliced[index] = action.newTodo;
return sliced;
default:
return todos;
}
}
});
import { createStore, applyMiddleware } from "redux";
import { addTodo, getTodos, editTodo, removeTodo } from "./actions";
import reducers from "./reducers";
import ReduxThunk from "redux-thunk";
const store = createStore(reducers, applyMiddleware(ReduxThunk));
start();
async function start() {
log("initial", store.getState());
await store.dispatch(getTodos());
log("getTodos", store.getState());
await store.dispatch(addTodo("Hello Todo!"));
log("addTodos", store.getState());
await store.dispatch(
editTodo({
id: store.getState().todos[0].id,
completed: true,
text: "Edited!"
})
);
log("editTodo", store.getState());
await store.dispatch(removeTodo(store.getState().todos[0].id));
log("removeTodo", store.getState());
}
function log(label, json) {
const app = document.getElementById("app");
app.innerHTML += `<h1>${label}<h1>`;
app.innerHTML += `<pre>${JSON.stringify(json, undefined, 2)}</pre>`;
}
기본적인 CRUD
작업을 완료하였다. 다음 포스트에서는 react
와 react-redux
를 이용해 뷰를 만드는 작업을 진행한다.
CodeSandBox에 예제를 올려두었으니 참고하면 된다.