async Action


이전 예제에서는 Action이 동기적으로 실행이 되어도 상관이 없었습니다.


하지만 만약 비동기적으로 처리되는 action이 필요하다면?


예를 들면 Server의 API를 Get 해오는 작업입니다.


API 를 호출하고, 성공하면 A라는 작업을 하고, 실패했을경우, 실패한 이유에 따라 또 다른 작업을 하고.... 여러 과정들이 있을것이라고 생각됩니다.


저는 github API를 이용하여 특정 값을 get 하는 예제를 만들어보겠습니다.


우선 action 을 만들어 보겠습니다.


src/action/actionType.tsx

// async 관련
export const START_GITHUB_API = 'START_GITHUB_API';
export const ERROR_GITHUB_API = 'ERROR_GITHUB_API';
export const END_GITHUB_API = 'END_GITHUB_API';


src/action/async.tsx

import * as types from './actionType';

export function startGithubApi(): { type: string; } {
return {
type: types.START_GITHUB_API
};
}
export function errorGithubApi(): { type: string; } {
return {
type: types.ERROR_GITHUB_API
};
}
export function endGithubApi(name: string): { type: string; name: string; } {
return {
type: types.END_GITHUB_API,
name
};
}


action 생성 함수인데, endGithubApi를 보면, name이라는 값을 받도록 되어있습니다.


여기서의 name은 payload로, api로 받아온 값중 특정 값을 저장하기 위해 사용합니다.


이제 리듀서를 작성해 보겠습니다.


src/reducer/async.tsx

import * as types from '../action/actionType';

type State = {
status: string;
name: string;
};

const initialState: State = {
status: 'INIT',
name: 'Jaro'
};

type Action = {
type: string;
name: string;
};

export function async(state: State = initialState, action: Action): State {
switch (action.type) {
case types.START_GITHUB_API:
return {
...state,
status: 'START'
};
case types.END_GITHUB_API:
return {
...state,
status: 'END',
name: action.name
};
case types.ERROR_GITHUB_API:
return {
...state,
status: 'ERROR'
};
default:
return state;
}
}


async action에서는 payload를 사용하므로, 이번에는 state가 status와 name 을 갖도록 하였습니다. 여기서 status는 api 상태를 표시할것이고, name에는 받아온 값을 저장할 것입니다.


이제 combine 해보겠습니다.


src/reducer/index.tsx

import { combineReducers } from 'redux';
import { age } from './age';
import { image } from './image';
import { async } from './async';

type CombinedState = {
age: number;
image: boolean;
async: { status: string; name: string; };
};

export const combine = combineReducers<CombinedState>({
age: age,
image: image,
async: async
});


async리듀서를 import 하고, combineReducer에 넣을 제네릭의 타입을 지정해줍니다.

그리고 combine해주면 리듀서까지 완성입니다.


이제 만들어진 스토어에서 값을 가져와서 렌더링 하는 컴포넌트를 작성해보겠습니다.


src/components/Async.tsx 파일을 생성하고 작성해주세요.

import * as React from 'react';
import { connect } from 'react-redux';

interface AsyncProps {
status: string;
name: string;
onAPIClick(): void;
}

const Async: React.SFC<AsyncProps> = (props) => {
return (
<div className="Async">
</div>
);
};

const AsyncContainer = connect(
mapStateToProps
mapDispatchToProps
)(Async);

export default AsyncContainer;

우선 기본적인 형태부터 잡고 시작하겠습니다.


src/components/index.tsx

import Image from './Image';
import Async from './Async';

export { Image, Async };


mapStateToProps 작성입니다.

const mapStateToProps = (state: { async: { status: string; name: string; }}) => {
return {
status: state.async.status,
name: state.async.name
};
};


그리고 mapDispatchToProps를 작성해야 합니다.


그 전에 모듈을 하나 추가하도록 하겠습니다.


react에는 기본적으로 HTTP Client를 내장하고 있지 않기 때문에 AJAX를 사용하기 위해서는 Javscript내장객체인 XMLRequest를 사용하거나 따로 모듈을 설치하여야 합니다.


