본문 바로가기
Frontend

middleware

by yongmin.Lee 2020. 10. 24.

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는 잘 쓰이지 않으므로 스킵..




'Frontend' 카테고리의 다른 글

1. basic react  (0) 2020.10.24
0. React intro  (0) 2020.10.24
Redux in React  (0) 2020.10.24
Redux  (0) 2020.10.24
Redux Life Cycle  (0) 2020.10.24