관리 메뉴

거니의 velog

(12) 리덕스 미들웨어를 통해 비동기 작업 관리 2 본문

React/React_리액트 심화

(12) 리덕스 미들웨어를 통해 비동기 작업 관리 2

Unlimited00 2023. 12. 14. 09:15

3. 비동기 작업을 처리하는 미들웨어 사용

* 미들웨어가 어떤 방식으로 작동하는지 이해했는가? 이제 오픈 소스 커뮤니티에 공개된 미들웨어를 사용하여 리덕스를 사용하고 있는 프로젝트에서 비동기 작업을 더욱 효율적으로 관리해 보겠다.

* 비동기 작업을 처리할 때 도움을 주는 미들웨어는 정말 다양하다. 이 책에서 다룰 미들웨어는 다음과 같다.

- redux-thunk : 비동기 작업을 처리할 때 가장 많이 사용하는 미들웨어이다. 객체가 아닌 함수 형태의 액션을
  디스패치할 수 있게 해 준다.
  
- redux-saga : redux-thunk 다음으로 많이 사용하는 비동기 작업 관련 미들웨어 라이브러리이다. 특정 액션이
  디스패치되었을 때 정해진 로직에 따라 다른 액션을 디스패치시키는 규칙을 작성하여 비동기 작업을 처리할 수 
  있게 해 준다.

(1) redux-thunk

* redux-thunk는 리덕스를 사용하는 프로젝트에서 비동기 작업을 처리할 때 가장 기본적으로 사용하는 미들웨어이다. 리덕스의 창시자인 댄 아브라모프(Dan Abramov)가 만들었으며, 리덕스 공식 매뉴얼에서도 이 미들웨어를 사용하여 비동기 작업을 다루는 예시를 보여 준다.

[1] Thunk란?

* Thunk는 특정 작업을 나중에 할 수 있도록 미루기 위해 함수 형태로 감싼 것을 의미한다. 예를 들어 주어진 파라미터에 1을 더하는 함수를 만들고 싶다면 다음과 같이 작성할 것이다.

const addOne = x => x + 1;
addOne(1); // 2

* 이 코드를 실행하면 addOne을 호출했을 때 바로 1 + 1이 연산된다. 그런데 이 연산 작업을 나중에 하도록 미루고 싶다면 어떻게 해야 할까?

const addOne = x => x + 1;
function addOneThunk(x) {
    const thunk = () => addOne(x);
    return thunk;
}

const fn = addOneThunk(1);
setTimeout(() => {
    const value = fn(); // fn이 실행되는 시점에서 연산
    console.log(value);
}, 1000);

* 이렇게 하면 특정 작업을 나중에 하도록 미룰 수 있다.

* 만약 addOneThunk를 화살표 함수로만 사용한다면 다음과 같이 구현할 수 있다.

const addOne = x => x + 1;
const addOneThunk = x => () => addOne(x);

const fn = addOneThunk(1);
setTimeout(() => {
    const value = fn(); // fn이 실행되는 시점에서 연산
    console.log(value);
}, 1000);

* redux-thunk 라이브러리를 사용하면 thunk 함수를 만들어서 디스패치할 수 있다. 그러면 리덕스 미들웨어가 그 함수를 전달받아 store의 dispatch와 getState를 파라미터로 넣어서 호출해 준다.

* 다음은 redux-thunk에서 사용할 수 있는 예시 thunk 함수이다.

const sampleThunk = () => (dispatch, getState) => {
    // 현재 상태를 참조할 수 있고,
    // 새 액션을 디스패치할 수도 있다.
}

[2] 미들웨어 적용하기

* redux-thunk 미들웨어를 설치하고 프로젝트에 적용해 보자. 

* 다음 명령어로 라이브러리를 설치하자.

$ yarn add redux-thunk

* 스토어를 만들 때 redux-thunk를 적용하자.