여러 라이브러리 들이 있지만, 이번포스팅에서는 superagent 를 사용해 보도록 하겠습니다.


yarn add superagent @types/superagent

혹은

npm install --s superagent @types/superagent


설치하고, import 해주세요.


import * as request from 'superagent';


그리고 mapDispatchToProps 를 작성해 보도록 하겠습니다.

const mapDispatchToProps = (dispatch: Function) => {
return {
onAPIClick: (): void => {
dispatch(startGithubApi());
request.get('https://api.github.com/users')
.end((err, res) => {
if (err) {
return dispatch(errorGithubApi());
}
const name = res.body[0].login;
return dispatch(endGithubApi(name));
});
}
};
};


우선 start action을 먼저 dispatch한다음, api get을 하고 err가 나오면 error를 dispatch,

성공하면 res.body의 첫번째 객체의 login 값을 payload로 받아 dispatch 하겠습니다.


이렇게 하면 이제 값들을 표시하고 버튼을 만들 차례입니다.

return (
<div className="Async">
<h1>{props.status}</h1>
<h2>{props.name}</h2>
<button onClick={props.onAPIClick}>깃헙 API 비동기 호출</button>
</div>
);


이제 src/contatiners/App.tsx 에 작성한 컴포넌트를 적용하고

import { Image, Async } from '../components';


const App: React.SFC<AppProps> = (props) => {
return (
<div className="App">
<Image />
<h1>{props.age}</h1>
<button onClick={props.onAddClick}>증가합니다.</button>
<Async />
</div>
);
};


실행해 보겠습니다.



초기 화면에서는 초기값인 INIT 과 Jaro 가 렌더링 되고 있습니다.

여기서 버튼을 누르면,


시작 action을 dispatch 보내므로 START 가 표시되고, 비동기 작업이 끝나면,


 END 가 표시되면서 받아온 값이 표시됩니다.


이번엔 mapDispatchToProps 코드를 async-await 를 사용해서 작성해보겠습니다.

( 타입스크립트에서는 async-await 를 매우 잘 지원합니다 )

const mapDispatchToProps = (dispatch: Function) => {
return {
onAPIClick: async (): Promise<void> => {
dispatch(startGithubApi());
let res = null;
try {
res = await request.get('https://api.github.com/users');
} catch (e) {
dispatch(errorGithubApi());
return;
}
const name = res.body[0].login;
dispatch(endGithubApi(name));
return;
}
};
};


실행한 결과는 동일합니다.



Redux MiddleWare


리덕스 미들웨어는 dispatch를 호출하였을때, action의 전 후에 다른 작업을 할수 있게 해줍니다.


미들웨어가 여러개라면 순차적으로 실행되게 됩니다.


* 미들웨어는 액션을 보내는 순간부터 스토어에 도착하는 순간까지 사이에 서드파티 확장을 사용할 수 있는 지점을 제공합니다


미들웨어를 등록하는 부분은 createStore를 하는 부분입니다.


예를들어 

import {middlewareA, middlewareB} from './Middleware';


const store = createStore<{ age: number; }>(ageApp, applyMiddleware(middlewareA, middlewareB));


이러한 모습으로 createStore에 미들웨어 A 와 B를 등록할수 있습니다.


예제로 한번 해보도록 하겠습니다.


src폴더에 middleware 폴더를 생성하고, index.tsx 파일을 생성해주세요.

( 파일을 더 나누지 않고 index.tsx 파일에 미들웨어 함수를 구성하겠습니다.)


import { Middleware, MiddlewareAPI, Dispatch, Action } from 'redux';

export const middleware: Middleware =
(api: MiddlewareAPI<void>) =>
(next: Dispatch<void>) =>
<A extends Action>(action: A) => {
console.log(`첫번째 미들웨어 before Value - ${JSON.stringify(api.getState())}`);
const returnValue = next(action);
console.log(`첫번째 미들웨어 after Value - ${JSON.stringify(api.getState())}`);
return returnValue;
};


여기서는 action이 발생하면, action이전의 state를 로그로 보여주고, 액션이 실행되고, 변경된 state를 로그로 보여주는 미들웨어를 구성하였습니다.


