본문 바로가기

Computer Programming/[리액트를 다루는 기술]

[리액트를 다루는 기술]18. 미들웨어 비동기 작업 2 (thunk with API | connect() | createRequestThunk | loading module)

1. 웹 요청 비동기 작업 처리하기

 

우선 api.js에 각 api를 모두 함수화해서 모아둔다.

 

import axios from "axios";

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

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

 

각 api를 호출하는 함수를 따로 작성하면, 나중에 가독성과 유지보수에 좋다. 이번 스터디에서 어떤 분이 이렇게 하나하나 작성하셨는데 이유가 있었다... 역시 민버지

 

 

 

다음은 thunk 함수를 생성해보자 

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

초기상태도 설정해준다.

 

 

//thunk 함수 생성
export const getPost = (id) => async (dispatch) => {
  dispatch({ type: GET_POST });
  try {
    const response = await api.getPostById(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;
  }
};

// 초기 상태
const initialState = {
  loading: {
    GET_POST: false,
    GET_USERS: false,
  },
  post: null,
  users: null,
};

 

 

그리도 리듀서도 만든 다음 export 한다. state의 loading 상태를 바꿀 때의 얕은 복사도 주의해야한다.

 

const sample = handleAction(
  {
    [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) => ({
      ...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) => ({
      ...state,
      loading: {
        ...state.loading,
        GET_USERS: false,
      },
    }),
  },
  initialState,
);

export default sample;

 

 

리듀서를 적용하기 위해 루트 리듀서도 업데이트해준다.

//configStore.js

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

export default rootReducer;

 

 

 

store와 리듀서함수를 사용할 컨테이너에서 connect 함수를 통해 props로 받아오고 싶은 값들을 받아온다.

 

* connect 함수?

react-redux 의 connect 함수란 특정 함수 또는 값을 props로 받아와서 사용하고 싶은 경우에 사용한다. redux stroe 안에 있는 상태를 props로 넣어줄 수 있고, 액션을 디스패치하는 (thunk 함수도 ㅇㅇ) prop로 넣어줄 수 있다.

 

connect(mapStateToProps, mapDispatchToProps) 이렇게 사용하며 첫번째 파라미터는 컴포넌트에 props로 넣어줄 리덕스 스토어의 상태에 관련된 함수이고, 두번째 파라미터는 컴포넌트에 prop로 넣어줄 액션을 디스패치하는 함수이다. 

 

//SampleContainer.js

import { connect } from "react-redux";
import { getPost, getUsers } from "../redux/modules/sample";
import Sample from "../components/Sample";
import { useEffect } from "react";

const SampleContainer = ({
  post,
  users,
  loadingPost,
  loadingUsers,
  getPost,
  getUsers,
}) => {
  useEffect(() => {
    getPost(1);
    getUsers();
  }, [getPost, getUsers]);

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

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

 

 

다음은 데이터를 props로 받아와서 사용하는 Sample 컴포넌트이다.

 

데이터가 없는 상태라면 조회할 때 자바스크립트 오류가 발생하기 때문에 반드시 유효성검사를 해야한다.

 

 

//Sample.js

import React from "react";

const Sample = ({ post, users, loadingPost, loadingUsers }) => {
  return (
    <div>
      <section>
        {loadingPost && "로딩중 ..."}
        {/* 유효성검사 */}
        {!loadingPost && post && (
          <div>
            <h3>{post.title}</h3>
            <p>{post.body}</p>
          </div>
        )}
      </section>
      <hr />
      <section>
        {loadingUsers && "로딩중 ..."}
        {/* 유효성검사 */}
        {!loadingUsers && users && (
          <div>
            <ul>
              {users.map((user) => (
                <li key={user.id}>
                  {user.username}({user.email})
                </li>
              ))}
            </ul>
          </div>
        )}
      </section>
    </div>
  );
};

export default Sample;

 

 

지금은 sample.js 의 코드가 아래처럼 반복되고 있기 때문에 나중에 함수들이 늘어날 수록 코드가 길고 복잡해진다. 따라서 createRequestThunk  라는 유틸 함수를 만들어 반복되는 로직을 따로 분리해서 코드의 양을 줄이는것이 좋다.

 

//thunk 함수 생성
export const getPost = (id) => async (dispatch) => {
  dispatch({ type: GET_POST });
  try {
    const response = await api.getPostById(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;
  }
};

 

 여기서는 액션 타입의 이름과 api. ~ 비동기 처리 코드(=request) 이 두가지를 제외하면 모두 같다. 그래서 이 두 가지를 파라미터로 받고 처리하는 함수를 내보내면 된다.

 

 

 

* createRequestThunk 함수로 재사용하기

 

utils/createRequestThunk.js

//utils/createRequestThunk.js
const createRequestThunk = (type, request) => {
  const SUCCESS = `${type}_SUCCESS`;
  const FAILURE = `${type}FAILURE`;

  return (params) => async (dispatch) => {
    dispatch({ type: type }); //시작됨
    try {
      const response = await request(params); //성공
      dispatch({ type: SUCCESS, payload: response.data });
    } catch (e) {
      //에러 발생
      dispatch({ type: FAILURE, payload: e, error: true });
      throw e;
    }
  };
};

export default createRequestThunk;

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

 

다른 파일에서 이 함수를 사용하려면 createRequestThunk(GET_POST, api.getUsers)와 같은 식으로 작성하면 된다.  <-GET_POST는 변수명! (텍스트 그대로 아님)

//modules/sample.js

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 함수 생성
export const getPost = createRequestThunk(GET_POST, api.getPost);
export const getUsers = createRequestThunk(GET_USERS, api.getUsers);

// initialState, handleActions 동일하게 작성 ...

 

 

 

 

* 로딩 상태 관리 모듈 따로 만들어서 사용하기

 

기존에는 리듀서 내부에서 액션이 디스패치 될 때 각각 로딩상태를 변경해주었다. 

이번에는 로딩 상태만 관리하는 모듈을 따로 만들어보자

 

//modules/loading.js

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

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

// 요청을 위한 액션 타입 (requestType) 은 payload로 설정함!
// 객체를 편하게 전달하기 위해 createAction 사용함
export const startLoading = createAction(
  START_LOADING,
  (requsetType) => requsetType,
);

//requestType 예시 : "sample/GET_POST"

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

const initialState = {};

const loading = handleActions(
  {
    [START_LOADING]: (state, action) => ({
      ...state,
      [action.payload]: true,
      //payload로 type 이름 전달했음
      //initialState에 이미 있다면 속성 변경, 없다면 속성 추가
    }),
    [FINISH_LOADING]: (state, action) => ({
      ...state,
      [action.payload]: false,
    }),
  },
  initialState,
);

export default loading;

 

 

이 loading module 은 전에 만들었던 createRequestThunk.js에서 사용해주면 된다 (loading dispatch 시)

//lib/createRequestThunk.js

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

const createRequestThunk = (type, request) => {
  const SUCCESS = `${type}_SUCCESS`;
  const FAILURE = `${type}_FAILURE`;

  return (params) => async (dispatch) => {
    dispatch(startLoading(type)); //loading 시작 dispatch
    try {
      const response = await request(params); //성공
      dispatch({ type: SUCCESS, payload: response.data });
      dispatch(finishLoading(type)); //loading 종료 dispatch
    } catch (e) {
      //에러 발생
      dispatch({ type: FAILURE, payload: e, error: true });
      dispatch(finishLoading(type)); //loading 종료 dispatch
      throw e;
    }
  };
};

export default createRequestThunk;

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

 

root reducer 에 loading module을 추가해주고

 

//configStore.js

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

 

Container에서 이 loading state을 사용할 경우

 

//SampleContainer.js

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

이렇게 connect 함수 안에서 sample 뿐만 아니라 loading 도 불러온 뒤, loadingPost : loading["sample/GET_POST"]로 접근해 boolean 값을 가져온다.

 

 

마지막으로 sample.js 모듈의 코드에서 불필요한 부분을 없애주면 된다. 이제 loading과 failure 시에는 createRequestThunk에서 관리한다.

 

따라서 sample.js 모듈에서는 thunk 함수를 생성하고 createRequestThunk로 액션 타입을 전달한 다음, handleActions 를 통해 payload의 데이터 전달만 작성하면 된다.

 

 

//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 함수 생성
export const getPost = createRequestThunk(GET_POST, api.getPost);
export const getUsers = createRequestThunk(GET_USERS, api.getUsers);

// 초기 상태
const initialState = {
  //loading 삭제
  post: null,
  users: null,
};

const sample = handleActions(
  {
    // Loading, failure 삭제 -> createRequestThunk에서 처리해줌
    [GET_POST_SUCCESS]: (state, action) => ({
      ...state,
      post: action.payload, //데이터 전달
    }),
    [GET_USERS_SUCCESS]: (state, action) => ({
      ...state,
      users: action.payload,
    }),
  },
  initialState,
);

export default sample;

 

 

 

 

 

 

 

어려워............ㅠㅎ흐귷ㄱ

 

 

 

 

 

 

 

 

 

 

 


Reference

https://velog.io/@tkaqhcjstk/redux-connect-%ED%95%A8%EC%88%98-dispatch-%ED%95%A8%EC%88%98-%EA%B0%9C%EB%85%90#:~:text=connect%ED%95%A8%EC%88%98%EB%9E%80,-%ED%8A%B9%EC%A0%95%20%ED%95%A8%EC%88%98%20%EB%98%90%EB%8A%94&text=redux%20store%EC%95%88%EC%97%90%20%EC%9E%88%EB%8A%94%20%EC%83%81%ED%83%9C,%ED%95%98%EA%B2%8C%20%ED%95%B4%EC%A3%BC%EB%8A%94%20%EC%97%AD%ED%95%A0%EC%9D%84%20%ED%95%9C%EB%8B%A4.