Redux with TypeScript

2woongjae@gmail.com

Mark Lee (이웅재)

  • Studio XID inc. ProtoPie Engineer
  • Seoul.JS 오거나이저
  • 타입스크립트 한국 유저 그룹 오거나이저
  • 일렉트론 한국 유저 그룹 운영진
  • Seoul.js 오거나이저
  • Microsoft MVP - Visual Studio and Development Technologies
  • Code Busking with Jimmy
    • https://www.youtube.com/channel/UCrKE8ihOKHxYHBzI0Ys-Oow

Quick Start - Redux

redux, react-redux

~/Project/workshop-201801 
➜ npx create-react-app redux-ts-quick-start --scripts-version=react-scripts-ts

~/Project/workshop-201801 took 1m 21s 
➜ cd redux-ts-quick-start

Project/workshop-201801/redux-ts-quick-start is 📦 v0.1.0 via ⬢ v8.9.4 
➜ npm i redux -D
+ redux@3.7.2
added 3 packages in 8.795s

Project/workshop-201801/redux-ts-quick-start is 📦 v0.1.0 via ⬢ v8.9.4 took 9s 
➜ npm i react-redux @types/react-redux -D
+ react-redux@5.0.6
+ @types/react-redux@5.0.14
added 3 packages in 8.63s

Project/workshop-201801/redux-ts-quick-start is 📦 v0.1.0 via ⬢ v8.9.4 took 9s 
➜ npm i react-router-dom @types/react-router-dom -D
+ react-router-dom@4.2.2
+ @types/react-router-dom@4.2.3
added 9 packages in 10.163s

index.tsx

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import './index.css';

import App from './App';
import registerServiceWorker from './registerServiceWorker';

import reducer, { State } from './reducers';
import { createStore } from 'redux';
import { Provider } from 'react-redux';

const store = createStore<State>(reducer);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root') as HTMLElement
);
registerServiceWorker();

reducers/index.tsx

import { combineReducers } from 'redux';
import { builds, Builds } from './builds';

export type State = Builds;

const reducer = combineReducers<State>({
  builds
});

export default reducer;

reducers/builds.tsx

import { BuildActions } from '../actions';
import * as types from '../constants';

export interface Build {
  text: string;
  completed: boolean;
  id: number;
}

const initialState: Build[] = [];

export interface Builds {
  builds: Build[];
}

export function builds(state: Build[] = initialState, action: BuildActions) {
  switch (action.type) {
    case types.ADD_BUILD:
      return [
        ...state,
        {
          id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
          completed: false,
          text: action.text
        }
      ];

    case types.DELETE_BUILD:
      return state.filter(todo => todo.id !== action.id);

    default:
      return state;
  }
}

actions/index.tsx

import * as types from '../constants';

export interface AddBuild {
  type: types.ADD_BUILD;
  text: string;
}

export interface DeleteBuild {
  type: types.DELETE_BUILD;
  id: number;
}

export type BuildActions = AddBuild | DeleteBuild;

export const addBuild = (text: string): AddBuild => ({
  type: types.ADD_BUILD,
  text
});
export const deleteBuild = (id: number): DeleteBuild => ({
  type: types.DELETE_BUILD,
  id
});

constants/index.tsx

export const ADD_BUILD = 'ADD_BUILD';
export type ADD_BUILD = typeof ADD_BUILD;

export const DELETE_BUILD = 'DELETE_BUILD';
export type DELETE_BUILD = typeof DELETE_BUILD;

App.tsx

import * as React from 'react';
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom';
import BuildContainer from './containers/BuildContainer';

const Routes = () => (
  <ul>
    <li>
      <Link to="/">Home</Link>
    </li>
    <li>
      <Link to="/build">Build</Link>
    </li>
    <li>
      <Link to="/version">Version</Link>
    </li>
  </ul>
);

const App = () => (
  <Router>
    <div>
      <Routes />
      <Switch>
        <Route exact={true} path="/" render={() => <h2>Version</h2>} />
        <Route path="/build" component={BuildContainer} />
        <Route path="/version" render={() => <h2>Version</h2>} />
        <Route render={() => <h2>404</h2>} />
      </Switch>
    </div>
  </Router>
);

