1. middleware?
: 미들웨어는 액션을 디스패치했을 때 리듀서에서 이를 처리하기 전에 사전에 지정된 작업들을 실행
-> 액션과 리듀서 사이의 중간자
1-1. loggerMiddleware.js 구현
const middleware = store => next => action => {
// 현재 스토어 상태 값 기록
console.log('현재 상태', store.getState());
// 액션 기록
console.log('액션',action);
// 액션을 다음 미들웨어 또는 리듀서에 전달
const result = next(action);
// 액션 처리 후 스토어의 상태를 기록
console.log('다음 상태', store.getState());
return result; // 여기에서 반환하는 값은 store.dispatch(ACTION_TYPE)했을 때 결과로 설정한다.
}
export default middleware;// 내보내기
- next(action) 을 했을 때는 그다음 처리해야 할 미들웨어로 액션을 넘겨주고, 추가로 처리할 미들웨어가 없다면 바로 리듀서에 넘겨준다
- store.dispatch는 다음 미들웨어로 넘기는 것이 아니라 액션을 처음부터 디스패치한다
1-2. 미들웨어 적용
import { createStore, applyMiddleware } from 'redux';
import modules from './modules';
import middleware from './lib/middleware';
// 미들웨어가 여러 개일 경우 파라미터로 전달하면 된다. (applyMiddleware(a,b,c))
// 미들웨어 순서는 여기에서 전달한 파라미터 순서대로 지정한다.
const store = createStore(modules, applyMiddleware(middleware));
export default store;
1-3. redux-logger 라이브러리
import { createStore, applyMiddleware } from 'redux';
import modules from './modules';
import {createLogger} from 'redux-logger';
const logger = createLogger();
const store = createStore(modules, applyMiddleware(logger));
export default store;
1-4 . 결과
*크롬브라우저의 콘솔창 확인
2. 미들웨어로 비동기 액션 처리
: 여러 오픈소스 라이브러리 존재 : redux-thunk, redux-saga, redux-pender, redux-promise-middleware etc...
2-1. redux-thunk 모듈
- thunk : 특정작업을 나중에 할 수 있도록 함수 형태로 감싼 것
- redux-thunk : 함수를 디스패치함으로써 일반 액션 객체로는 할 수 없는 작업을 가능하게함
ex) 1초 뒤에 액션이 디스패치되는 코드
import { createStore, applyMiddleware } from 'redux';
import modules from './modules';
import {createLogger} from 'redux-logger';
import ReduxThunk from 'redux-thunk';
const logger = createLogger();
const store = createStore(modules, applyMiddleware(logger, ReduxThunk));
export default store;
import {handleActions, createAction} from 'redux-actions';
//action types
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
//action creators
export const increment = createAction(INCREMENT);
export const decrement = createAction(DECREMENT);
//thunks
export const incrementAsync = () => dispatch => {
// 1초 뒤 액션 디스패치
setTimeout(
() => { dispatch(increment()) }, //함수를 dispatch
1000
);
}
export const decrementAsync = () => dispatch => {
// 1초 뒤 액션 디스패치
setTimeout(
() => { dispatch(decrement()) }, //함수를 dispatch
1000
);
}
//reducer
export default handleActions({
[INCREMENT]: (state, action) => state +1,
[DECREMENT]: (state, action) => state -1
}, 0);
3. Promise
- Promise : 비동기 처리를 다루는데 사용하는 객체
function printLater(number, fn) {
setTImeout(
function() {
console.log(number);
if(fn) fn();
},
1000
);
}
//콜백지옥
printLater(1, function() {
printLater(2, function() {
printLater(3, function() {
printLater(4);
})
})
});
위와 같은 콜백지옥( 비동기적 작업에 의한 깊고 복잡해진 코드)을 Promise를 통해 다음과 같이 해결
function printLater(number) {
return new Promise( // Promise 생성 후 리턴
(resolve, reject) => {
if ( number > 4 ) {
return reject('number is greater than 4'); // reject 는 오류를 발생시킨다.
}
setTimeout( // 1초 뒤 실행
() => {
console.log(number);
resolve(number + 1); // 현재 숫자에 1을 더한 값을 반환한다.
}, 1000
);
}
);
}
printLater(1)
.then( num => printLater(num) )
.then( num => printLater(num) )
.then( num => printLater(num) )
.then( num => printLater(num) )
.catch( e => console.log(e) );
- Promise 에서 결과 값을 반환할 때는 resolve(결과 값) 을 작성하고, 오류를 발생시킬 때는 reject(오류)를 작성.
- 여기에서 반환하는 결과 값은.then(), 오류 값은.catch() 에 전달하는 함수의 파라미터로 설정됨.
4. redux-thunk와 axios를 사용한 웹 요청 처리
- axios : Promise 기반 HTTP 클라이언트 라이브러리
yarn add axios
4-1. post 모듈 : API함수, 액션, 액션생성함수, thunk, reducer
// src/modules/post.js
import {handleActions, createAction} from 'reudx-actions';
import axios from 'axios';
//API function
funciton getPostAPI(postId) {
return axios.get(`url/${postId}`); //Promise 객체형태로 반환
}
//action types
const GET_POST_PENDING = 'GET_POST_PENDING';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_FAILURE = 'GET_POST_FAILURE';
//action creators
const getPostPending = createAction(GET_POST_PENDING);
const getPostSuccess = createAction(GET_POST_SUCCESS);
const getPostFailure = createAction(GET_POST_FAILURE);
//thunk
export const getPost = (postId) => dispatch => {
dispatch(getPostPending()); // 요청 시작했다는 것을 알림
// 요청 시작. 여기에서 만든 promise를 return해야 나중에 컴포넌트에서 호출할 때 getPost().then(...)을 할 수 있다.
return getPostAPI(postId)
.then( () => {
// 요청이 성공했다면 서버 응답 내용을 payload로 설정하여 GET_POST_SUCCESS 액션을 디스패치한다.
dispatch(getPostSuccess(response));
// 후에 getPostAPI.then 을 했을 때 then에 전달하는 함수에서 response 에 접근할 수 있게 한다.
return response;
} )
.catch( error => {
// 오류가 발생하면 오류 내용을 payload로 설정하여 GET_POST_FAILURE 액션을 디스패치한다.
dispatch(getPostFailure(error));
// error를 throw하여 이 함수를 실행한 후 다시 한 번 catch를 할 수 있게 한다.
throw(error);
} );
}
const initialState = {
pending: false,
error: false,
data: {
title: '',
body: ''
}
}
//reducer
export default handleAction({
[GET_POST_PENDING]: (state, action) => {
return {
...state,
pending: true,
error: false
};
},
[GET_POST_SUCCESS]: (state, action) => {
const {title, body} = action.payload.data;
return {
...state,
pending: false,
data: {
title,
body
}
};
},
[GET_POST_FAILURE]: (state, action) => {
return {
...state,
pending: false,
error: true
}
}
}, initialState);
- thunk : 요청상태에 따라 액션을 디스패치
4-2. post 모듈의 리듀서를 루트 리듀서에 넣어준다
import {combineReducers} from 'redux';
import counter from './counter';
import post from './post';
export default combineReducers({
counter,
post
});
4-3. counter의 기본 값을 postId로 사용하므로 1로 설정, 0이면 오류 발생
// scr/modules/counter.js
(...)
export default handleActions({
[INCREMENT]: (state, action) => state + 1,
[DECREMENT]: (state, action) => state - 1
},1); // counter의 기본 값을 postId로 사용하므로 1로 설정, 0이면 오류 발생
4-4. App 컴포넌트에서 액션으로 웹 요청 시도
// src/App.js
import React, {Component} from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import * as counterActions from './modules/counter';
import * as postActions from './modules/post';
class App extends Component {
loadData = () => {
const {PostActions, number} = this.props;
PostActions.getPost(number);
}
componentDidMount() {
this.loadData();
}
componentDidUpdate(prevProps, prevState) {
// 이전 number 와 현재 number 가 다르면 요청을 시작
if(this.props.number != prevProps.number) {
this.loadData();
}
}
render() {
const {CounterActions, number, post, error, loading} = this.props;
return (
<div>
<h1>{number}</h1>
{
( () => {
if (loading) return (<h2>로딩중...</h2>);
if (error) return (<h2>에러발생...</h2>);
return (
<div>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
);
} )
}
<button onClick={CounterActions.increment}>+</button>
<button onClick={CounterActions.decrement}>-</button>
</div>
);
}
}
export default connect(
(state) => ({
number: state.counter,
post: state.post.data,
loading: state.post.pending,
error: state.post.error
}),
(dispatch) => ({
CounterActions: bindActionCreators(counterActions, dispatch),
PostActions: bindActionCreators(postActions, dispatch)
})
)(App);
4-5. 요청 완료후 작업및 오류 발생시 작업 추가
// es6
// loadData = () => {
// const { PostActions, number } = this.props;
// //this.props.Postactions.getPost(this.props.number);
//
// PostActions.getPost(number).then( // 요청 완료 후 작업
// (response) => {
// console.log(response);
// }
// ).catch( // 오류 발생시 작업
// (error) => {
// console.log(error);
// }
// );
// };
//es7
loadData = async () => {
const { PostActions, number } = this.props;
// this.props.Postactions.getPost(this.props.number);
try {
const response = await PostActions.getPost(number); // 요청 완료 후 작업
console.log(response);
}
catch (error) { // 오류 발생시 작업
console.log(error);
}
};
- await를 쓸 함수의 앞부분에 async 키워드를 붙인다.
- 기다려야 할 Promise 앞에 await 키워드를 붙인다.
- await를 사용할 때는 꼭 try~catch문으로 오류를 처리해야 한다.
4-6. 요청취소기능 추가
*redux-pender, redux-promise-middleware는 잘 쓰이지 않으므로 스킵..