[index.js]

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { applyMiddleware, createStore } from "redux";
import rootReducer from "./modules";
import { Provider } from "react-redux";
// import loggerMiddleware from "./lib/loggerMiddleware";
import { createLogger } from "redux-logger";
import { thunk as ReduxThunk } from "redux-thunk"; // Import the 'thunk' function

const logger = createLogger();
const store = createStore(rootReducer, applyMiddleware(logger, ReduxThunk));

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

reportWebVitals();

[3] thunk 생성 함수 만들기

* redux-thunk는 액션 생성 함수에서 일반 액션 객체를 반환하는 대신에 함수를 반환한다. increaseAsync와 decreaseAsync 함수를 만들어 카운터 값을 비동기적으로 한번 변경시켜 보자.

[modules/counter.js]

import React from "react";
import { createAction, handleActions } from "redux-actions";

const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";

export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);

// 1초 뒤에 increase 혹은 decrease 함수를 디스패치함
export const increaseAsync = () => (dispatch) => {
  setTimeout(() => {
    dispatch(increase());
  }, 1000);
};
export const decreaseAsync = () => (dispatch) => {
  setTimeout(() => {
    dispatch(decrease());
  }, 1000);
};

const initialState = 0; // 상태는 꼭 객체일 필요는 없다. 숫자도 잘 작동한다.

const counter = handleActions(
  {
    [INCREASE]: (state) => state + 1,
    [DECREASE]: (state) => state - 1,
  },
  initialState
);

export default counter;

* 리덕스 모듈을 수정했으면 CounterContainer에서 호출하던 액션 생성 함수도 변경해 주자.

[containers/CounterContainer.js]

import React from "react";
import { connect } from "react-redux";
import { increaseAsync, decreaseAsync } from "../modules/counter";
import Counter from "../components/Counter";

const CounterContainer = ({ number, increaseAsync, decreaseAsync }) => {
  return (
    <Counter
      number={number}
      onIncrease={increaseAsync}
      onDecrease={decreaseAsync}
    />
  );
};

export default connect(
  (state) => ({
    number: state.counter,
  }),
  {
    increaseAsync,
    decreaseAsync,
  }
)(CounterContainer);

* 코드를 저장하고 브라우저에서 버튼을 눌러 보자. 숫자가 1초 뒤에 변경되는가? 개발자 도구를 열어서 발생한 액션 기록을 확인해 보자.

redux-thunk 적용

* 처음 디스패치되는 액션은 함수 형태이고, 두 번째 액션은 객체 형태이다.

[4] 웹 요청 비동기 작업 처리하기

* 이번에는 thunk의 속성을 활용하여 웹 요청 비동기 작업을 처리하는 방법에 대해 알아보자. 웹 요청을 연습하기 위해 JSONPlaceholder에서 제공되는 가짜 API를 사용하겠다.

https://jsonplaceholder.typicode.com/

 

JSONPlaceholder - Free Fake REST API

{JSON} Placeholder Free fake API for testing and prototyping. Powered by JSON Server + LowDB. Tested with XV. Serving ~2 billion requests each month.

jsonplaceholder.typicode.com

* 사용할 API는 다음과 같다.

# 포스트 읽기(:id는 1~100 사이의 숫자)
GET https://jsonplaceholder.typicode.com/posts/:id

# 모든 사용자 정보 불러오기
GET https://jsonplaceholder.typicode.com/users

* API를 호출할 때는 주로 Promise 기반 웹 클라이언트인 axios를 사용한다. 해당 라이브러리를 설치해 주자.

$ yarn add axios

* API를 모두 함수화해 주자. 각 API를 호출하는 함수를 따로 작성하면, 나중에 사용할 때 가독성도 좋고 유지 보수도 쉬워진다. 다른 파일에서 불러와 사용할 수 있도록 export를 사용하여 내보내 주자.

[lib/api.js]

import axios from "axios";