* 이해를 도와주신 최준영님께 감사를 ㅠㅠ *


여기서 Middleware 의 Definition을 살펴보면,


export interface Middleware {
<S>(api: MiddlewareAPI<S>): (next: Dispatch<S>) => Dispatch<S>;
}


이렇게 되어있고, MiddlewareAPI는 


export interface MiddlewareAPI<S> {
dispatch: Dispatch<S>;
getState(): S;
}


이렇게 정의되어있는것을 확인할수 있습니다.


여기서 next가 바로 미들웨어가 여러개일경우 다음 미들웨어를 호출하고, 만약 다음 미들웨어가 없다면, 실제 dispatch를 호출하게 됩니다.


적용해 보겠습니다.


createStore를 하는 src/index.tsx 파일을 열어주세요.


applyMiddleware를 import 해주세요.


import { createStore, applyMiddleware } from 'redux';


작성한 middleware도 import 해주세요.


import { middleware } from './middleware';


그리고 createStore를 할때 middleware를 삽입합니다.


const store = createStore<StoreTypes>(combine, applyMiddleware(middleware));


이제 실행하고 개발자 콘솔을 열어 확인해주세요.




버튼을 누를때 ( action을 할때 ) 마다 미들웨어를 통해 로그가 나오는것을 확인할수 있습니다.


이것을 이용하면 실제 프로덕트에서 어떠한 일이 일어날때마다 로그를 수집하는 등의 작업을 할수 있습니다.


이번에는 2개의 미들웨어를 적용해보겠습니다.


src/middleware/index.tsx 에 미들웨어 하나를 더 추가하겠습니다.


import { Middleware, MiddlewareAPI, Dispatch, Action } from 'redux';

export const middleware: Middleware =
(api: MiddlewareAPI<void>) =>
(next: Dispatch<void>) =>
<A extends Action>(action: A) => {
console.log(`첫번째 미들웨어 before Value - ${JSON.stringify(api.getState())}`);
const returnValue = next(action);
console.log(`첫번째 미들웨어 after Value - ${JSON.stringify(api.getState())}`);
return returnValue;
};

export const middlewareB: Middleware =
(api: MiddlewareAPI<void>) =>
(next: Dispatch<void>) =>
<A extends Action>(action: A) => {
console.log(`두번째 미들웨어 before Value - ${JSON.stringify(api.getState())}`);
const returnValue = next(action);
console.log(`두번째 미들웨어 after Value - ${JSON.stringify(api.getState())}`);
return returnValue;
};


이제 createStore에 적용하면 됩니다.


import { middleware, middlewareB } from './middleware';


const store = createStore<StoreTypes>(combine, applyMiddleware(middleware, middlewareB));


실행해보겠습니다.



로그에 보이듯, 첫번째 미들웨어가 시작하고 두번째 미들웨어가 시작, 끝나고 나서 첫번째 미들웨어가 끝나는것 ( 실제 dispatch가 이루어지는것 ) 을 확인할수 있습니다.



redux-thunk


redux-thunk는 리덕스에서 비동기 처리를 위해 만든 미들웨어입니다.

리덕스를 만든 분이 만들었습니다!


redux-thunk를 간단하게 설명하자면, 객체 대신 함수를 생성하는 액션 생성함수를 작성 할 수 있게 해줍니다. 즉 액션 생성자를 활용하여 비동기 처리를 할수 있습니다. -> 액션 생성자가 액션을 리턴하지 않고 함수를 리턴 -> 내부에서 여러 작업이 가능 ( 네트워크 요청이나, 또다른 액션을 여러번 디스패치 하는것도 가능함)


설명으로는 뭔가 이해하기 어려울거 같습니다.


빠르게 예제로 살펴보겠습니다.


우선 redux-thunk를 설치해주세요.


yarn add redux-thunk

혹은

npm install --save redux-thunk


( @types/redux-thunk 는 안하셔도 됩니다. )


그리고 src/index.tsx에 thunk를 import하고, applyMiddleware에 추가해주세요.


import thunk from 'redux-thunk';


const store = createStore<StoreTypes>(combine, applyMiddleware(middleware, middlewareB, thunk));


