벨로퍼트 리액트 - 7. 리덕스 미들웨어
7-5. redux-thunk로 프로미스 다루기
❗❗필요한 선행지식 : 자바스크립트의 Promise
자바스크립트의 동기적 처리와 비동기 처리
- 동기적 처리 : 작업이 끝날때까지 기다리는 동안 중지상태가 됨으로 다른 작업을 할 수 없음. 현재 작업이 끝나야지만 다음 작업으로 넘어간다.
- 비동기적 처리 : 현재 작업을 처리중이더라도 멈추지 않기 때문에 여러 작업을 처리할 수도 있고 기다리는 과정에서 다른 함수도 호출할 수 있다.
비동기 처리가 사용되는 작업들
- Ajax Web API 요청 : 서버쪽에서 데이터를 받아와야 할때는 요청을 하고 서버에서 응답을 할때까지 대기해야하기 때문에 비동기로 처리한다.
- 파일 읽기 : 주로 서버 쪽에서 파일을 읽어야 하는 상황에는 비동기적으로 처리함.
- 암호화/복호화 : 암호화/복호화를 할때도 바로 처리가 되지 않고 시간이 걸리는 경우가 있기 때문에 비동기적으로 처리한다.
- 작업 예약 : 특정 작업을 몇초 후에 스케줄링 해야 하는 경우, setTimeout을 사용하여 비동기적으로 처리한다.
Promise
비동기 작업을 편하게 처리 할 수 있도록 ES6에 도입된 기능. 이전에는 비동기 작업 처리시 콜백 함수로 처리를 했어야 했는데, 이 경우에는 비동기 작업이 많아질 경우 코드가 쉽게 난잡해졌다.
⇒ 비동기적으로 처리해야하는 일이 많아질수록 코드의 깊이가 깊어지는 현상이 발생하는데, Promise를 사용하면 이를 방지할 수 있다.
1) 가짜 API 함수 만들기
Promise를 사용하여 데이터를 반환하는 가짜 API 함수 만들기.
src/api/posts.js
// n 밀리세컨드 동안 기다리는 프로미스를 만들어주는 함수
const sleep = n => new Promise(resolve => setTimeout(resolve, n));
// 가짜 포스트 목록 데이터
const posts = [
{
id: 1,
title: '리덕스 미들웨어를 배워봅시다',
body: '리덕스 미들웨어를 직접 만들어보면 이해하기 쉽죠'
},
{
id: 2,
title: 'redux-thunk를 사용해봅시다',
body: 'redux-thunk를 사용해서 비동기 작업을 처리해 봅시다!'
},
{
id: 3,
title: 'redux-saga도 사용해봅시다',
body: '나중엔 redux-saga를 사용해서 비동기 작업을 처리하는 방법도 배워볼거에요'
},
];
// 포스트 목록을 가져오는 비동기 함수
export const getPosts = async () => {
await sleep(500); // 0.5초 쉬고
return posts; // posts 배열
};
// ID로 포스트를 조회하는 비동기 함수
export const getPostById = async id => {
await sleep(500); // 0.5초 쉬고
return posts.find(post => post.id === id); // id로 찾아서 반환
};
2) posts 리덕스 모듈 준비하기
프로미스를 다루는 리덕스 모듈을 다룰때 고려해야 할 사항
- 프로미스가 시작, 성공, 실패했을때 각각 다른 액션을 디스패치해야한다.
- 각 프로미스마다 thunk 함수를 만들어주어야 한다.
- 리듀서에서 액션에 따라 로딩중, 결과, 에러 상태를 변경해 주어야 한다.
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;
}
};
3) 리덕스 모듈 리팩토링 하기
src/lib/asyncUtils.js
// Promise에 기반한 Thunk를 만들어주는 함수
export const createPromiseThunk = (type, promiseCreator) => {
const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
// 이 함수는 promiseCreator가 단 하나의 파라미터만 받는다는 전제 하에 작성됨
// 만약 여러 종류의 파라미터를 전달해야한다면 객체 타입의 파라미터를 받아오도록 작성하면 됨
// 예) writeComment({ postId: 1, text: '댓글 내용' })
return param => async dispatch => {
// 요청 시작
dispatch({ type, param });
try {
// 결과물의 이름을 payload 라는 이름으로 통일시킵니다
const payload = await promiseCreator(param);
dispatch({ type: SUCCESS, payload }); // 성공
} catch (e) {
dispatch({ type: ERROR, payload: e, error: true}); // 실패
}
};
};
// 리듀서에서 사용할 수 있는 여러 유틸 함수
export const reducerUtils = {
// 초기상태.
// 초기 data 값은 기본적으로 null 이지만 바꿀수도 있다
initial: (initialData = null) => ({
loading: false,
data: initialData,
error: null
}),
// 로딩중 상태.
// prevState의 경우에 기본값은 null 이지만 따로 값을 지정하면 null 로 바꾸지 않고 다른 값을 유지시킬 수 있다
loading: (prevState = null) => ({
loading: true,
data: prevState,
error: null
}),
// 성공 상태
success: payload => ({
loading: false,
data: payload,
error: null
}),
// 실패 상태
error: error => ({
loading: false,
data: null,
error: error
})
};
위 함수들을 사용하여 기존 posts 모듈 리팩토링
⇒ modules/posts.js 수정
import * as postsAPI from '../api/posts'; // api/posts 안의 함수 모두 불러오기
import { createPromiseThunk, reducerUtils } from '../lib/asyncUtils';
// 액션 타입
// 포스트 여러개 조회하기
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 = createPromiseThunk(GET_POSTS, postsAPI.getPosts);
export const getPost = createPromiseThunk(GET_POST, postsAPI.getPostById);
// => 리팩토링
const initialState = {
posts: reducerUtils.initial(),
post: reducerUtils.initial()
}
// => 리팩토링
export default function posts(state = initialState, action) {
switch (action.type) {
case GET_POSTS:
return {
...state,
posts: reducerUtils.loading()
}
case GET_POSTS_SUCCESS:
return {
...state,
posts: reducerUtils.success(action.payload) // action.posts가 변경됨
}
case GET_POSTS_ERROR:
return {
...state,
posts: reducerUtils.error(action.error)
}
case GET_POST:
return {
...state,
post: reducerUtils.loading()
}
case GET_POST_SUCCESS:
return {
...state,
post: reducerUtils.success(action.payload)
}
case GET_POST_ERROR:
return {
...state,
post: reducerUtils.error(action.error)
}
default:
return state;
}
};
lib/asyncUtils.js 에 handleAsyncActions 함수 작성
// Promise에 기반한 Thunk를 만들어주는 함수
export const createPromiseThunk = (type, promiseCreator) => {
const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
// 이 함수는 promiseCreator가 단 하나의 파라미터만 받는다는 전제 하에 작성됨
// 만약 여러 종류의 파라미터를 전달해야한다면 객체 타입의 파라미터를 받아오도록 작성하면 됨
// 예) writeComment({ postId: 1, text: '댓글 내용' })
return param => async dispatch => {
// 요청 시작
dispatch({ type, param });
try {
// 결과물의 이름을 payload 라는 이름으로 통일시킵니다
const payload = await promiseCreator(param);
dispatch({ type: SUCCESS, payload }); // 성공
} catch (e) {
dispatch({ type: ERROR, payload: e, error: true}); // 실패
}
};
};
// 리듀서에서 사용할 수 있는 여러 유틸 함수
export const reducerUtils = {
// 초기상태.
// 초기 data 값은 기본적으로 null 이지만 바꿀수도 있다
initial: (initialData = null) => ({
loading: false,
data: initialData,
error: null
}),
// 로딩중 상태.
// prevState의 경우에 기본값은 null 이지만 따로 값을 지정하면 null 로 바꾸지 않고 다른 값을 유지시킬 수 있다
loading: (prevState = null) => ({
loading: true,
data: prevState,
error: null
}),
// 성공 상태
success: payload => ({
loading: false,
data: payload,
error: null
}),
// 실패 상태
error: error => ({
loading: false,
data: null,
error: error
})
};
// 비동기 관련 액션 처리하는 리듀서
// type: 액션의 타입, key: 상태의 key (ex: posts, post)
export const handleAsyncActions = (type, key) => {
const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
return (state, action) => {
switch (action.type) {
case type:
return {
...state,
[key]: reducerUtils.loading()
};
case SUCCESS:
return {
...state,
[key]: reducerUtils.success(action.payload)
};
case ERROR:
return {
...state,
[key]: reducerUtils.error(action.payload)
};
default:
return state;
}
};
};
modules/posts.js 최종 리팩토링
import * as postsAPI from '../api/posts'; // api/posts 안의 함수 모두 불러오기
import {
createPromiseThunk,
reducerUtils,
handleAsyncActions
} from '../lib/asyncUtils';
// 액션 타입
// 포스트 여러개 조회하기
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 = createPromiseThunk(GET_POSTS, postsAPI.getPosts);
export const getPost = createPromiseThunk(GET_POST, postsAPI.getPostById);
// => 리팩토링
const initialState = {
posts: reducerUtils.initial(),
post: reducerUtils.initial()
}
// => 최종 리팩토링
export default function posts(state = initialState, action) {
switch (action.type) {
case GET_POSTS:
case GET_POSTS_SUCCESS:
case GET_POSTS_ERROR:
return handleAsyncActions(GET_POSTS, 'posts')(state, action);
case GET_POST:
case GET_POST_SUCCESS:
case GET_POST_ERROR:
return handleAsyncActions(GET_POST, 'post')(state, action);
default:
return state;
}
};
모듈을 루트 리듀서에 등록
modules/index.js 수정
import { combineReducers } from 'redux';
import counter from './counter';
import posts from './posts';
const rootReducer = combineReducers({ counter, posts });
export default rootReducer;