export const getPost = (id) =>
  axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`);

export const getUsers = (id) =>
  axios.get(`https://jsonplaceholder.typicode.com/users`);

* 이제 새로운 리듀서를 만들어 줄 차례이다. 위 API를 사용하여 데이터를 받아와 상태를 관리할 sample이라는 리듀서를 생성해 보겠다. 주석을 읽으면서 다음 코드를 작성해 보자.

import { handleActions } from "redux-actions";
import * as api from "../lib/api";

// 액션 타입을 선언한다.
// 한 요청당 세 개를 만들어야 한다.

const GET_POST = "sample/GET_POST";
const GET_POST_SUCCESS = "sample/GET_POST_SUCCESS";
const GET_POST_FAILURE = "sample/GET_POST_FAILURE";

const GET_USERS = "sample/GET_USERS";
const GET_USERS_SUCCESS = "sample/GET_USERS_SUCCESS";
const GET_USERS_FAILURE = "sample/GET_USERS_FAILURE";

// thunk 함수를 생성한다.
// thunk 함수 내부에서는 시작할 때, 성공했을 때, 실패했을 때 다른 액션을 디스패치한다.

export const getPost = (id) => async (dispatch) => {
  dispatch({ type: GET_POST }); // 요청을 시작한 것을 알림
  try {
    const response = await api.getPost(id);
    dispatch({
      type: GET_POST_SUCCESS,
      payload: response.data,
    }); // 요청 성공
  } catch (e) {
    dispatch({
      type: GET_POST_FAILURE,
      payload: e,
      error: true,
    }); // 에러 발생
    throw e; // 나중에 컴포넌트단에서 에러를 조회할 수 있게 해 줌
  }
};

export const getUsers = () => async (dispatch) => {
  dispatch({ type: GET_USERS }); // 요청을 시작한 것을 알림
  try {
    const response = await api.getUsers();
    dispatch({
      type: GET_USERS_SUCCESS,
      payload: response.data,
    }); // 요청 성공
  } catch (e) {
    dispatch({
      type: GET_USERS_FAILURE,
      payload: e,
      error: true,
    }); // 에러 발생
    throw e; // 나중에 컴포넌트단에서 에러를 조회할 수 있게 해 줌
  }
};

// 초기 상태를 선언한다.
// 요청의 로딩 중 상태는 loading이라는 객체에서 관리한다.

const initialState = {
  loading: {
    GET_POST: false,
    GET_USERS: false,
  },
  post: null,
  users: null,
};

const sample = handleActions(
  {
    [GET_POST]: (state) => ({
      ...state,
      loading: {
        ...state.loading,
        GET_POST: true, // 요청 시작
      },
    }),
    [GET_POST_SUCCESS]: (state, action) => ({
      ...state,
      loading: {
        ...state.loading,
        GET_POST: false, // 요청 완료
      },
      post: action.payload,
    }),
    [GET_POST_FAILURE]: (state, action) => ({
      ...state,
      loading: {
        ...state.loading,
        GET_POST: false, // 요청 완료
      },
    }),
    [GET_USERS]: (state) => ({
      ...state,
      loading: {
        ...state.loading,
        GET_USERS: true, // 요청 시작
      },
    }),
    [GET_USERS_SUCCESS]: (state, action) => ({
      ...state,
      loading: {
        ...state.loading,
        GET_USERS: false, // 요청 완료
      },
      users: action.payload,
    }),
    [GET_USERS_FAILURE]: (state, action) => ({
      ...state,
      loading: {
        ...state.loading,
        GET_USERS: false, // 요청 완료
      },
    }),
  },
  initialState
);

export default sample;

* 코드에서 반복되는 로직이 꽤 있다. 우선 컨테이너 컴포넌트를 사용하여 데이터 요청을 성공적으로 처리하고, 나중에 반복되는 로직을 따로 분리하여 재사용하는 형태로 코드를 리팩토링하겠다.

