리덕스 미들웨어는 액션을 디스패치했을 때 리듀서에서 이를 처리하기에 앞서 사전에 지정된 작업을 실행합니다.
미들웨어 = 액션과 리듀서 사이의 중간자
미들웨어 기본 구조
const loogerMiddleware = function loggerMiddleware(store) {
return function(next) {
return function(action) {
//미들웨어 기본 구조
}
}
}
미들웨어는 결국 함수를 반환하는 함수를 반환하는 함수 입니다.
store.dispatch
와 비슷한 역학을 합니다.리덕스에서 미들웨어를 사용할 때는 이미 완성된 미들웨어를 라이브러리로 설치해서 사용하는 경우가 많습니다.
redux-thunk는 리덕스를 사용하는 프로젝트에서 비동기 작업을 처리할 때 가장 기본적으로 사용하는 미들웨어입니다.
특정 작업을 나중에 할 수 있도록 미루기 위해 함수 형태로 감싼 것을 의미합니다.
reudx-thunk 라이브러리를 사용하면 thunk 함수를 만들어서 디스패치할 수 있습니다.
그러면 리덕스 미들웨어가 그 함수를 전달받아 store의 dispatch 와 getState를 파라미터로 넣어서 호출해 줍니다.
const sampleThunk = () => (dispatch, getState) => {
//현재 상태를 참조할 수 있고,
// 새 액션을 디스패치할 수도 있습니다.
}
설치
npm install redux-thunk
src/index.js
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 { createLogger } from "redux-logger";
import ReduxThunk from "redux-thunk";
const logger = createLogger();
const store = createStore(rootReducer, applyMiddleware(logger, ReduxThunk));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
redux-thunk는 액션 생성 함수에서 일반 액션 객체를 반환하는 대신에 함수를 반환합니다.
modules/counter.js
import { createAction, handleActions } from "redux-actions";
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
// 1초 뒤에 increase 혹은 decrease 함수를 디스패치함
export const increaseAsync = () => (dispatch) => {
setTimeout(() => {
dispatch(increase());
}, 1000);
};
export const decreaseAsync = () => (dispatch) => {
setTimeout(() => {
dispatch(decrease());
}, 1000);
};
const initialState = 0;
const counter = handleActions(
{
[INCREASE]: (state) => state + 1,
[DECREASE]: (state) => state - 1,
},
initialState
);
export default counter;
container/CounterContainer.js
import React from "react";
import { connect } from "react-redux";
import { increaseAsync, decreaseAsync } from "../modules/counter";
import Counter from "../components/Counter";
const CounterContainer = ({ number, increaseAsync, decreaseAsync }) => {
return (
<Counter
number={number}
onIncrease={increaseAsync}
onDecrease={decreaseAsync}
/>
);
};
export default connect(
(state) => ({
number: state.counter,
}),
{
increaseAsync,
decreaseAsync,
}
)(CounterContainer);
처음 디스패치되는 액션은 함수 형태이고, 두 번째 액션은 객체 형태입니다.
thunk 속성을 활용하여 웹 요청 비동기 작업을 처리하는 방법에 대해 알아보겠습니다.
웹 요청을 연습하기 위해 사용할 가짜 API
# 포스트 읽기( :id는 1~100 사이 숫자)
GET https://jsonplaceholder.typicode.com/posts/:id
# 모든 사용자 정보 불러오기
GET https://jsonplaceholder.typicode.com/users
API를 호출할 때는 주로 Promise 기반 웹 클라이언트인 axios를 사용합니다.
_설치
$ npm install axios
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`);
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_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 = (id) => async (dispatch) => {
dispatch({ type: GET_USERS }); //요청을 시작한 것을 알림
try {
const response = await api.getUsers(id);
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;
module/index.js
import { combineReducers } from "redux";
import counter from "./counter";
import sample from "./sample";
const rootReducer = combineReducers({
counter,
sample,
});
export default rootReducer;
반복되는 로직을 따로 분리하여 코드의 양을 줄입니다.
lib/createRequestThunk.js
export default function createRequestThunk(type, request) {
//성공 및 실패 액션 타입을 정의합니다.
const SUCCESS = `${type}_SUCCESS`;
const FAILURE = `${type}_FAILURE`;
return (params) => async (dispatch) => {
dispatch({ type }); //시작됨
try {
const response = await request(params);
dispatch({
type: SUCCESS,
payload: response.data,
}); //성공
} catch (e) {
dispatch({
type: FAILURE,
payload: e,
error: true,
}); //에러 발생
throw e;
}
};
}
modules/sample.js
import createRequestThunk from "../lib/createRequestThunk";
...
// thunk 함수를 생성합니다.
// thunk 함수 내부에서는 시작할 때, 성공했을 때, 실패했을 때 다른 액션을 디스패치합니다.
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 = "loaidng/FINSIH_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;
루트 리듀서에 포함시켜줍니다.
modules/index.js
import loading from "./loading";
const rootReducer = combineReducers({
counter,
sample,
loading,
});
export default rootReducer;
loading 리덕스 모듈에서 만든 액션 생성 함수는 앞에서 만든 createRequestThunk에서 사용해 줍니다.
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;
}
};
}
로딩 상태를 다음과 같이 조회할 수 있습니다.
containers/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);
최종 moudle/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_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 = createRequestThunk(GET_POST, api.getPost);
export const getUsers = createRequestThunk(GET_USERS, api.getUsers);
// 초기 상태를 선언합니다.
// 요청의 로딩 중 상태는 loading이라는 객체에서 관리합니다.
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
);
export default sample;