MobX


MobX란?


Simple, scalable state management

https://github.com/mobxjs/mobx


mobx는 redux처럼 state를 관리해주지만, redux와는 다른점이 좀 있는 라이브러리입니다.

위에서 명시하여쓴 간단하고, 확장 가능한 상태 관리 도구입니다.


주요 특징


* 데코레이터(Decorator) 를 적극 활용합니다. -> Decorator에 대해서는 mobx 포스팅을 마친뒤 별도로 포스팅 하여 링크하겠습니다.                                                

* Typescript를 base로 만들어졌기 때문에 typescript에 대한 지원이 좋습니다.

* Redux와는 매우 큰 차이로, 단일 스토어를 강제하지 않습니다!

* 처음 사용이 Redux보다는 쉽습니다.



mobx의 전체적인 동작을 나타낸 그림인데요,

그림에 대한 설명은 예제를 통해 알아보면서 이해할수 있도록 해보겠습니다.


이제 CRA를 이용해 프로젝트를 생성하는것은 익숙해지셨을것입니다.

새로운 프로젝트를 생성해주세요.

그리고 App.tsx는 containers 폴더로 이동하고 index.tsx 파일 생성과,

component 폴더 생성 및 index.tsx 생성까지 해주세요.




매우 간단한 MobX 예제 따라하기


step 1 - state 객체를 정한다. -> 리액트의 state가 아니다.

step 2 - 정한 state 객체에 @observable 데코레이터를 붙인다.

step 3 - state 값을 다루는 함수들을 만든다.

step 4 - state 값이 변경되면 반응하는 컴포넌트를 만든다. -> 이 컴포넌트에 @observer 붙인다

step 5 - 컴포넌트에서 state값을 사용한다.

step 6 - 컴포넌트에서 state값을 변경하는 함수를 사용한다.



Step 1 ~ 2


src폴더에 stores 폴더를 생성해주세요. 그리고 index.tsx 파일과 AgeStore.tsx 파일을 생성해주세요.


src/stores/AgeStore.tsx

import { observable } from 'mobx';

export class AgeStore {
@observable
age: number = 30;

}


우선 기본적으로 클래스를 만들고 그 안에 age 라는 값을 명시합니다.

여기서 감지할 값에 @observable 을 사용해줘야 하는데, 여기서는 age 값에 해주겠습니다.

observable은 mobx 에 있는 기능이므로, mobx를 설치해주세요.


yarn add mobx

혹은 

npm install --save mobx 


자 그런데, 이렇게 해도 아마 에디터에서는 오류를 내며 뭐라뭐라 할것입니다.

왜냐하면 @ observable은 데코레이터인데, typescript에서 데코레이터를 사용하기 위해서는

tsconfig의 설정에서 experimentalDecorators 값을 true로 해주어야 합니다.

하지만 디폴트값이 false로 되있기 때문에 직접 true로 바꿔 줘야 합니다.


tsconfig.json

{
"compilerOptions": {
"outDir": "build/dist",
"module": "commonjs",
"target": "es5",
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"jsx": "react",
"moduleResolution": "node",
"rootDir": "src",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true,
"experimentalDecorators": true // 데코레이터를 사용하기 위해 true로 변경
},
"exclude": [
"node_modules",
"build",
"scripts",
"acceptance-tests",
"webpack",
"jest",
"src/setupTests.ts"
],
"types": [
"typePatches"
]
}


src/stores/index.tsx

import AgeStore from './AgeStore';

export { AgeStore };


Step 3


이제 함수를 생성해 주겠습니다.

기본적으로 state 값을 가져오는 get 함수와 set 함수를 생성하도록 하겠습니다.


import { observable } from 'mobx';

export default class AgeStore {
@observable
private _age: number = 30;

constructor(age: number) {
this._age = age;
}
public getAge(): number {
return this._age;
}
public setAge(age: number): void {
this._age = age;
}

}


Step 4


Age component를 만들겠습니다.


src/components/Age.tsx

import * as React from 'react';

class Age extends React.Component<{}, {}> {
constructor(props: {}) {
super(props);
}
render() {
return (
<div className="Age">
</div>
);
}
}

export default Age;


src/components/index.tsx

import Age from './Age';

export { Age };



Step 5 ~ 6


컴포넌트에서 age값을 사용해보도록 하겠습니다.


import 해주시고

import { AgeStore } from '../stores';


생성해줍니다. ( store라고 생각하시면 됩니다. )

const ageState = new AgeStore(30);


render() {
return (
<div className="Age">
<h1>{ageState.getAge()}</h1>
<button onClick={() => this.addAge()}>한해가 지났다.</button>
</div>
);
}


그리고 값이 증가하는 함수와 버튼까지 추가해 보도록 하겠습니다.


import * as React from 'react';

import { AgeStore } from '../stores';

const ageState = new AgeStore(30);

