[Redux] Redux-Thunk로 프로미스 다루기

mhlog·2023년 7월 11일
0

React

목록 보기
10/10
post-thumbnail

Velopert님의 블로그를 보면서 공부한 내용을 정리한 글입니다. Redux-Thunk

지난 포스팅에서는 setTimeout을 이용한 간단한 비동기 처리를 구현하였지만, 이번 포스팅에서는 실제 API의 요청으로 비동기 처리를 하는 작업에 대해 공부한 내용을 포스팅 하려고 한다.

1. json-server로 API 세팅

보통은 express나 spring을 통해 backend 서버를 구현하지만, 연습의 용도이기 때문에 json-server로 간단한 웹 서버를 구현해보려고 한다.

root 디렉토리에 data.json파일을 만들고 다음과 같이 저장한다.

// src/data.json
{
  "posts": [
    {
      "id": 1,
      "title": "리덕스 미들웨어를 배워봅시다",
      "body": "리덕스 미들웨어를 직접 만들어보면 이해하기 쉽죠."
    },
    {
      "id": 2,
      "title": "redux-thunk를 사용해봅시다",
      "body": "redux-thunk를 사용해서 비동기 작업을 처리해봅시다!"
    },
    {
      "id": 3,
      "title": "redux-saga도 사용해봅시다",
      "body": "나중엔 redux-saga를 사용해서 비동기 작업을 처리하는 방법도 배워볼 거예요."
    }
  ]
}
npx json-server ./data.json --port 4000

이제 data.json을 기반으로 4000번 포트에 가상 웹서버가 열릴 것이다.

2. thunk로 promise 처리 (Reducer 생성)

우선 클라이언트에서 서버로부터 API 요청을 보내야하니 axios를 설치한다

npm i axios
// api/posts.js
import axios from 'axios';

export const getPosts = async () => {
  const response = await axios.get('http://localhost:4000/posts');
  return response.data;
};

export const getPostById = async id => {
  const response = await axios.get(`http://localhost:4000/posts/${id}`);
  return response.data;
};

API 요청을 하는 함수를 따로 작성하였으니 액션 타입을 선언하고, 액션 생성함수를 만든 뒤, posts라는 리듀서를 생생해보겠다.

프로미스를 다루는 리덕스 모듈을 다룰 땐 다음과 같은 사항을 고려해야 한다.

  1. 프로미스가 시작, 성공, 실패했을때 다른 액션을 디스패치해야한다.
  2. 각 프로미스마다 thunk 함수를 만들어주어야 한다.
  3. 리듀서에서 액션에 따라 로딩중, 결과, 에러 상태를 변경해주어야 한다.
// modules/posts.js
import * as postsAPI from '../api/posts'; // api/posts 안의 함수 모두 불러오기

/* 액션 타입 */

// 포스트 여러개 조회하기
const GET_POSTS = 'GET_POSTS'; // 요청 시작
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS'; // 요청 성공
const GET_POSTS_ERROR = 'GET_POSTS_ERROR'; // 요청 실패

// 포스트 하나 조회하기
const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_ERROR = 'GET_POST_ERROR';

// thunk 를 사용 할 때, 꼭 모든 액션들에 대하여 액션 생성함수를 만들 필요는 없습니다.
// 그냥 thunk 함수에서 바로 액션 객체를 만들어주어도 괜찮습니다.

export const getPosts = () => async dispatch => {
  dispatch({ type: GET_POSTS }); // 요청이 시작됨
  try {
    const posts = await postsAPI.getPosts(); // API 호출
    dispatch({ type: GET_POSTS_SUCCESS, posts }); // 성공
  } catch (e) {
    dispatch({ type: GET_POSTS_ERROR, error: e }); // 실패
  }
};

// thunk 함수에서도 파라미터를 받아와서 사용 할 수 있습니다.
export const getPost = id => async dispatch => {
  dispatch({ type: GET_POST }); // 요청이 시작됨
  try {
    const post = await postsAPI.getPostById(id); // API 호출
    dispatch({ type: GET_POST_SUCCESS, post }); // 성공
  } catch (e) {
    dispatch({ type: GET_POST_ERROR, error: e }); // 실패
  }
};

const initialState = {
  posts: {
    loading: false,
    data: null,
    error: null
  },
  post: {
    loading: false,
    data: null,
    error: null
  }
};

export default function posts(state = initialState, action) {
  switch (action.type) {
    case GET_POSTS:
      return {
        ...state,
        posts: {
          loading: true,
          data: null,
          error: null
        }
      };
    case GET_POSTS_SUCCESS:
      return {
        ...state,
        posts: {
          loading: true,
          data: action.posts,
          error: null
        }
      };
    case GET_POSTS_ERROR:
      return {
        ...state,
        posts: {
          loading: true,
          data: null,
          error: action.error
        }
      };
    case GET_POST:
      return {
        ...state,
        post: {
          loading: true,
          data: null,
          error: null
        }
      };
    case GET_POST_SUCCESS:
      return {
        ...state,
        post: {
          loading: true,
          data: action.post,
          error: null
        }
      };
    case GET_POST_ERROR:
      return {
        ...state,
        post: {
          loading: true,
          data: null,
          error: action.error
        }
      };
    default:
      return state;
  }
}

액션 생성자 함수는 dispatch를 인자로 받아 사용하며, 비동기 작업을 수행한 후에 원하는 타이밍에 dispatch 할 수 있다.

리듀서 함수 작성이 끝났으면 root 리듀서로 등록해준다.

// modules/index.js
import { combineReducers } from 'redux';
import counter from './counter';
import posts from './posts';

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

export default rootReducer;

3. client에서 보여주기

// components/PostList.js
import React from 'react';

function PostList({ posts }) {
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          {post.title}
        </li>
      ))}
    </ul>
  );
}

export default PostList;

// containers/PostListContainer.js
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import PostList from '../components/PostList';
import { getPosts } from '../modules/posts';

function PostListContainer() {
  const { data, loading, error } = useSelector(state => state.posts.posts);
  const dispatch = useDispatch();

  // 컴포넌트 마운트 후 포스트 목록 요청
  useEffect(() => {
    dispatch(getPosts());
  }, [dispatch]);

  if (loading) return <div>로딩중...</div>;
  if (error) return <div>에러 발생!</div>;
  if (!data) return null;
  return <PostList posts={data} />;
}

export default PostListContainer;

이전에 initialState로 loading, data, error 객체를 등록해주었기 때문에 client에서도 selector를 이용해 data, loading, error를 받고 그에 따라서 UI를 다르게 보여준다. 포스트 목록 요청은 컴포넌트가 처음 마운트 될 때 dispatch함수를 통해서 요청을 해준다.

logger로 action이 올바르게 처리 된 것을 확인할 수 있다.

0개의 댓글