export default App;

containers/BuildContainer.tsx

import * as React from 'react';
import { bindActionCreators, AnyAction } from 'redux';
import { connect, Dispatch } from 'react-redux';
import * as BuildActions from '../actions';
import { Builds } from '../reducers/builds';
import { State } from '../reducers/index';
import Build from '../components/Build';

interface BuildProps {}

type BuildStateProps = Builds;

interface BuildDispatchProps {
  actions: {
    addBuild: (text: string) => AnyAction;
    deleteBuild: (id: number) => AnyAction;
  };
}

const Container: React.SFC<
  BuildProps & BuildStateProps & BuildDispatchProps
> = ({ builds, actions }) => (
  <Build
    builds={builds}
    addBuild={actions.addBuild}
    deleteBuild={actions.deleteBuild}
  />
);

const mapStateToProps = (state: State): BuildStateProps => ({
  builds: state.builds
});

const mapDispatchToProps = (
  dispatch: Dispatch<AnyAction>
): BuildDispatchProps => ({
  actions: bindActionCreators(BuildActions, dispatch)
});

const BuildContainer = connect(mapStateToProps, mapDispatchToProps)(Container);

export default BuildContainer;

components/Build.tsx

import * as React from 'react';
import { AnyAction } from 'redux';
import { Build } from '../reducers/builds';
import BuildAdd from './BuildAdd';
import BuildCard from './BuildCard';

interface BuildProps {
  builds: Build[];
  addBuild: (text: string) => AnyAction;
  deleteBuild: (id: number) => AnyAction;
}

const Build: React.SFC<BuildProps> = props => (
  <div>
    <BuildAdd addBuild={props.addBuild} />
    <div>
      {props.builds.map(build => (
        <BuildCard
          key={build.id}
          id={build.id}
          text={build.text}
          delete={props.deleteBuild}
        />
      ))}
    </div>
  </div>
);

export default Build;

components/BuildAdd.tsx

import * as React from 'react';
import { AnyAction } from 'redux';

interface BuildAddProps {
  addBuild: (text: string) => AnyAction;
}

const BuildAdd: React.SFC<BuildAddProps> = props => {
  let input: HTMLInputElement;
  function addBuild() {
    props.addBuild(input.value);
    input.value = '';
  }
  return (
    <div>
      <input
        type="text"
        ref={ref => {
          input = ref as HTMLInputElement;
        }}
      />
      <button onClick={addBuild}>추가</button>
    </div>
  );
};

export default BuildAdd;

components/BuildCard.tsx

import * as React from 'react';

interface BuildCardProps {
  id: number;
  text: string;
  delete: Function;
}

const BuildCard: React.SFC<BuildCardProps> = props => (
  <div>
    <h2>build</h2>
    <p>{props.text}</p>
    <button onClick={() => props.delete(props.id)}>delete</button>
  </div>
);

export default BuildCard;

Redux 개요

React Funamentals - Remind

React Funamentals - Remind

Redux

Redux

단일 스토어를 만들고, 사용하는 법을 익히는 시간

  • 단일 스토어다!
  • [만들기] 단일 스토어 사용 준비하기
    • import redux
    • 액션을 정의하고,
    • 액션을 사용하는, 리듀서를 만들고,
    • 리듀서들을 합친다.
    • 최종 합쳐진 리듀서를 인자로, 단일 스토어를 만든다.
  • [사용하기] 준비한 스토어를 리액트 컴포넌트에서 사용하기
    • import react-redux
    • connect 함수를 이용해서 컴포넌트에 연결

Action - 액션

리덕스의 액션이란 ?

  • 액션은 사실 그냥 객체입니다.
    • { type: 'TEST' } // payload 없는 액션
    • { type: 'TEST', params: 'hello' } // payload 있는 액션
    • type 만이 필수 프로퍼티이며, type 은 string 타입입니다.
      • { type: 'TEST' }
  • 액션을 생성하는 함수를 '액션 크리에이터 = 액션 생성자' 라고 합니다.
    • 함수를 통해 액션을 생성해서, 액션 객체를 리턴해줍니다.
      • createTest('hello');
        • { type: 'TEST', params: 'hello' }
  • 이 액션 오브젝트를 만들어내서 스토어에게 보낼 예정입니다.
  • 스토어에 보내는 일종의 인풋이라 생각하면 됩니다.

