4년 이상 전

Redux 마스터하기 - 리덕스의 미들웨어에 대하여 알아보자.

ReduxActionReducer에 전해지기 전 해야할 작업을 정의할 수 있는 미들웨어를 지원한다. Redux의 비동기 작업들을 도와주는 미들웨어에 대하여 알아보자.

let todoId = 0;
export const addTodo = text =>
  new Promise(resolve => {
    window.setTimeout(() => {
      resolve({
        text: text,
        id: todoId++,
        completed: false
      });
    }, 2000);
  });

setTimeout을 걸어 Promise를 반환하는 비동기 작업을 임시로 구현하였다. 위와 같은 비동기 작업이 있을때 ReduxAction은 어떻게 처리해야 할까?

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 }));

actionPromise를 반환한다는거 외에는 크게 달라진점이 없다. 나머지 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 작업을 완료하였다. 다음 포스트에서는 reactreact-redux를 이용해 뷰를 만드는 작업을 진행한다. CodeSandBox에 예제를 올려두었으니 참고하면 된다.