2-2. 웹 요청 처리하기

비동기작업 처리해보기

redux-thunk 를 사용하여 비동기 작업을 한번 처리해보겠습니다. 우리는 axios 라는 라이브러리를 이용햐여 웹 요청을 하겠습니다. axios 는 Promise 기반 HTTP Client 입니다.

Promise 가 뭔가요?

Promise는 ES6 에서 비동기 처리를 다루기위해 사용되는 객체입니다.

예를들어서, 숫자를 1초뒤에 프린트하는 코드를 작성해보겠습니다.

이 코드를 크롬 개발자 도구에서 실행해보세요. (크롬 개발자 콘솔에서 새 줄을 입력 할땐 SHIFT 키를 누르고 엔터를 누르면 됩니다)

function printLater(number) {
    setTimeout(
        function() { 
            console.log(number); 
        },
        1000
    );
}

printLater(1);

이렇게 doItLater 함수 안에 1 을 프린트하는 함수를 전달해서 호출을 하면, 1초뒤에 프린트가 됩니다.

이번엔 1 초에 걸쳐서 숫자를 더해가면서 1, 2, 3, 4를 프린트하는 코드를 작성해보겠습니다.

function printLater(number, fn) {
    setTimeout(
        function() { console.log(number); fn(); },
        1000
    );
}

printLater(1, function() {
    printLater(2, function() {
        printLater(3, function() {
            printLater(4);
        })
    })
})

비동기적으로 해야 할 작업이 많아진다면, 코드의 구조는 자연스레 깊어질 것이고 그러면 코드를 읽기 힘들어지겠죠? 이를 콜백 지옥이라고도 부릅니다.

기존의 자바스크립트의 이러한 문제에서 구제해주는것이 바로 Promise 입니다. 한번 위 코드를 Promise 로 해결해보겠습니다. 추가적으로, 코드를 더 읽기 쉽게 작성하기위해서 화살표 함수도 사용해볼게요.

function printLater(number) {
    return new Promise( // 새 Promise 를 만들어서 리턴함
        resolve => {
            setTimeout( // 1초뒤 실행하도록 설정
                () => {
                    console.log(number);
                    resolve(); // promise 가 끝났음을 알림
                },
                1000
            )
        }
    )
}


printLater(1)
.then(() => printLater(2))
.then(() => printLater(3))
.then(() => printLater(4))
.then(() => printLater(5))
.then(() => printLater(6))

몇번 하던간에 코드의 깊이는 일정합니다. 따라서 콜백지옥에 빠질일이 없겠죠?

Promise 에서는 값을 리턴 하거나, 에러를 발생 시킬 수도 있습니다.

코드를 다음과 같이 입력해보세요.

function printLater(number) {
    return new Promise( // 새 Promise 를 만들어서 리턴함
        (resolve, reject) => { // resolve 와 reject 를 파라미터로 받습니다
            setTimeout( // 1초뒤 실행하도록 설정
                () => {
                    if(number > 5) { return reject('number is greater than 5'); } // reject 는 에러를 발생시킵니다
                    resolve(number+1); // 현재 숫자에 1을 더한 값을 반환합니다
                    console.log(number);
                },
                1000
            )
        }
    )
}

printLater(1)
.then(num => printLater(num))
.then(num => printLater(num))
.then(num => printLater(num))
.then(num => printLater(num))
.then(num => printLater(num))
.then(num => printLater(num))
.then(num => printLater(num))
.catch(e => console.log(e));

결과:

1
2
3
4
5
number is greater than 5

Promise 를 이제 이해했다면, 본격적으로 axios 를 사용하여 웹 요청을 해보도록 하겠습니다.

axios 설치

$ yarn add axios

yarn 을 통하여 axios 를 설치하세요.

axios 사용해보기

먼저 리덕스와 axios 를 함께 사용해보기전에, axios 만 따로 리액트 컴포넌트 사용해보도록 하겠습니다.

App 컴포넌트에서 axios 를 불러오고 componentDidMount 메소드를 다음과 같이 입력해보세요.

src/App.js

import axios from 'axios';
    componentDidMount() {
        axios.get('https://jsonplaceholder.typicode.com/posts/1')
             .then(response => console.log(response.data));
    }

자, 이제 페이지에 들어가서 개발자 도구의 콘솔을 확인해보세요. 뭔가가 프린트 되었나요?

Thunk 를 통하여 웹 요청 해보기

자 이제 지난 섹션에서 배운 redux-thunk 를 사용하여 웹 요청을 해보겠습니다. modules 디렉토리에 post 모듈을 생성하세요.

src/modules/post.js

import { handleActions } from 'redux-actions';

import axios from 'axios';

function getPostAPI(postId) {
    return axios.get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
}

const GET_POST_PENDING = 'GET_POST_PENDING';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_FAILURE = 'GET_POST_FAILURE';

export const getPost = (postId) => dispatch => {
    // 먼저, 요청이 시작했다는것을 알립니다
    dispatch({type: GET_POST_PENDING});

    // 요청을 시작합니다
    // 여기서 만든 promise 를 return 해줘야, 나중에 컴포넌트에서 호출 할 때 getPost().then(...) 을 할 수 있습니다
    return getPostAPI(postId).then(
        (response) => {
            // 요청이 성공했을경우, 서버 응답내용을 payload 로 설정하여 GET_POST_SUCCESS 액션을 디스패치합니다.
            dispatch({
                type: GET_POST_SUCCESS,
                payload: response
            })
        }
    ).catch(error => {
        // 에러가 발생했을 경우, 에로 내용을 payload 로 설정하여 GET_POST_FAILURE 액션을 디스패치합니다.
        dispatch({
            type: GET_POST_FAILURE,
            payload: error
        });
        // error 를 throw 하여, 이 함수가 실행 된 다음에 다시한번 catch 를 할 수 있게 합니다.
        throw(error);
    })

}