액션을 준비하기 위해서는 ?

  • 액션의 타입을 정의하여 변수로 빼는 단계
    • 왜욤 ?
      • 강제는 아니죵
      • 그러므로 안해도 됩니다.
      • 그냥 타입을 문자열로 넣기에는 실수를 잡아내지 못하지만,
      • 미리 정의한 변수를 사용하면, 헬퍼도 동작을 하고 스펠링에 주의를 덜 기울여도 됩니다.
      • 하지만 미리 정의한 타입 변수를 문자열로 넣을때는 주의를 기울여야겠지요
  • 액션 객체를 만들어 내는 함수를 만드는 단계
    • 하나의 액션 객체를 만들기 위한 하나의 함수를 매칭하여 만들어냅니다.
    • 액션의 타입은 미리 정의한 타입으로 부터 가져와서 사용하며, 사용자가 인자로 주지 않습니다. (??)

액션 준비 코드

// 파일 분리를 하지 않을때
// ADD_TODO => addTodo

// 액션의 type 정의
const ADD_TODO = 'ADD_TODO';


// 액션 생산자
// 액션의 타입은 미리 정의한 타입으로 부터 가져와서 사용하며,
// 사용자가 인자로 주지 않습니다.
export function addTodo(text) {
  return { type: ADD_TODO, text }; // { type: ADD_TODO, text: text }
}

Reducers - 리듀서

리듀서

  • 액션을 주면, 그 액션이 적용되어 달라진(안달라질수도...) 결과를 만들어 줌.
  • 그냥 함수
    • Pure Function
    • immutable
      • 왜용 ?
        • 리듀서를 통해 스테이트가 달라졌음을 리턱스가 인지하는 방식
  • 액션을 받아서 스테이트를 리턴하는 구조
    • function todoApp(state, action) { 
        return state;
      }

리듀서 만들기

function todos(state = [], action) {
  switch (action.type) {
  case ADD_TODO:
    return [...state, {
      text: action.text,
      completed: false
    }];
  case COMPLETE_TODO:
    return [
      ...state.slice(0, action.index),
      Object.assign({}, state[action.index], {
        completed: true
      }),
      ...state.slice(action.index + 1)
    ];
  default:
    return state;
  }
}

리듀서에서 모든 액션이 다 처리 가능해야 하지만,

  • 원래 리듀서를 겁나 크게 만들고, 변경되는 지점에 맞춰 쪼개는 것이 코드를 관리하기 좋다라는 결론으로
  • 결론을 알기 때문에 변경지점이 같이 애들끼지 리듀서를 분할해서 만들어 놓고, 합치는 방법을 사용
  • state = {todos: [], filter: '어쩌구'} 라고 할때,
    • todos 만 변경하는 액션들을 처리하는 A 라는 리듀서 함수를 만들고,
    • filter 만을  변경하는 액션들을 처리하는 B 라는 리듀서 함수를 만들고,
    • A 와 B 를 합침.
  • 코드 관리의 차원이죠잉

한번에 다하는 리듀서

function todoApp(state = initialState, action) {
  switch (action.type) {
  case SET_VISIBILITY_FILTER:
    return Object.assign({}, state, {
      visibilityFilter: action.filter
    });
  case ADD_TODO:
    return Object.assign({}, state, {
      todos: [...state.todos, {
        text: action.text,
        completed: false
      }]
    });
  case COMPLETE_TODO:
    return Object.assign({}, state, {
      todos: [
        ...state.todos.slice(0, action.index),
        Object.assign({}, state.todos[action.index], {
          completed: true
        }),
        ...state.todos.slice(action.index + 1)
      ]
    });
  default:
    return state;
  }
}

영역별로 분리한 리듀서

function todos(state = [], action) {
  switch (action.type) {
  case ADD_TODO:
    return [...state, {
      text: action.text,
      completed: false
    }];
  case COMPLETE_TODO:
    return [
      ...state.slice(0, action.index),
      Object.assign({}, state[action.index], {
        completed: true
      }),
      ...state.slice(action.index + 1)
    ];
  default:
    return state;
  }
}

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
  case SET_VISIBILITY_FILTER:
    return action.filter;
  default:
    return state;
  }
}

