리액트에서 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 |