const initialState = {
    pending: false,
    error: false,
    data: {
        title: '',
        body: ''
    }
}

export default handleActions({
    [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);

새 모듈을 만들었으니, 리듀서에도 추가해주어야겠죠?

src/modules/index.js

import { combineReducers } from 'redux';
import counter from './counter';
import post from './post';

export default combineReducers({
    counter,
    post
});

이제 곧 컴포넌트로 넘어갈건데요, 그 전에 카운터의 기본 값을 1 로 설정해주세요. 우리가, 이 숫자를 postId 로 사용하여 포스트를 불러올것이기 때문이에요. (postId 가 0인 포스트는 존재하지 않습니다.)

src/modules/counter.js

(...)
export default handleActions({
    [INCREMENT]: (state, action) => state + 1,
    [DECREMENT]: (state, action) => state - 1
}, 1);

컴포넌트에서 액션을 통해 웹 요청 시도하기

App 컴포넌트에서 기존의 axios 를 사용하여 웹요청을 하는 코드를 제거하고, incrementAsync 와 decrementAsync 도 Async 를 지워 이전 상태로 돌려주세요.

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 {

    componentDidMount() {
        // 컴포넌트가 처음 마운트 될 때 현재 number 를 postId 로 사용하여 포스트 내용을 불러옵니다.
        const { number, PostActions } = this.props;
        PostActions.getPost(number);
    }

    componentWillReceiveProps(nextProps) {
        const { PostActions } = this.props;

        // 현재 number 와 새로 받을 number 가 다를 경우에 요청을 시도합니다.
        if(this.props.number !== nextProps.number) {
            PostActions.getPost(nextProps.number)
        }
    }

    render() {
        const { CounterActions, number, post, error, loading } = this.props;

        return (
            <div>
                <p>{number}</p>
                <button onClick={CounterActions.increment}>+</button>
                <button onClick={CounterActions.decrement}>-</button>
                { loading && <h2>로딩중...</h2>}
                { error 
                    ? <h1>에러발생!</h1> 
                    : (
                        <div>
                            <h1>{post.title}</h1>
                            <p>{post.title}</p>
                        </div>
                    )}
            </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);

자, 이제 요청이 제대로 되는지 확인해보세요.

요청 완료 후 / 에러 발생했을때 추가 작업 하기

만약에 여러분이 요청을 완료 후 컴포넌트에서 해야 할 작업이 있거나, 에러가 발생했을때 어떠한 작업을 해야된다면, asyncawait 을 사용하세요. 이 키워드들은 우리가 액션생성자 함수에서 반환한 Promise 를 기다려준답니다.

async await 을 사용하기위해 새 함수를 다음과 같이 만들고 호출하세요.

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 {

    componentDidMount() {
        const { number } = this.props;
        this.getPost(number);

    }

    componentWillReceiveProps(nextProps) {
        if(this.props.number !== nextProps.number) {
            this.getPost(nextProps.number);
        }
    }

    getPost = async (postId) => {
        const { PostActions } = this.props;

        try {
            await PostActions.getPost(postId);
            console.log('요청이 완료 된 다음에 실행됨')
        } catch(e) {
            console.log('에러가 발생!');
        }
    }

    render() {
        const { CounterActions, number, post, error, loading } = this.props;

        return (
            <div>
                <p>{number}</p>
                <button onClick={CounterActions.increment}>+</button>
                <button onClick={CounterActions.decrement}>-</button>
                { loading && <h2>로딩중...</h2>}
                { error 
                    ? <h1>에러발생!</h1> 
                    : (
                        <div>
                            <h1>{post.title}</h1>
                            <p>{post.title}</p>
                        </div>
                    )}
            </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);

async 함수를 만들때는 다음과 같이 합니다:

async function foo() {
    const result = await Promise.resolve('hello') ; // Promise.resolve 는 파라미터로 전달된 값을 바로 반환하는 Promise 를 만듭니다.
    console.log(result); // hello
}
// 혹은
const foo = async () => {
    const result = await Promise.resolve('hello') ; // Promise.resolve 는 파라미터로 전달된 값을 바로 반환하는 Promise 를 만듭니다.
    console.log(result); // hello
}

현재 async await 이 작동하는 이유는 create-react-app 으로 만든 프로젝트에는 babel 의 Async to generator transform 플러그인이 적용되어있기 때문입니다. 만약에 이 플러그인이 설치되어있지 않다면 작동하지 않습니다. 그런 경우에는 이렇게 하면 됩니다:

getPost = (postId) => {
    const { PostActions } = this.props;

    PostActions.getPost(postId).then(
        () => {
            console.log('요청이 완료 된 다음에 실행 됨');
        }
    ).catch((e) => {
        console.log('에러가 발생!');
    })
}

여러분들은 Redux 의 정석대로, 비동기 웹 요청을 하는 방법을 배워보았습니다. 어떤가요? 조금은 복잡해 보이지 않나요? 모든 흐름을 다 이해한다 하더라도, 각 요청마다 액션타입을 3개씩 선언하고, 요청전, 요청완료, 요청실패의 상황에 각각 다른 액션을 디스패치해야된다는건 조금은 귀찮은 작업입니다.

하지만 걱정하지마세요. 이 작업을 간소화 해 줄 미들웨어가 존재합니다!

바로 redux-promise-middleware 인데요, 이 미들웨어는 Promise 를 액션의 payload 로 설정해주면, 자동으로 3가지의 액션을 디스패치해줍니다. 다음 섹션에선 이 미들웨어의 사용법을 배워보도록 하겠습니다.

results matching ""

    No results matching ""