combineReducers

리듀서들 2개를 합친 최종 리듀서 함수

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  };
}

리덕스에서 제공하는 combineReducers 사용

import { combineReducers } from 'redux';

const todoApp = combineReducers({
  visibilityFilter,
  todos
});

createStore(리듀서);

const store = createStore(리듀서, 초기값);

  • createStore
    • <S>(reducer: Reducer<S>, preloadedState: S, enhancer?: StoreEnhancer<S>): Store<S>;
  • store
    • Store<S>

const store = createStore(리듀서);

  • store
    • Store<S>
  • store.getState();
    • getState(): S;

  • store.dispatch(액션);
    • dispatch: Dispatch<S>;

  • store.subscribe(리스너);
    • subscribe(listener: () => void): Unsubscribe;

    • 리턴이 Unsubscribe 라는 점 !

  • store.replaceReducer(다른리듀서);
    • replaceReducer(nextReducer: Reducer<S>): void;

Redux 를 React 에 연결

(react-redux 안쓰기)

후다닥 스토어 만들기까지

  • step1. 액션 타입 만들기 : ADD_AGE
  • step2. 액션 생성 함수 만들기 : addAge()
  • step3. 리듀서 만들기 : ageApp(state, action)
  • step4. 스토어 만들기 : createStore
  • step5. createStore 를 위해 redux 추가
    • npm i redux -D

후다닥 스토어 만들기

// npm i redux -D
import {createStore} from 'redux';

// 타입 정의
const ADD_AGE = 'ADD_AGE';

// 타입 생성 함수
function addAge(): {type: string;} {
  return {
    type: ADD_AGE
  };
}

// 리듀서
function ageApp(state: {age: number;} = {age: 35}, action: {type: string;}): {age: number} {
  if (action.type === ADD_AGE) {
    return {age: state.age + 1};
  }
  return state;
}

// 스토어 만들기
const store = createStore<{age: number;}>(ageApp);

단일 store 를 만들어서 props 로 하위 컴포넌트로 전달

  • store 를 다루는 명시적인 방법
  • index.tsx
    • createStore(리듀서, 초기값) 을 통해 스토어 객체를 리턴 받음.
    • 그 store 객체를 하위 컴포넌트인 App 컴포넌트에 props 로 전달
  • App.tsx
    • props 로 받은 store 를 다음 하위 컴포넌트에 보내거나,
    • 컴포넌트 내부에서 사용
      • componentDidMount
        • subscribe
      • componentWillUnmount
        • unsubscribe

props 로 보내려면

// index.tsx 에서 랜더링 다시 하기
function render() {
  ReactDOM.render(
    <App store={store} />,
    document.getElementById('root') as HTMLElement
  );
}

store.subscribe(render);
render();

// App.tsx 에서 랜더링 다시 하기
ReactDOM.render(
  <App store={store} />,
  document.getElementById('root') as HTMLElement
);

props 를 받아봅시당

import {Store, Unsubscribe} from 'redux';
import {addAge} from './index';

interface AppProps {
  store: Store<{ age: number; }>;
}

class App extends React.Component<AppProps, {}> {
  private _unsubscribe: Unsubscribe;
  constructor(props: AppProps) {
    super(props);

    this._addAge = this._addAge.bind(this);
  }
  componentDidMount() {
    const store = this.props.store;
    this._unsubscribe = store.subscribe(() => {
      this.forceUpdate();
    });
  }
  componentWillUnmount() {
    if (this._unsubscribe !== null) {
      this._unsubscribe();
    }
  }
  render() {
    const state = this.props.store.getState();
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p className="App-intro">
          나이가 {state.age}
          <button onClick={this._addAge}>한해가 지났다.</button>
        </p>
      </div>
    );
  }
  private _addAge(): void {
    const store = this.props.store;
    const action = addAge();
    store.dispatch(action);
  }
}

