일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- typescript
- Kotlin
- hot Reloading
- 자동반영
- 가상회선교환
- 리액트
- createPortal
- pull
- CSS
- list.map
- redux
- useCallback
- 비동기처리
- javascript
- 웹
- 리덕스
- 리액트를 다루는 기술
- async
- await
- react
- 공부
- rebase
- equalityFn
- 컬러구성
- list
- useState
- merge
- php문법
- 리사이클러뷰
- Git
- Today
- Total
공부블로그
18장. 리덕스 미들웨어를 통한 비동기 작업 관리 본문
리액트 웹 애플리케이션에서 API 서버를 연동할 때는 API 요청에 대한 상태도 잘 관리해야 한다. '미들웨어'를 사용하여 비동기 작업을 처리하는 방법을 알아보자.
1. 미들웨어란?
리덕스 미들웨어는 액션을 디스패치했을 때 리듀서에서 이를 처리하기에 앞서 사전에 저장된 작업들을 실행한다. 따라서 액션과 리듀서 사이의 중간자라고 할 수 있다.

1.1 미들웨어 만들기
액션이 디스패치될 때마다 액션의 정보와 액션이 디스패치되기 전후의 상태를 콘솔에 보여주는 로깅 미들웨어를 작성해보자.
src디렉터리에 lib디렉터리를 생성하고 그 안에 loggerMiddleware. js 파일을 생성한다.
next 를 호출하면 그다음 처리해야 할 미들웨어에게 액션을 넘겨주고 미들웨어가 없다면 리듀서에게 액션을 넘겨준다
const loggerMiddleware = (store) => (next) => (action) => {
//미들웨어 기본구조
console.group(action && action.type); //액션 타입으로 log를 그룹화함
console.log("이전 상태", store.getState());
console.log("액션", action); //액션 정보
next(action); //다음 미들웨어 혹은 리듀서에게 전달
console.log("다음상태", store.getState()); //업데이트된 상태
console.groupEnd(); //그룹 끝
};
export default loggerMiddleware;
src/ index. js ( redux-logger사용 )
//리덕스 적용하기
import React from "react";
import ReactDOM from "react-dom";
import { createStore, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import "./index.css";
import App from "./App";
import rootReducer from "./modules";
//import loggerMiddleware from "./lib/loggerMiddleware";
import { createLogger } from "redux-logger";
const logger = createLogger(); //리덕스 logger 사용하기
//리덕스 미들웨어를 스토어를 생성하는 과정에서 적용
const store = createStore(rootReducer, applyMiddleware(logger)); //루트 리듀서를 이용하여 스토어 생성
ReactDOM.render(
//Provider 컴포넌트로 App컴포넌트를 감싸서 리덕스 적용
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);

미들웨어에서는 여러 종류의 작업을 처리할 수 있다.
조건에 따라 액션을 무시할 수도, 액션 정보를 가로채서 변경한 후 리듀서에 전달해 줄 수도, 새로운 액션을 여러번 디스패치할 수도 있다.
2. 비동기 작업을 처리하는 미들웨어 사용
오픈 소스 커뮤니티에 공개된 미들웨어를 사용하여 리덕스를 사용하고 있는 프로젝트에서 비동기 작업을 더욱 효율적으로 관리한다.
- redux-thunk 미들웨어: 가장 많이 이용, 객체가 아닌 함수 형태의 액션을 디스패치할 수 있게 해줌.
- redux-saga 미들웨어: 특정 액션이 디스패치되었을 때 정해진 로직에 따라 다른 액션을 디스패치시키는 규칙을 작성
2. 1 redux-thunk
thunk는 특정 작업을 나중에 할 수 있도록 미루기 위해 함수 형태로 감싼 것을 의미한다.
1. index. js 에서 스토어를 만들 때 redux-thunk를 적용한다.
...
import ReduxThunk from "redux-thunk";
const logger = createLogger(); //리덕스 logger 사용하기
//리덕스 미들웨어를 스토어를 생성하는 과정에서 적용, 스토어 만들 때 redux-thunk를 적용
const store = createStore(rootReducer, applyMiddleware(logger, ReduxThunk));
...
2. counter. js 리덕스 모듈에서 Thunk생성 함수를 만든다.
//redux-thunk는 액션 생성 함수에서 일반 액션 객체를 반환하는 대신에 함수를 반환한다.
export const increaseAsync = () => (dispatch) => {
//1초 뒤에 increase 함수를 디스패치
setTimeout(() => {
dispatch(increase());
}, 1000);
};
export const decreaseAsync = () => (dispatch) => {
setTimeout(() => {
dispatch(decrease());
}, 1000);
};
3. CounterContainer. js 에서 호출하는 액션 생성함수를 변경해준다.
const CounterContainer = ({
number,
increaseAsync,
decreaseAsync,
}) => {
return (
<Counter
number={number}
onIncrease={increaseAsync}
onDecrease={decreaseAsync}
/>
);
};
export default connect(
(state) => ({
// 리덕스 스토어안에 있는 상태
number: state.counter,
}),
{
increaseAsync, //액션을 디스패치하는 함수들
decreaseAsync,
}
)(CounterContainer);

4. 웹 요청 비동기 작업 처리
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 )를 만든다.
modules/ sample. js
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;
새로운 리듀서는 루트 리듀서에 포함시킨다.
데이터를 렌더링할 프레젠테이셔널 컴포넌트를 작성한다.
components/ Sample. js
import React from "react";
const Sample = ({ post, users, loadingPost, loadingUsers }) => {
return (
<div>
<section>
<h1>포스트</h1>
{loadingPost && "로딩중..."}
{!loadingPost &&
post && ( //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;
컨테이너 컴포넌트를 만든다.
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,
}) => {
// 클래스 형태 컴포넌트였더라면, componentDidMount
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. js에서 렌더링한다.

5. 리팩토링
API를 요청할 때마다 thunk함수를 작성하는 것과 로딩 상태를 리듀서에서 관히라는 작업은 귀찮으므로 반복되는 로직을 따로 분리한다.
lib/ createRequestThunk. js
import { startLoading, finishLoading } 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);
이렇게 만든 유틸 함수는 API요청을 해주는 thunk함수를 한 줄로 생성할 수 있게 해준다. 액션 타입과 API를 요청하는 함수를 파라미터로 넣어준다.
modules/ sample. js
export const getPost = createRequestThunk(GET_POST, api.getPost);
export const getUsers = createRequestThunk(GET_USERS, api.getUsers);
로딩 상태를 관리하는 작업을 개선하기 위해 리듀서 내부에서 각 요청에 관련된 액션이 디스패치될 때마다 로딩 상태를 변경해 주던 것을 로딩 상태만 관리하는 리덕스 모듈을 따로 생성하여 처리한다.
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;
만든 loading 리듀서를 루트 리듀서에 포함시킨다.
createRequestThunk에서 loading 리덕스 모듈에서 만든 액션 생성함수를 사용한다.
import { startLoading, finishLoading } from "../modules/loading";
...
dispatch(startLoading(type));
try{
...dispatch(finishLoading(type));
}catch(e){
...dispatch(startLoading(type));
}
SampleContainer에서 로딩상태를 조회할 수 있다.
export default connect(
({ sample, loading }) => ({
post: sample.post,
users: sample.users,
loadingPost: loading["sample/GET_POST"],
loadingUsers: loading["sample/GET_USERS"],
}),
{
getPost,
getUsers,
}
최종적으로 불필요한 코드를 지우면 sample 리듀서의 코드가 깔끔해진다.
...
const initialState = {
/*loading: {
GET_POST: false,
GET_USERS: false,
},*/
post: null,
users: null,
};
const sample = handleActions(
{
[GET_POST_SUCCESS]: (state, action) => ({
...state,
post: action.payload,
}),
[GET_USERS_SUCCESS]: (state, action) => ({
...state,
users: action.payload,
}),
},
initialState
);
2. 2 redux-saga
좀 더 까다로운 상황에서 유용
- 기존 요청을 취소 처리해야 할 때
- 특저 액션이 발생했을 때 다른 액션을 발생시키거나, API요청 등 리덕스와 관계없는 코드를 실행할 때
- 웹 소켓을 사용할 때
- API요청 실패 시 재요청해야 할 때
제너레이터 함수 사용: 함수를 작성할 때 함수를 특정 구간에 멈춰 놓을 수도 있고, 원할 때 다시 돌아가게 할 수도 있다. function* 키워드 사용, next( )가 호출되면 다음 yield가 있는 곳까지 호출하고 다시 함수가 멈춘다.
'리액트 > 리액트를 다루는 기술' 카테고리의 다른 글
19장 코드 스플리팅, 20강 서버 사이드 렌더링 (0) | 2021.02.18 |
---|---|
17장. 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기 (0) | 2021.02.09 |
14장. 외부 API를 연동하여 뉴스 뷰어 만들기 (0) | 2021.02.03 |
13장. 리액트 라우터로 SPA 개발하기 (0) | 2021.02.02 |
11장. 컴포넌트 성능 최적화 (0) | 2021.01.23 |