리덕스 미들웨어를 통한 비동기 작업 처리

Posted by yunki kim on July 29, 2021

  리액트에서 API서버를 연동할 때는 API요청에 대한 상태를 잘 관리 해야 한다. 요청 시작 후 로딩 중, 요청 성공 또는 실패했을 때는 로딩이 끝났다는 것을 명시해야 한다. 이런 작업은 리덕스 미들웨어를 사용해서 효율적으로 처리할 수 있다.

  리덕스 미들웨어는 액션을 디스패치 한 후 리듀서가 이를 처리하기 전에 지정된 작업을 실행한다. 이때 미들웨어 에서는 전달 받은 액션을 단순히 콘솔에 기록하거나, 전달 받은 액션 정보를 기반으로 액션을 취소하거나, 다른 종류의 액션을 디스패치 하는 등의 일을 할 수 있다.

  사실 이미 수 많은 미들웨어가 존재 하기 때문에 미들웨어를 직접 만들 일은 거의 없다. 하지만 배우는 단계 이므로 간단한 미들웨어를 만들어 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//lib/loggerMiddleware.js
//store는 리덕스 스토어 인스턴스
//action은 디스패치된 액션
//next는 함수형태 이며, sotre.dispatch와 비슷한 역할을 한다
//next(action)을 하면 다음에 처리할 미들웨어 에게 액션을 넘긴다.
//다음 미들웨어가 없으면 리듀서에게 액션을 넘긴다.
//next를 사용하지 않으면 액션이 리듀서에 전달되지 않는다.
const loggerMiddleWare = store => next => action => {
    console.group(action && action.type);//액션 타입으로 log를 그룹화
    console.log('previous state', store.getState());
    console.log('action', action);
    next(action);//다음 미들웨어 또는 리듀서에 전달
    console.log('next state', store.getState());
    console.groupEnd();
};
 
export default loggerMiddleWare;
cs
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
//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 reportWebVitals from './reportWebVitals';
import rootReducer from "./modules";
import loggerMiddleWare from "./lib/loggerMiddleware";
 
//리덕스 미들웨어 스토어에 적용
//미들웨어는 스토어를 생성하는 과정에서 적용 된다.
const store = createStore(rootReducer, applyMiddleware(loggerMiddleWare));
 
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
 
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
 
cs

redux-logger

  커뮤니티에 올라와 있는 로거 미들웨어 이다.

1
2
3
import {createLogger} from 'redux-logger/src';
const logger = createLogger();
const store = createStore(rootReducer, applyMiddleware(logger));
cs

리덕스 미들웨어를 통한 비동기 작업 관리

  비동기 작없을 처리할때 다음과 같은 미들웨어를 사용하면 된다.

    reudx-thunk: 비등기 작업을 처리할 때 가장 많이 사용하는 미들웨이다. 객체가 하닌 함수 형태의 액션을 디스패치할 수 있게 한다.

    redux-saga: 특정 액션이 디스패치 되었을 때 정해진 로직에 따라 다른 액션을 디스패치 시키는 규칙을 작성해 비동기 작업을 처리할 수 있게 한다.

 

  redux-thunk

    Thunk는 특정 장업을 나중에 할 수 있게 미루기 위해 함수 형태로 감싼 것을 의미한다.

1
2
3
4
5
6
7
const addOne = x => x + 1;
const addOneThunk = x => () => addOne(x);
const fn = addOneThunk(1);
setTimeout(() => {
    const val = fn();
    console.log(val);
}, 1000);
cs

  redux-thunk는 다음과 같이 사용하면 된다

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
//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 reportWebVitals from './reportWebVitals';
import rootReducer from "./modules";
import {createLogger} from "redux-logger/src";
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')
);
 
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
 
cs
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
31
32
33
//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);
 
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;
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//containers/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);
cs

  redux와  웹 요청 비동기 작업 처리

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
//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함수 생성
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(err) {
        dispatch({
            type: GET_POST_FAILURE,
            payload: err,
            error: true
        });
        throw err;
    }
};
 
export const getUsers = () => async dispatch => {
    dispatch({type: GET_USERS});//요청 시작을 알린다
    try {
        const response = await api.getUsers();
        dispatch({
            type: GET_USERS_SUCCESS,
            payload: response.data,
        });
    }
    catch(err) {
        dispatch({
            type: GET_USERS_FAILURE,
            payload: err,
            error: true,
        });
        throw err;
    }
};
 
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;
cs
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
31
32
33
34
35
36
37
38
39
40
41
//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,
}) => {
    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);
cs
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
31
32
33
34
35
//components/Sample.js
import React from 'react';
 
const Sample = ({loadingPost, loadingUsers, post, users}) => {
    return (
        <div>
            <section>
                <h1>post</h1>
                {loadingPost && 'loading...'}
                {!loadingPost && post && (
                    <div>
                        <h3>{post.title}</h3>
                        <h3>{post.body}</h3>
                    </div>
                )}
            </section>
            <hr/>
            <section>
                <h1>user lists</h1>
                {loadingUsers && 'loading...'}
                {!loadingUsers && users && (
                    <ul>
                        {users.map(user => (
                            <li key={user.id}>
                                {user.username} ({user.email})
                            </li>
                        ))}
                    </ul>
                )}
            </section>
        </div>
    )
}
 
export default Sample;
cs

위의 코드는 다음과 같이 줄일 수 있다.

 

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
//lib/createRequestThunk.js
import {startLoading, finishLoading} from "../modules/loading";
 
export default function createRequesThunk(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(err) {
            dispatch({
                type: FAILURE,
                payload: err,
                error: true
            });
            dispatch(startLoading(type));
            throw err;
        }
    };
}
cs

 

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
31
32
33
34
35
36
37
38
39
//modules/sample.js
import {handleActions} from 'redux-actions';
import * as api from '../lib/api';
import createRequesThunk 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함수 생성
export const getPost = createRequesThunk(GET_POST, api.getPost);
export const getUsers = createRequesThunk(GET_USERS, api.getUsers);
 
const initialState = {
    loading: {
        GET_POST: false,
        GET_USERS: false,
    },
};
 
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;
cs

 

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
31
32
33
//modules/loading.js
import {createAction, handleActions} from 'redux-actions';
 
const START_LOADING = 'loading/START_LOADING';
const FINISH_LOADING = 'loading/FINISH_LOADING';
 
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;
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
//modules/index.js
import
 {combineReducers} from "redux";
import counter from './counter';
import sample from './sample';
import loading from "./loading";
 
const rootReducer = combineReducers({
    counter,
    sample,
    loading,
});
 
export default rootReducer;
cs

 

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
31
32
33
34
35
36
37
38
39
40
41
//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,
}) => {
    useEffect(() => {
        getPost(1);
        getUsers(1);
    }, [getPost, getUsers]);
    return (
        <Sample
            post={post}
            users={users}
            loadingPost={loadingPost}
            loadingUsers={loadingUsers}
        />
    );
};
 
export default connect(
    ({sample, loading}) => ({
        post: sample.post,
        users: sample.users,
        loadingPost: loading['sample/GET_POST'],
        loadingUsers: loading['sample/GET_USERS'],
    }),
    {
        getPost,
        getUsers,
    }
)(SampleContainer);
cs