단일 store 를 만들어서 context 로 하위 컴포넌트로 전달

  • store 를 다루는 암시적인 방법
  • index.tsx
    • Provider 라는 컴포넌트에 props 로 store 를 받고,
    • getChildContext 로 store 에 props.store  를 지정
  • App.tsx
    • context 에서 store 를 추출하여 사용
    • 컴포넌트 내부에서 사용
      • componentDidMount
        • subscribe
      • componentWillUnmount
        • unsubscribe

Provider 컴포넌트 만들기

import * as PropTypes from 'prop-types';

// Provider 만들기
class Provider extends React.Component<{ store: Store<{ age: number; }>; children: JSX.Element; }, {}> {
  public static childContextTypes = {
    store: PropTypes.object // React.PropTypes.object
  };
  getChildContext() {
    return {
      store: this.props.store
    }; 
  }
  render() {
    return this.props.children;
  }
}

Provider 로 루트 컴포넌트 감싸기

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

하위(루트) 컴포넌트에서 context 사용하기

import * as PropTypes from 'prop-types';

class App extends React.Component<{}, {}> {
  // App.contextTypes 에 스토어를 받아오도록 정의해야 합니다.
  public static contextTypes = {
    store: PropTypes.object
  };

  private _unsubscribe: Unsubscribe;
  constructor(props: {}) {
    super(props);

    this._addAge = this._addAge.bind(this);
  }
  componentDidMount() {
    const store = this.context.store;
    this._unsubscribe = store.subscribe(() => {
      this.forceUpdate();
    });
  }
  componentWillUnmount() {
    if (this._unsubscribe !== null) {
      this._unsubscribe();
    }
  }
  render() {
    const state = this.context.store.getState();
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p className="App-intro">
          나이가 {state.age}
          <button onClick={this._addAge}>한해가 지났다.</button>
        </p>
      </div>
    );
  }
  private _addAge(): void {
    const store = this.context.store;
    const action = addAge();
    store.dispatch(action);
  }
}

그 하위 컴포넌트에서 context 사용하기

import * as React from 'react';
import {Unsubscribe} from 'redux';
import {addAge} from './index';
import * as PropTypes from 'prop-types';

class Button extends React.Component<{}, {}> {
  // Button.contextTypes 에 스토어를 받아오도록 정의해야 합니다.
  public static contextTypes = {
    store: PropTypes.object
  };

  private _unsubscribe: Unsubscribe;
  constructor(props: {}) {
    super(props);

    this._addAge = this._addAge.bind(this);
  }
  componentDidMount() {
    const store = this.context.store;
    this._unsubscribe = store.subscribe(() => {
      this.forceUpdate();
    });
  }
  componentWillUnmount() {
    if (this._unsubscribe !== null) {
      this._unsubscribe();
    }
  }
  render() {
    return <button onClick={this._addAge}>하위 컴포넌트에서 한해가 지났다.</button>;
  }
  private _addAge(): void {
    const store = this.context.store;
    const action = addAge();
    store.dispatch(action);
  }
}

export default Button;

Redux 를 React 에 연결

(react-redux 쓰기)

react-redux

  • 앞서 만든 Provider 컴포넌트를 제공해줍니다.
  • connect 함수를 통해 컨테이너를 만들어줍니다.
    • 컨테이너는 스토어의 statedispatch(액션) 를 연결한 컴포넌트에 props 로 넣어주는 역할을 합니다.​
    • 그렇다면 필요한 것은 ?
      • 어떤 state 를 어떤 props 에 연결할 것인지에 대한 정의
      • 어떤 dispatch(액션) 을 어떤 props 에 연결할 것인지에 대한 정의
      • 그 props 를 보낼 컴포넌트를 정의

Provide from react-redux

// npm i redux -D
import {createStore} from 'redux';
// npm i react-redux @types/react-redux -D
import {Provider} from 'react-redux';

const store = createStore<{ age: number; }>(ageApp);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

// 나머지 코드 그대로 두고도 작동합니다

connect from react-redux

import * as ReactRedux from 'react-redux';
const { connect } = ReactRedux;

const AppContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(App);
// 1. mapStateToProps - 어떤 state 를 어떤 props 에 연결할 것인지에 대한 정의
// 2. mapDispatchToProps - 어떤 dispatch(action) 을 어떤 props 에 연결할 것인지에 대한 정의
// 3. App - 그 props 를 보낼 컴포넌트를 정의

export default AppContainer;

mapStateToProps, mapDispatchToProps


// 이 함수는 store.getState() 한 state 를
// 연결한(connect) App 컴포넌트의 어떤 props 로 줄 것인지를 리턴
// 그래서 이 함수의 리턴이 곧 App 컴포넌트의 AppProps 의 부분집합이어야 한다.
const mapStateToProps = (state: { age: number; }) => {
  return {
    age: state.age,
  };
};

// 이 함수는 store.dispatch(액션)을
// 연결한(connect) App 컴포넌트의 어떤 props 로 줄 것인지를 리턴
// 그래서 이 함수의 리턴이 곧 App 컴포넌트의 AppProps 의 부분집합이어야 한다.
const mapDispatchToProps = (dispatch: Function) => {
  return {
    onAddClick: () => {
      dispatch(addAge());
    }
  };
};

// mapStateToProps 와 mapDispatchToProps 의 리턴을 합치면 나오는 형태로 지정
interface AppProps {
  age: number;
  onAddClick(): void;
}

App 컴포넌트 변경

/*
class App extends React.Component<AppProps, {}> {
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p className="App-intro">
          나이가 {this.props.age}
          <button onClick={this.props.onAddClick}>한해가 지났다.</button>
          <Button />
        </p>
      </div>
    );
  }
}
*/

const App: React.SFC<AppProps> = (props) => {
  return (
    <div className="App">
      <div className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <h2>Welcome to React</h2>
      </div>
      <p className="App-intro">
        나이가 {props.age}
        <button onClick={props.onAddClick}>한해가 지났다.</button>
        <Button />
      </p>
    </div>
  );
};

그래서 컨테이너 라는 아이가 등장한 것

  • Smart 컴포넌트 (영민?)
    • 리덕스에 연결 => 컨테이너
    • 주로 mapStateToProps 만 사용한다.
    Dumb 컴포넌트 (우직?)
    • 리덕스에 연결없이 props 로만 사용
  • Smart 에서 리덕스에 연결하는 props 를 만들고, Dumb 에서 하위로 내려 사용하는 것...

Server-Side-Rendering with Redux

SSR - express

app.get('*', (req, res) => {
    const html = path.join(__dirname, '../build/index.html');
    const htmlData = fs.readFileSync(html).toString();

    const store = createStore(ageApp);

    const ReactApp = ReactDOMServer.renderToString(
        <Provider store={store}>
            <AppContainer />
        </Provider>
    );

    const initialState = store.getState();
    
    const renderedHtml = htmlData.replace(`<div id="root">{{SSR}}</div>`, `<div id="root">${ReactApp}</div><script>window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};</script>`);
    res.status(200).send(renderedHtml);
});

HTML

  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root">${html}</div>
    <script>window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};</script>
  </body>

CSR - 서버에서 스토어 받아오기

const initialState = (window as any).__INITIAL_STATE__;

// 서버에서 받은 초기값으로 스토어 만들기
const store = createStore<{ age: number; }>(ageApp, initialState);

React.render(
  <Provider store={store}>
    <AppContainer />
  </Provider>,
  document.getElementById('root')
);

index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
        <meta name="theme-color" content="#000000">
        <link rel="manifest" href="/manifest.json">
        <link rel="shortcut icon" href="/favicon.ico">
        <title>React App</title>
        <link href="/static/css/main.cacbacc7.css" rel="stylesheet">
    </head>
    <body>
        <noscript>You need to enable JavaScript to run this app.</noscript>
        <div id="root">
            <div class="App" data-reactroot="" data-reactid="1" data-react-checksum="1528495119">
                <div class="App-header" data-reactid="2">
                    <img src="/logo.svg" class="App-logo" alt="logo" data-reactid="3"/>
                    <h2 data-reactid="4">Welcome to React</h2>
                </div>
                <p class="App-intro" data-reactid="5">
                    <!-- react-text: 6 -->나이가 
                    <!-- /react-text -->
                    <!-- react-text: 7 -->35
                    <!-- /react-text -->
                    <button data-reactid="8">한해가 지났다.</button>
                    <button data-reactid="9">하위 컴포넌트에서 한해가 지났다.</button>
                </p>
            </div>
        </div>
        <script>window.__INITIAL_STATE__ = {"age":35};</script>
        <script type="text/javascript" src="/static/js/main.ccc68f1a.js"></script>
    </body>