저는 사진을 토글하는 버튼을 비동기적으로 처리하려는 예제를 만들것입니다.


우선 image component를 조금 수정하도록 하겠습니다.


import * as React from 'react';
import * as ReactRedux from 'react-redux';
import { imageShowHide } from '../action/image';

const image = require('./test.jpeg');

interface ImageProps {
image: boolean;
}

const Image: React.SFC<ImageProps & ReactRedux.DispatchProp<{}>> = (props) => {
return (
<div className="Image">
{ props.image ?
<img src={image} className="Test-image" alt="jaro" /> :
null }
<br/>
<button onClick={() => props.dispatch(imageShowHide())}>이미지 버튼</button>
</div>
);
};

const mapStateToProps = (state: { image: boolean; }) => {
return {
image: state.image,
};
};

const { connect } = ReactRedux;

const ImageContainer = connect(
mapStateToProps,
)(Image);

export default ImageContainer;


connect 할때 mapDispatchToProps를 삭제하였습니다.


이렇게 쓰게 되면 props에 dispatch가 자동으로 넘어오게 됩니다.


그래서 컴포넌트가 Props를 받아오는 곳에서 

ImageProps & ReactRedux.DispatchProp<{}>

의 형태로 dispatch를 받아옵니다.


이렇게 이전 방법과의 차이점은,  정해진 props로 디스패치할것을 전부 보내는 방식이라는것과,

그냥 모든 디스패치를 다 쓸수 있다는 차이가 있습니다.


이제 비동기적인 액션 생성자 함수를 만들어 보겠습니다.


src/action/image.tsx 에 작성하도록 하겠습니다.


import * as types from './actionType';
import { Dispatch } from 'redux';

export function imageShowHide(): { type: string; } {
return {
type: types.SHOW_HIDE_IMAGE
};
}

export function delayImage() {
return (dispatch: Dispatch<void>) => {
setTimeout(
() => {
dispatch(imageShowHide());
},
1000
);
};
}


이제 delayImage 함수를 호출하면, 디스패치를 리턴하면서 1초뒤에 dispatch가 이루어 질것입니다.

( setTimeout을 저렇게 쓴 이유는 lint 때문입니다... 그냥 편하신대로 하셔도 됩니다 )


그럼 다시 image component로 가서 사용해보겠습니다.


imageShowHide 대신 delayImage를 import 하고, 버튼 클릭시 delayImage가 dispatch되게 해주세요.

import { delayImage } from '../action/image';


const Image: React.SFC<ImageProps & ReactRedux.DispatchProp<{}>> = (props) => {
return (
<div className="Image">
{ props.image ?
<img src={image} className="Test-image" alt="jaro" /> :
null }
<br/>
<button onClick={() => props.dispatch(delayImage())}>이미지 버튼</button>
</div>
);
};


그리고 실행하여 이미지를 토글하는 버튼을 누르면 비동기적으로 실행되는것을 확인할수 있습니다.


이번 포스팅은 리덕스에 대한 포스팅이었습니다.


이해하는데 오랜 시간이 걸려 빠르게 포스팅을 업로드 하지 못하였는데, 쉽게 설명하였는지는 의문입니다.


앞으로 남은 내용 역시 열심히 포스팅 하도록 노력하겠습니다.


소스코드 주소입니다.


https://github.com/JaroInside/tistory-react-typescript-study/tree/13.redux-part5



참고 슬라이드 - http://slides.com/woongjae/react-with-typescript-3#/

참고 동영상 - https://www.youtube.com/playlist?list=PLV6pYUAZ-ZoHx0OjUduzaFSZ4_cUqXLm0



'React > React&typeScript' 카테고리의 다른 글

14.mobx-part2  (0) 2017.08.01
14.mobx-part1  (5) 2017.07.28
13. redux - part4  (0) 2017.07.26
13. redux - part3  (0) 2017.07.25
13. redux - part2  (0) 2017.07.24
블로그 이미지

Jaro

대한민국 , 인천 , 남자 , 기혼 , 개발자 jaro0116@gmail.com , https://github.com/JaroInside https://www.linkedin.com/in/seong-eon-park-16a97b113/

,