class Age extends React.Component<{}, {}> {
constructor(props: {}) {
super(props);
this.addAge = this.addAge.bind(this);
}
render() {
return (
<div className="Age">
<h1>{ageState.getAge()}</h1>
<button onClick={() => this.addAge()}>나이증가</button>
</div>
);
}
addAge() {
const age = ageState.getAge();
ageState.setAge(age + 1);
console.log(ageState.getAge());
}
}

export default Age;


이제 실행해보겠습니다.



버튼을 아무리 눌러도 값이 변하는것처럼 보이지는 않습니다.


그런데 제가 위의 코드에 addAge 함수에 console.log를 하나 삽입해 놓은것이 있습니다.


개발자 도구로 로그를 살펴보겠습니다.



값은 증가하는데, 렌더링 적용만 안되었다고 추론할수 있습니다.


이제 @observer 를 적용시켜 보겠습니다.


observer 는 mobx-react 를 설치해야 사용 가능합니다.


yarn add mobx-react

혹은

npm install --save mobx-react


import * as React from 'react';

import { AgeStore } from '../stores';

import { observer } from 'mobx-react';

const ageState = new AgeStore(30);

@observer
class Age extends React.Component<{}, {}> {
constructor(props: {}) {
super(props);
this.addAge = this.addAge.bind(this);
}
render() {
return (
<div className="Age">
<h1>{ageState.getAge()}</h1>
<button onClick={() => this.addAge()}>나이증가</button>
</div>
);
}
addAge() {
const age = ageState.getAge();
ageState.setAge(age + 1);
console.log(ageState.getAge());
}
}

export default Age;


간단하게 observer를 import하고, 사용할 컴포넌트 위에 @observer 를 명시해줍니다.


실행해주세요.



이제 정상적으로 렌더링이 됩니다.


즉, 변화를 감지하고 싶은 대상에는 observable 데코레이터 를 사용하고,

observable된 값 변화에 따라 반응하는 컴포넌트에는 observer 데코레이터를 사용하여

제어하는 간단한 구조입니다.


뭔가 redux보다는 확실히 중간 과정이 편한 느낌입니다.

redux는 액션타입을 지정하고, 액션 크리에이터 함수를 만들고, 리덕스를 만들고, 스토어 생성한다음,..... 많은 과정이 필요하지만, mobx는 그 과정이 정말 최소화된 느낌입니다.


물론 각각의 장단점이 있으리라 생각되지만, 더 공부를 해야 확실하게 생각을 말할수 있을거 같습니다.


이번 포스팅은 간단하게 끝내고, 다음 포스팅도 계속해서 mobx에 대해 포스팅을 하도록 하겠습니다.


감사합니다.



소스코드 주소입니다.


https://github.com/JaroInside/tistory-react-typescript-study/tree/14.mobx-part1



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

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

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

14.mobx-part3  (0) 2017.08.03
14.mobx-part2  (0) 2017.08.01
13. redux - part5  (0) 2017.07.26
13. redux - part4  (0) 2017.07.26
13. redux - part3  (0) 2017.07.25
블로그 이미지

Jaro

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

,

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/

,


이전 포스팅에 이어서 redux의 심화내용에 대해 간단히 포스팅 하겠습니다.


combineReducers


이전 포스팅에서 했던 예제 프로젝트에서는, action 1개 reducer 1개로 매우매우 관리하기 쉬운 프로젝트였습니다.


하지만, 프로젝트의 크기가 커지고, 여러 개발자분들이 동시에 개발을 하게 되어 각각 맡은 부분의 reducer를 만들게 되었다면 어떻게 될까요?

A라는 사람은 age를 관리하는 reducer를, B라는 사람은 imageShow에 관한 reducer를 작성한다고 가정해 보도록 하겠습니다.


이전 포스팅에서 사용한 프로젝트를 열어주세요.


그리고 우선 image를 띄워주는 컴포넌트를 생성해보도록 하겠습니다.

( 그냥 img 태그를 쓰셔도 되지만 복습할겸 하겠습니다. )


이미지는 아무거나 쓰셔도 됩니다. components 폴더에 넣어주세요.


src/components/Image.tsx

import * as React from 'react';

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

interface ImageProps {

}

const Image: React.SFC<ImageProps> = (props) => {
return (
<div className="Image">
<img src={image} className="Test-image" alt="jaro" />
</div>
);
};

export default Image;


src/components/index.tsx

import Image from './Image';

export { Image };


그리고 App.tsx에 적용하여 확인해 보겠습니다.  ( 헤더를 지워버렸습니다! )

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


확인해보겠습니다.



이미지가 제대로 나오는것을 확인할수 있습니다.


이제, 이 이미지를 보이거나 혹은 안보이게 하는 action을 만들고 reducer를 작성해 보겠습니다.


src/action/actionType.tsx

// age 관련
export const ADD_AGE = 'ADD_AGE';

// image 관련
export const SHOW_HIDE_IMAGE = 'SHOW_HIDE_IMAGE';

그리고 image.tsx 파일을 생성하여 작성하겠습니다.