</html>

여기까지는 기본, 앞으로는 심화 내용입니다.

  • async Action with Redux
  • redux 미들웨어
  • redux-thunk
  • redux-saga

async Action with Redux

비동기 작업을 어디서 하느냐 ? 가 젤 중요

  • 액션을 분리합니다.
    • Start
    • Success
    • Fail
    • ... 등등
  • dispatch 를 할때 해줍니다.
    • 당연히 리듀서는 동기적인 것 => Pure

해봅시다. 비동기 처리를 위한 액션 추가

// 타입 정의
export const START_GITHUB_API = 'START_GITHUB_API';
export const ERROR_GITHUB_API = 'ERROR_GITHUB_API';
export const END_GITHUB_API = 'END_GITHUB_API';

// 타입 생성 함수
export function startGithubApi(): { type: string; } {
  return {
    type: ADD_AGE
  };
}
export function errorGithubApi(): { type: string; } {
  return {
    type: ADD_AGE
  };
}
export function endGithubApi(age: number): { type: string; age: number; } {
  return {
    type: ADD_AGE,
    age
  };
}

mapDispatchToProps => dispatch

// @types/react-redux
// connect 함수를 mapStateToProps 함수 하나만 인자로 쓰면 DispatchProp 가 넘어옵니다.
export declare function connect<TStateProps, no_dispatch, TOwnProps>(
    mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps>
): ComponentDecorator<DispatchProp<any> & TStateProps, TOwnProps>;

// 수정
const { connect } = ReactRedux;

const mapStateToProps = (state: { age: number; }) => {
  return {
    age: state.age
  };
};

const AppContainer = connect(mapStateToProps)(App);

잠깐 ! Dispatch<S> ?

// @types/react-redux
type Dispatch<S> = Redux.Dispatch<S>;

// @types/react-redux
export interface DispatchProp<S> {
  dispatch: Dispatch<S>;
}

// redux/index.d.ts
export interface Dispatch<S> {
    <A extends Action>(action: A): A;
}

App 컴포넌트에서 props.dispatch 를 쓸수 있도록 수정

const App: React.SFC<AppProps & ReactRedux.DispatchProp<{}>> = (props) => {
  function getCountFromGithub(): void {
    // 여기를 구현
  }
  return (
    <div className="App">
      <div className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <h2>Welcome to React</h2>
      </div>
      <p className="App-intro">
        나이가 {props.age}
        <button onClick={() => props.dispatch(addAge())}>한해가 지났다.</button>
        <button onClick={() => getCountFromGithub()}>깃헙 API 비동기 호출</button>
      </p>
    </div>
  );
};

Promise

function getCountFromGithub(): void {
  const dispatch: ReactRedux.Dispatch<any> = props.dispatch;
  dispatch(startGithubApi());
  request.get('https://api.github.com/users')
    .end((err, res) => {
      if (err) {
        return dispatch(errorGithubApi());
      }
      const age = JSON.parse(res.text).length;
      return dispatch(endGithubApi(age));
    });
}

async - await

async function getCountFromGithub(): Promise<void> {
  const dispatch: ReactRedux.Dispatch<{}> = props.dispatch;
  dispatch(startGithubApi());
  let res = null;
  try {
    res = await request.get('https://api.github.com/users');
  } catch (e) {
    dispatch(errorGithubApi());
    return;
  }
  const age = JSON.parse(res.text).length;
  dispatch(endGithubApi(age));
  return;
}

오로지 된다는 것을 보여주기 위한 리듀서 추가

import {ADD_AGE, START_GITHUB_API, ERROR_GITHUB_API, END_GITHUB_API} from '../action';

