Velopert님의 블로그를 보면서 공부한 내용을 정리한 글입니다. Redux-Thunk
지난 포스팅에서는 setTimeout을 이용한 간단한 비동기 처리를 구현하였지만, 이번 포스팅에서는 실제 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번 포트에 가상 웹서버가 열릴 것이다.
우선 클라이언트에서 서버로부터 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라는 리듀서를 생생해보겠다.
프로미스를 다루는 리덕스 모듈을 다룰 땐 다음과 같은 사항을 고려해야 한다.
// 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;
// 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이 올바르게 처리 된 것을 확인할 수 있다.