* 리듀서를 다 작성했다면 해당 리듀서를 루트 리듀서에 포함시키자.

[modules/index.js]

import { combineReducers } from "redux";
import counter from "./counter";
import sample from "./sample";

const rootReducer = combineReducers({ counter, sample });

export default rootReducer;

* 우선 데이터를 렌더링할 프레젠테이셔널 컴포넌트부터 작성한다. 이 컴포넌트를 작성하려면 먼저 API를 통해 전달받은 데이터의 형식이 어떤 구조인지 확인해야 한다.

// post
{
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
// users
[
    {
        "id": 1,
        "name": "Leanne Graham",
        "username": "Bret",
        "email": "Sincere@april.biz",
        "address": {
            "street": "Kulas Light",
            "suite": "Apt. 556",
            "city": "Gwenborough",
            "zipcode": "92998-3874",
            "geo": {
                "lat": "-37.3159",
                "lng": "81.1496"
            }
        },
        "phone": "1-770-736-8031 x56442",
        "website": "hildegard.org",
        "company": {
            "name": "Romaguera-Crona",
            "catchPhrase": "Multi-layered client-server neural-net",
            "bs": "harness real-time e-markets"
        }
    },
    (......)
]

* 이번에 만들 컴포넌트는 post의 경우 title과 body만 보여주고, user의 경우 username과 email만 보여 줄 것이다.

* Sample 컴포넌트를 다음과 같이 작성해 보자.

[components/Sample.js]

import React from "react";

const Sample = ({ loadingPost, loadingUsers, post, users }) => {
  return (
    <div>
      <section>
        <h1>포스트</h1>
        {loadingPost && "로딩 중..."}
        {!loadingPost && post && (
          <div>
            <h3>{post.title}</h3>
            <h3>{post.body}</h3>
          </div>
        )}
      </section>
      <hr />
      <section>
        <h1>사용자 목록</h1>
        {loadingUsers && "로딩 중..."}
        {!loadingUsers && users && (
          <ul>
            {users.map((user) => (
              <li key={user.id}>
                {user.username} ({user.email})
              </li>
            ))}
          </ul>
        )}
      </section>
    </div>
  );
};

export default Sample;

* 데이터를 불러와서 렌더링해 줄 때는 유효성 검사를 해 주는 것이 중요하다. 예를 들어 post && 를 사용하면 post 객체가 유효할 때만 그 내부의 post.title 혹은 post.body 값을 보여 준다. 만약 데이터가 없는 상태라면 post.title을 조회하려고 할 때 자바스크립트 오류가 발생하니 반드시 유효성 검사를 해주어야 한다.

* users 도 마찬가지로 데이터가 배열 형태로 들어올 것을 기대하고 map 함수를 사용하고 있다. 하지만 유효성 검사를 하지 않으면 null 값에 대해 map 함수를 호출하고, 결국 map 함수가 존재하지 않아 오류가 발생한다.

* 이제 컨테이너 컴포넌트를 만들어 보자.

[containers/SampleContainer.js]

import React from "react";
import { connect } from "react-redux";
import Sample from "../components/Sample";
import { getPost, getUsers } from "../modules/sample"; // 액션 생성 함수를 가져와야 함

const { useEffect } = React;
const SampleContainer = ({
  getPost,
  getUsers,
  post,
  users,
  loadingPost,
  loadingUsers,
}) => {
  useEffect(() => {
    getPost(1); // 액션 생성 함수를 호출해야 함
    getUsers(1); // 액션 생성 함수를 호출해야 함
  }, [getPost, getUsers]);

  return (
    <Sample
      post={post}
      users={users}
      loadingPost={loadingPost}
      loadingUsers={loadingUsers}
    />
  );
};

export default connect(
  ({ sample }) => ({
    post: sample.post,
    users: sample.users,
    loadingPost: sample.loading.GET_POST,
    loadingUsers: sample.loading.GET_USERS,
  }),
  { getPost, getUsers }
)(SampleContainer);

* 그 다음 App 컴포넌트에서 CouinterContainer 대신 SampleContainer를 렌더링해 보자.

[App.js]

import React from "react";
import SampleContainer from "./containers/SampleContainer";

const App = () => {
  return (
    <div>
      <SampleContainer />
    </div>
  );
};

export default App;

* 브라우저를 열어 데이터가 잘 로딩되었는지, 개발자 도구 콘솔에서 액션이 어떤 순서로 발생하는지 확인해 보자.

데이터 요청 성공

[5] 리팩토링

* API를 요청해야 할 때마다 17줄 정도 되는 thunk 함수를 작성하는 것이 아니라 로딩 상태를 리듀서에서 관리하는 작업은 귀찮을 뿐 아니라 코드도 길어지게 만든다. 그러므로 반복되는 로직을 따로 분리하여 코드의 양을 줄여 보자.

export default function createRequestThunk(type, request) {
  // 성공 및 실패 액션 타입을 정의한다.
  const SUCCESS = `${type}_SUCCESS`;
  const FAILURE = `${type}_FAILURE`;
  return (params) => async (dispatch) => {
    dispatch({ type }); // 시작됨
    try {
      const response = await request(params);
      dispatch({
        type: SUCCESS,
        payload: response.data,
      }); // 성공
    } catch (e) {
      dispatch({
        type: FAILURE,
        payload: e,
        error: true,
      }); // 에러 발생
      throw e;
    }
  };
}

// 사용법 : createRequestThunk('GET_USERS', api.getUsers)

* 이반에 만든 유틸 함수는 API 요청을 해 주는 thunk 함수는 한 줄로 생성할 수 있게 해 준다. 액션 타입과 API를 요청하는 함수를 파라미터로 넣어 주면 나머지 작업을 대신 처리해 준다. 이 함수를 사용하여 기존 thunk 함수의 코드를 대체시켜 보자.

[modules/sample.js]

import { handleActions } from "redux-actions";
import * as api from "../lib/api";
import createRequestThunk from "../lib/createRequestThunk";

// 액션 타입을 선언한다.
// 한 요청당 세 개를 만들어야 한다.

const GET_POST = "sample/GET_POST";
const GET_POST_SUCCESS = "sample/GET_POST_SUCCESS";
const GET_POST_FAILURE = "sample/GET_POST_FAILURE";

const GET_USERS = "sample/GET_USERS";
const GET_USERS_SUCCESS = "sample/GET_USERS_SUCCESS";
const GET_USERS_FAILURE = "sample/GET_USERS_FAILURE";

// thunk 함수를 생성한다.
// thunk 함수 내부에서는 시작할 때, 성공했을 때, 실패했을 때 다른 액션을 디스패치한다.

export const getPost = createRequestThunk(GET_POST, api.getPost);
export const getUsers = createRequestThunk(GET_USERS, api.getUsers);

// 초기 상태를 선언한다.
// 요청의 로딩 중 상태는 loading이라는 객체에서 관리한다.

const initialState = {
  loading: {
    GET_POST: false,
    GET_USERS: false,
  },
  post: null,
  users: null,
};

const sample = handleActions(
  {
    [GET_POST]: (state) => ({
      ...state,
      loading: {
        ...state.loading,
        GET_POST: true, // 요청 시작
      },
    }),
    [GET_POST_SUCCESS]: (state, action) => ({
      ...state,
      loading: {
        ...state.loading,
        GET_POST: false, // 요청 완료
      },
      post: action.payload,
    }),
    [GET_POST_FAILURE]: (state, action) => ({
      ...state,
      loading: {
        ...state.loading,
        GET_POST: false, // 요청 완료
      },
    }),
    [GET_USERS]: (state) => ({
      ...state,
      loading: {
        ...state.loading,
        GET_USERS: true, // 요청 시작
      },
    }),
    [GET_USERS_SUCCESS]: (state, action) => ({
      ...state,
      loading: {
        ...state.loading,
        GET_USERS: false, // 요청 완료
      },
      users: action.payload,
    }),
    [GET_USERS_FAILURE]: (state, action) => ({
      ...state,
      loading: {
        ...state.loading,
        GET_USERS: false, // 요청 완료
      },
    }),
  },
  initialState
);

export default sample;

* 어떤가? 코드가 많이 줄었다. 브라우저를 열어서 이전과 똑같이 작동하는지도 확인해 보자. 똑같은 기능을 훨씬 짧은 코드로 구현했다!

* 이번에는 요청의 로딩 상태를 관리하는 작업을 개선하겠다. 기존에는 리듀서 내부에서 각 요청에 관련된 액션이 디스패치될 때마다 로딩 상태를 변경해 주었는데, 이 작업을 로딩 상태만 관리하는 리덕스 모듈을 따로 생성하여 처리하겠다.

[modules/loading.js]

import { createAction, handleActions } from "redux-actions";

const START_LOADING = "loading/START_LOADING";
const FINISH_LOADING = "loading/FINISH_LOADING";

/* 
    요청을 위한 액션 타입을 payload로 설정한다(예: "sample/GET_POST").
*/

export const startLoading = createAction(
  START_LOADING,
  (requestType) => requestType
);

export const finishLoading = createAction(
  FINISH_LOADING,
  (requestType) => requestType
);

const initialState = {};

const loading = handleActions(
  {
    [START_LOADING]: (state, action) => ({
      ...state,
      [action.payload]: true,
    }),
    [FINISH_LOADING]: (state, action) => ({
      ...state,
      [action.payload]: false,
    }),
  },
  initialState
);

export default loading;

* 다음은 요청이 시작될 때 디스패치할 액션이다.

{
    type: 'loading/START_LOADING',
    payload: 'sample/GET_POST'
}

* 위 액션이 디스패치되면 loading 리듀서가 관리하고 있는 상태에서 sample/GET_POST 값을 true로 설정해 준다. 만약 기존 상태에 sample/GET_POST 필드가 존재하지 않으면 새로 값을 설정해 준다.

* 그리고 요청이 끝나면 다음 액션을 디스패치해야 한다.

{
    type: 'loading/FINISH_LOADING',
    payload: 'sample/GET_POST'
}

* 그러면 기존에 true로 설정했던 값을 다시 false로 전환해 준다.

* 리듀서를 다 작성했으면 루트 리듀서에 포함시키자.

[modules/index.js]

import { combineReducers } from "redux";
import counter from "./counter";
import sample from "./sample";
import loading from "./loading";

const rootReducer = combineReducers({ counter, sample, loading });

export default rootReducer;

* loading 리덕스 모듈에서 만든 액션 생성 함수는 앞에서 만든 createRequestThunk에서 사용해 준다.

[lib/createRequestThunk.js]

import { finishLoading, startLoading } from "../modules/loading";

export default function createRequestThunk(type, request) {
  // 성공 및 실패 액션 타입을 정의한다.
  const SUCCESS = `${type}_SUCCESS`;
  const FAILURE = `${type}_FAILURE`;
  return (params) => async (dispatch) => {
    dispatch({ type }); // 시작됨
    dispatch(startLoading(type));
    try {
      const response = await request(params);
      dispatch({
        type: SUCCESS,
        payload: response.data,
      }); // 성공
      dispatch(finishLoading(type));
    } catch (e) {
      dispatch({
        type: FAILURE,
        payload: e,
        error: true,
      }); // 에러 발생
      dispatch(startLoading(type));
      throw e;
    }
  };
}

// 사용법 : createRequestThunk('GET_USERS', api.getUsers)

* 그러면 SampleContainer에서 로딩 상태를 다음과 같이 조회할 수 있다.

[containers/SampleContainer.js]

import React from "react";
import { connect } from "react-redux";
import Sample from "../components/Sample";
import { getPost, getUsers } from "../modules/sample"; // 액션 생성 함수를 가져와야 함
import loading from "../modules/loading";

const { useEffect } = React;
const SampleContainer = ({
  getPost,
  getUsers,
  post,
  users,
  loadingPost,
  loadingUsers,
}) => {
  useEffect(() => {
    getPost(1); // 액션 생성 함수를 호출해야 함
    getUsers(1); // 액션 생성 함수를 호출해야 함
  }, [getPost, getUsers]);

  return (
    <Sample
      post={post}
      users={users}
      loadingPost={loadingPost}
      loadingUsers={loadingUsers}
    />
  );
};

export default connect(
  ({ sample }) => ({
    post: sample.post,
    users: sample.users,
    loadingPost: loading["sample/GET_POST"],
    loadingUsers: loading["sample/GET_USERS"],
  }),
  { getPost, getUsers }
)(SampleContainer);

* 코드를 저장하고 브라우저를 열어서 기능이 제대로 작동하는지 확인해 보자.

잘 작동한다.

* 이제 sample 리듀서에서 불필요한 코드를 지워 보자.

[modules/sample.js]

import { handleActions } from "redux-actions";
import * as api from "../lib/api";
import createRequestThunk from "../lib/createRequestThunk";

// 액션 타입을 선언한다.
// 한 요청당 세 개를 만들어야 한다.

const GET_POST = "sample/GET_POST";
const GET_POST_SUCCESS = "sample/GET_POST_SUCCESS";

const GET_USERS = "sample/GET_USERS";
const GET_USERS_SUCCESS = "sample/GET_USERS_SUCCESS";

// thunk 함수를 생성한다.
// thunk 함수 내부에서는 시작할 때, 성공했을 때, 실패했을 때 다른 액션을 디스패치한다.

export const getPost = createRequestThunk(GET_POST, api.getPost);
export const getUsers = createRequestThunk(GET_USERS, api.getUsers);

// 초기 상태를 선언한다.
// 요청의 로딩 중 상태는 loading이라는 객체에서 관리한다.

const initialState = {
  post: null,
  users: null,
};

const sample = handleActions(
  {
    [GET_POST_SUCCESS]: (state, action) => ({
      ...state,
      loading: {
        ...state.loading,
        GET_POST: false, // 요청 완료
      },
      post: action.payload,
    }),
    [GET_USERS_SUCCESS]: (state, action) => ({
      ...state,
      loading: {
        ...state.loading,
        GET_USERS: false, // 요청 완료
      },
      users: action.payload,
    }),
  },
  initialState
);

export default sample;

* 코드가 훨씬 깔끔해졌다! 이제 sample 리듀서에서는 로딩 중에 대한 상태를 관리할 필요가 없다. 성공했을 때의 케이스만 잘 관리해 주면 된다. 추가로 실패했을 때의 케이스를 관리하고 싶다면 _FAILURE가 붙은 액션을 리듀서에서 처리해 주면 된다. 혹은 컨테이너 컴포넌트에서 try/catch 구문을 사용하여 에러 값을 조회할 수도 있다.

[SampleContainer.js - useEffect]

  useEffect(() => {
    // useEffect에 파라미터로 넣는 함수는 async로 할 수 없기 때문에
    // 그 내부에서 async 함수를 선언하고 호출해 준다.
    const fn = async () => {
      try {
        await getPost(1);
        await getUsers(1);
      } catch (e) {
        console.log(e); // 에러 조회
      }
    };
    fn();
  }, [getPost, getUsers]);

* redux-thunk 를 처음 쓸 때는 비록 작성해야 할 코드가 많아서 불편할 수 있지만, 유용한 함수와 리듀서를 만들어서 상태를 관리한다면 매우 깔끔한 코드로 기능을 구현할 수 있다.