src/action/image.tsx

import * as types from './actionType';

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

이제 리듀서 차례입니다.


src/reducer/image.tsx 파일을 생성하고 리듀서를 작성해주세요.

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

type State = boolean;

const initialState: State = true;

type Action = {
type: string;
};

export function image(state: State = initialState, action: Action): State {
switch (action.type) {
case types.SHOW_HIDE_IMAGE:
return !state;
default:
return state;
}
}


이전에 ageApp 리듀서를 만들었을때와는 조금 다른 방식입니다.


typescript를 사용하면서 좀더 type을 명확하게 사용하기 위해 type을 사용해 보았습니다. ( 안하셔도 됩니다. )


그런데 ageApp 리듀서와 다른 또한가지가 눈에 보입니다.


이전에 ageApp 리듀서 코드를 보겠습니다.


export function ageApp(state: { age: number; } = { age: 30 }, action: { type: string; }): { age: number } {
switch (action.type) {
case types.ADD_AGE:
return { age: state.age + 1 };
default:
return state;
}
}


이전에 리듀서에서는 state에 age라는 프로퍼티를 명시하여 사용하였는데, image에서는 그냥 state값을 변화시키고 있습니다.


사실 리듀서를 쪼개서 작업하는것은 처음부터 하나의 리듀서를 생각하고 쪼개서 작업을 해야 합니다. 따라서 합쳐진 하나의 리듀서를 쪼갯을때 각각의 리듀서들의 state들이 곧 프로퍼티가 된다고 생각하셔야 합니다. 


예를들어 위에서 image 라는 리듀서의 state는 각각의 리듀서들이 합쳐졌을때,

state.image로써 역할을 한다는 의미입니다.


그렇다면, ageApp.tsx 파일도 바꿔야겠네요.

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

type State = number;

const initialState: State = 30;

type Action = {
type: string;
};

export function age(state: State = initialState, action: Action): State {
switch (action.type) {
case types.ADD_AGE:
return state + 1;
default:
return state;
}
}


이렇게 바꿔주세요. 이렇게 바꾸면 나중에 바꾸었을때, state.age로 쓸수 있을것입니다.


그럼 이제 이 두개의 리듀서를 합쳐보겠습니다. 우선 combineReducer를 쓰지 않고 해보겠습니다.


ageApp.tsx의 내용과 image.tsx의 내용을 combine.tsx 파일로 합쳐보겠습니다.


src/reducer/index.tsx 파일을 생성해주세요.

import { age } from './age';
import { image } from './image';

type CombinedState = {
age: number;
image: boolean;
};

const initialCombinedState: CombinedState = {
age: 30,
image: true
};

type Action = {
type: string;
};

export function combine(state: CombinedState = initialCombinedState, action: Action): CombinedState {
return {
age: age(state.age, action),
image: image(state.image, action)
};
}


2개의 리듀서를 가져와서 combine 하는 작업입니다.

함수를 생성하고, state와 action을 넣어주면서 return 값으로 각 리듀서들을 넣어주면 됩니다.

여기서 이제 state의 프로퍼티가 생성됩니다.


이제 src/index.tsx에 가서 createStore에 reducer를 변경해주세요.


import { combine } from './reducer';


const store = createStore<{ age: number; image: boolean; }>(combine);


이전 코드와 좀 바뀐것은 좌측에 Store와 제네릭이 빠졌는데요, 우측의 createStore의 제네릭과 동일하기때문에 빼도 상관이 없습니다.


이제 image component에 redux를 연결하여 사용해 보겠습니다.

( 복습입니다 )


src/components/image.tsx


컨테이너를 만들고


import { connect } from 'react-redux';


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

export default ImageContainer;


함수를 구현하고

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

const mapDispatchToProps = (dispatch: Function) => {
return {
onShowClick: (): void => {
dispatch(imageShowHide());
}
};
};


인터페이스를 작성하고

interface ImageProps {
image: boolean;
onShowClick(): void;
}


적용합니다.

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


그리고 실행하여 이미지 버튼을 눌러주세요.


버튼을 누를때마다 props.image 값이 true 혹은 false로 바뀌면서 이미지가 생겼다가 사라졌다가를 할것입니다.


그렇다면 이번에는 combineReducers 를 사용하여 해보겠습니다.


src/reducer/index.tsx

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

type CombinedState = {
age: number;
image: boolean;
};

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


생성할 state의 type을 지정하는것 말고는 그냥 쓰면 바로 작동이 됩니다.

아주 편리하네요.


실행시켜 확인하면, 제대로 동작할것입니다.


이번 포스팅에서는 redux의 심화과정(?)중 하나인 combineReducer를 해보았습니다.


원래 목표는 async Action과 middleware, redux-thunk도 같이 포스팅 하려 했으나


아직 좀더 공부가 필요한것 같아 빠르게 이해한뒤 바로 포스팅 하겠습니다.


소스코드입니다.


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



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

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

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

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

Jaro

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

,