export function ageApp(state: { age: number; } = {age: 35}, action: { type: string; age: number; }): { age: number; } {
  if (action.type === ADD_AGE) {
    return {age: state.age + 1};
  } else if (action.type === START_GITHUB_API) {
    return {age: 0};
  } else if (action.type === ERROR_GITHUB_API) {
    return {age: 35};
  } else if (action.type === END_GITHUB_API) {
    return {age: action.age};
  }
  return state;
}

Redux 미들웨어

리덕스 미들웨어

  • 미들웨어가 디스패치의 앞뒤에 코드를 추가할수 있게 해줍니다.
  • 미들웨어가 여러개면 미들웨어가 순차적으로 실행됩니다.
  • 두 단계가 있습니다.
    • 스토어를 만들때 미들웨어를 설정하는 부분
      • createStore, applyMiddleware from redux
    • 디스패치가 호출될때 실제로 미들웨어를 통과하는 부분
      • function middleware(store) {

            return (next: any) => (action: any) => { 

                // 다음 미들웨어 호출, 없으면 dispatch

                const returnValue = next(action);  

                return returnValue;

            };

        }

applyMiddleware(함수1, 함수2, ...)

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

// 스토어를 만들때 순서대로 넣어주면, 순서대로 미들웨어 실행
const store = createStore<{ age: number; }>(ageApp, applyMiddleware(middlewareA, middlewareB));

middleware 함수 한개

function middleware(store: Store<{ age: number; }>) {
    return (next: any) => (action: any) => {
        console.log(`before store : ${JSON.stringify(store.getState())}`); // before

        const returnValue = next(action); // 다음 미들웨어 호출, 없으면 실제 dispatch

        console.log(`after : ${JSON.stringify(store.getState())}`); // after

        return returnValue;
    };
}

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

// before store : {"age":35}
// after : {"age":36}

middleware 함수 두개

export function middlewareA(store: Store<{ age: number; }>) {
    return (next: any) => (action: any) => {
        console.log(`A before store : ${JSON.stringify(store.getState())}`); // before

        const returnValue = next(action); // 다음 미들웨어 호출, 없으면 실제 dispatch

        console.log(`A after : ${JSON.stringify(store.getState())}`); // after

        return returnValue;
    };
}

export function middlewareB(store: Store<{ age: number; }>) {
    return (next: any) => (action: any) => {
        console.log(`B before store : ${JSON.stringify(store.getState())}`); // before

        const returnValue = next(action); // 다음 미들웨어 호출, 없으면 실제 dispatch

        console.log(`B after : ${JSON.stringify(store.getState())}`); // after

        return returnValue;
    };
}

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

// A before store : {"age":35}
// B before store : {"age":35}
// B after : {"age":36}
// A after : {"age":36}

redux-thunk

redux-thunk

  • 미들웨어
  • 리덕스 만든 사람이 만들었음.
  • 리덕스에서 비동기 처리를 위함.
  • yarn add redux-thunk (@types/redux-thunk 필요없음)
  • 액션 생성자를 활용하여 비동기 처리
    • 액션 생성자가 액션을 리턴하지 않고, 함수를 리턴함.

import * as thunk from 'redux-thunk';

// import
import thunk from 'redux-thunk';

// 미들웨어 설정
const store = createStore<{ age: number; }>(ageApp, applyMiddleware(middleware, thunk));

비동기 액션 만들기

export function addAge(): { type: string; } {
  return {
    type: ADD_AGE
  };
}

export function addAgeAsync() {
  return (dispatch: any) => {
    setTimeout(() => {
      dispatch(addAge());
    }, 1000);
  };
}

// 사용
const App: React.SFC<AppProps & ReactRedux.DispatchProp<{}>> = (props) => {
  return (
    <div className="App">
      <div className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <h2>Welcome to React</h2>
      </div>
      <p className="App-intro">
        나이가 {props.age}
        <button onClick={() => props.dispatch(addAgeAsync())}>한해가 지났다.</button>
      </p>
    </div>
  );
};

redux-saga

[CODEBUSKING WORKSHOP] Redux with TypeScript

By Woongjae Lee

[CODEBUSKING WORKSHOP] Redux with TypeScript

코드버스킹 워크샵 - React with TypeScript 세번째 (2018년 1월 버전)

  • 1,216