React with TypeScript 4th

2woongjae@gmail.com

MobX

Redux  32282   VS   9893  MobX

2017.07

MobX 주요 특징

  • 데코레이터를 적극 활용한다.
    • experimentalDecoratorstrue 로 설정.
    • 스토어 객체에 붙이는 데코레이터가 있고, => @observable
    • 컴포넌트에서 사용하는 데코레이터가 있다. => @observer
  • TypeScript 가 Base 인 라이브러리이다.
    • 당연히 @types/mobx, @types/mobx-react 는 필요가 없음.
  • Redux 와 마찬가지로, 스토어에 필요한 부분과 리액트에 필요한 부분이 있다.
    • yarn add mobx
    • yarn add mobx-react
  • 리덕스와 다르게 단일 스토어를 강제하진 않는다.
    • DI 없는 Angular Service ???
  • ​처음 사용이 무척 쉽다.

MobX

후다닥 만들어 보기 단계 - 나이 먹는 앱

  • step.1 state 객체를 정한다. => 리액트의 state 가 아니다.
    • 그냥 객체거나, 프리미티브 값일수도 있다.
  • step.2 정한 state 객체에 @observable 데코레이터를 붙인다.
  • step.3 state 값을 다루는 함수들을 만든다.
    • get / set 함수
    • 혹은 액션과 같은 역할을 하는 함수
    • 이뮤터블이 아니다 ! => 뮤테이트 하는것이다 !
  • step.4 state 값에 변경되면 반응하는 컴포넌트를 만든다.
    • 그리고 그 컴포넌트에 @observer 데코레이터를 붙인다.
  • step.5 컴포넌트에서 state 값을 사용한다.
  • step.6 컴포넌트에서 state 값을 변경하는 함수를 사용한다.

step 1 ~ 3

import {observable} from 'mobx';

export class AgeState {
    @observable private _age = 35;
    constructor(age: number) {
        this._age = age;
    }
    public getAge(): number {
        return this._age;
    }
    public setAge(age: number): void {
        this._age = age;
    }
    public addAge(): void {
        this._age = this._age + 1;
    }
}

step 4 ~ 6

import * as React from 'react';
import './App.css';
import {AgeState} from './AgeState';
import {observer} from 'mobx-react';

const logo = require('./logo.svg');

const ageState = new AgeState(35);

@observer class App extends React.Component<{}, {}> {
  constructor(props: {}) {
    super(props);

    this.addAge = this.addAge.bind(this);
  }
  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">
          {ageState.getAge()}
          <button onClick={() => this.addAge()}>한해가 지났다.</button>
        </p>
      </div>
    );
  }
  addAge() {
    const age = ageState.getAge();
    ageState.setAge(age + 1);
    // ageState.addAge();
  }
}

export default App;

https://github.com/2woongjae/mobx-ts-basic

  • mobx 에서 말하는 state 는 리액트 컴포넌트의 state 와 다른 것
  • (리덕스보다) 쉽지 않습니까 ?
    • 리액티브 프로그래밍 > 함수형 프로그래밍
    • 간편한 비동기 처리 => 넘나 쉬워요
  • 데코레이터의 적극적인 사용으로 인해, 깔끔한 구성
  • 하지만,
    • 단일 스토어가 아니다.
      • 스토어를 어떻게 사용지에 대한 적합한 해결을 모색해야 할 것
      • 결국 최상위 스토어를 만들고, props 로 공유해가는 방식으로
      • https://github.com/gothinkster/react-mobx-realworld-example-app
    • 라이프사이클에 대한 고민

mobx-react-devtools

mobx-react-devtools

  • 이것은 컴포넌트입니다.
  • yarn add mobx-react-devtools -D
  • import DevTools from 'mobx-react-devtools';
  • <DevTools />
  • https://github.com/mobxjs/mobx-react-devtools

mobx-react-devtools

@observable (by mobx)

observable 사용법 - 2가지 방식

  • observable(<value>)
    • 데코레이터 없이 사용하는 방식
    • @ 없이, 함수처럼 사용해서 리턴한 객체를 사용
  • @observable <클래스의 프로퍼티>
    • 데코레이터로 사용하는 법
    • 클래스 내부에 프로퍼티 앞에 붙여서 사용
    • 한 클래스 안에 여러개의 @observable 존재
    • 타입스크립트는 이 방식으로 하는게 당연하겠죠!

observable 사용법 - 2가지 방식

import { observable } from 'mobx';

const list = observable([1, 2, 4]);

const isLogin = observable(true);

const age = observable({
    age: 35
});

class AgeState {
    @observable
    private _age = 35;
}

const ageState = new AgeState();

@observer (by mobx-react)

observer 사용법 - 2가지 방식

  • observer(<컴포넌트>);
    • 데코레이터 없이 사용하는 방식
    • SFC 에 붙여서 사용
    • 어차피 리액트의 state 를 사용하지 않으면 SFC 로 해도 관계가 없다.
  • <컴포넌트 클래스> 에 @observer 달아서 처리
    • 리액트의 라이프사이클이 아니라, mobx 의 라이프 사이클
      • componentWillReact
      • componentWillUpdate
      • componentDidUpdate

observer 사용법 - 2가지 방식

const StatelessApp = observer(() => {
  function addAge(): void {
    ageState.addAge();
  }
  return (
    <div>
      {ageState.getAge()}
      <button onClick={() => addAge()}>한해가 지났다.</button>
    </div>
  );
});

@observer
class App extends React.Component<{}, {}> {
  constructor(props: {}) {
    super(props);
    this.addAge = this.addAge.bind(this);
  }
  render() {
    return (
      <div>
        {ageState.getAge()}
        <button onClick={() => this.addAge()}>한해가 지났다.</button>
      </div>
    );
  }
  addAge() {
    ageState.addAge();
  }
}

@computed  (by mobx)

computed 란 ?

  • getter 에만 붙일수 있다. (setter 부르면 getter 도 실행된다.)
  • 함수가 아니라 리액티브 하다는 것에 주목
  • 실제 컴포넌트에서 사용하는 (게터)값들에 달아서 사용하면 최소 범위로 변경할 수 있기 때문에 유용하다.
    • 40살이 넘었을때만 나이를 올리면 40살 이하일때는 재랜더링 대상이 아닌 것과 같은 경우
    • 내부적으로 고도의 최적화 => 어떻게 ?
      • 매번 재계산을 하지 않는다
      • 계산에 사용할 observable 값이 변경되지 않으면 재실행하지 않음.
      • 다른 computed 또는 reaction 에 의해 호출되지 않으면 재실행하지 않음.
      • observable 과 처리 방식의 차이로 인한 성능 이슈에 주목
        • observable 이 변했는데 computed 가 변하지 않을때 사용에 따른 차이

computed 사용법 - 2가지 방식

  • computed(<함수>);
    • 데코레이터 없이 사용하는 방식
    • 별 의미가 없다.
  • <클래스의 getter 메서드> 에 @computed 달아서 처리

computed 사용법 - 2가지 방식

class AgeState {
    constructor() {
        extendObservable(this, {
            _age: 35,
            age: computed(function() {
                return (this._age > 40) ? this._age : 0;
            })
        })
    }
}

class AgeState {
    @observable private _age: number = 35;
    @computed
    get age(): number {
        return (this._age > 40) ? this._age : 0;
    }
}

@action  (by mobx)

action - MobX 2.2

  • <action> wraps your original function automatically in a transaction. This means that the out-of-the-box performance of MobX will be even better.
  • <actions> integrate very well with the mobx-react-devtools. This means that you can trace any mutation back to the causing action with the devtools or debugger.
  • <actions> are always untracked. Which means that you cannot turn them into reactions accidentally by calling them from a reaction. While most people won’t run into this issue,
    • action makes the distinction between actions (something that modifies the state) and reactions (the state needs to trigger a side effect automatically) very clear.
  • there is strict mode. <Actions> are an opt-in concept. You may use an action to clearly express the intent of your code, getting transactions for free; but you are in no way obliged to do so. All existing code will continue working as is. Unless strict mode is enabled using ‘useStrict(true)’.
    • In strict mode, MobX will throw an exception on any attempt to modify state outside an action.

action 이란 ?

  • state 를 수정하는 함수
    • 어디에 state 를 수정하는 함수가 있는지 마킹
    • untracked, transaction 및 allowStateChanges로 래핑 한 후 리턴
  • 평소엔 옵셔널
    • useStrict 모드를 쓰면 필수
    • useStrict 모드에서 observable 을 변경하는 함수가, action 을 마킹하지 않으면 런타임 에러
  • computed 의 setter 는 액션이다.

useStrict(true);

action 사용법

ref.child('todos').on('value', action((snapshot: firebase.database.DataSnapshot) => {
  if (snapshot) {
    const list = snapshot.val();
    const todos = [];
    if (list !== null) {
      for (const key of Object.keys(list)) {
        todos.push({
          id: key,
          text: list[key]
        });
      }
    }
    this.todos = todos;
  }
}));

@action addTodo = (text: string) => {
  const ref = db.ref();
  ref.child('todos').push().set(text);
}

@inject 와 Provider

Provider

  • 네, 그 프로바이더가 맞습니다.
    • 네, 그래서 컨테이너라는 개념을 사용해도 좋습니다.
  • 프로바이더에 props 로 넣고, @inject 로 꺼내 쓴다고 생각하시면 됩니다.
    • 상당히 명시적이고, 편합니다.
    • 컨테이너를 쓰지 않아도 될것 같습니다.
      • props 로 바꿔줍니다.
      • this.props.store as IAgeState; => 중요한 부분

사용법

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

@inject('store')
@observer
class App extends React.Component<{ store?: IAgeState; }, {}> {
  render() {
    const store = this.props.store as IAgeState;
    return (
      <div className="App">
        <DevTools />
        <p className="App-intro">
          {store.age}
          <button onClick={() => store.addAge()}>한해가 지났다.</button>
          <button onClick={() => store.addAgeAsync()}>깃헙 비동기 호출</button>
        </p>
      </div>
    );
  }
}

autorun

간단 todoApp

todoApp 만들기 (1)

  • create-react-app mobx-ts-todo --scripts-version=react-scripts-ts
  • 폴더 만들기
    • stores, components
  • 스토어 만들기
    • yarn add mobx
    • experimentalDecorators 설정
    • todoStore.tsx 만들기 => export default new TodoStore();
  • 프로바이더 설정
    • yarn add mobx-react
    • <App /> 감싸기

todoStore.tsx 최초

import { observable } from 'mobx';

export class TodoStore {
  @observable todos = [];
}

export default new TodoStore();

index.tsx 에 Provider 추가

import {Provider} from 'mobx-react';
import todoStore from './stores/todoStore';

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

todoApp 만들기 (2)

  • TodoInput 컴포넌트 만들기
    • input 태그에 ref 사용.
    • addTodo(text: string): void;
  • TodoList 컴포넌트 만들기
    • todos: { id: string; text: string; }[];
    • deleteTodo(id: string): void;

TodoInput.tsx - dumb

import * as React from 'react';
import { observer } from 'mobx-react';

interface TodoInputProps {
    addTodo(text: string): void;
}

@observer
class TodoInput extends React.Component<TodoInputProps, {}> {
  private _input: HTMLInputElement;
  render() {
    return (
      <div>
        <input type="text" ref={ref => this._input = ref as HTMLInputElement} />
        <button onClick={() => this._addTodo()}>저장</button>
      </div>
    );
  }
  _addTodo = () => {
    const input = this._input;
    if (input.value !== '') {
        this.props.addTodo(input.value);
        input.value = '';
    }
  }
}

export default TodoInput;

TodoList.tsx - dumb

import * as React from 'react';
import { observer } from 'mobx-react';

interface TodoListProps {
    todos: { id: string; text: string; }[];
    deleteTodo(id: string): void;
}

@observer
class TodoInput extends React.Component<TodoListProps, {}> {
  render() {
    const list = this.props.todos.map(todo => (
        <li key={todo.id}>
            {todo.text}
            <button onClick={() => this.props.deleteTodo(todo.id)}>삭제</button>
        </li>
    ));
    return (
      <div>
        <ul>{list}</ul>
      </div>
    );
  }
}

export default TodoInput;

todoApp 만들기 (3)

  • Firebase 연결 및 설정
    • 보안 규칙 설정
    • yarn add firebase (not @types/firebase)
  • todoStore 작성
    • firebase 연결부
    • 리액트 컴포넌트에서 사용하는 action 작성
    • firebase 가 적용되는 포인트에 action 처리
      • 트랜잭션
  • useStrict(true); 처리

파이어베이스 디비 보안규칙 변경

{
  "rules": {
    ".read": true,
    ".write": true
  }
}

todoStore.tsx

import { observable, action } from 'mobx';
import * as firebase from 'firebase';

const config = {
    apiKey: 'AIzaSyD4Ywl6RRaFrSy8ZXL10hsSl6orA2PF5hc',
    authDomain: 'mobx-ts-todo.firebaseapp.com',
    databaseURL: 'https://mobx-ts-todo.firebaseio.com',
    projectId: 'mobx-ts-todo',
    storageBucket: 'mobx-ts-todo.appspot.com',
    messagingSenderId: '900175359555'
};
firebase.initializeApp(config);
const db: firebase.database.Database = firebase.database();

export class TodoStore {
  @observable todos: { id: string; text: string; }[] = [];
  constructor() {
    const ref = db.ref();
    ref.child('todos').on('value', action((snapshot: firebase.database.DataSnapshot) => {
      if (snapshot) {
        const list = snapshot.val();
        const todos = [];
        if (list !== null) {
          for (const key of Object.keys(list)) {
            todos.push({
              id: key,
              text: list[key]
            });
          }
        }
        this.todos = todos;
      }
    }));
  }
  @action addTodo = (text: string) => {
    const ref = db.ref();
    ref.child('todos').push().set(text);
  }
  @action deleteTodo = (id: string) => {
    const ref = db.ref();
    ref.child('todos').child(id).remove();
  }
}

export default new TodoStore();

todoApp 만들기 (4)

  • https://github.com/2woongjae/mobx-ts-todo

React with TypeScript (4) - mobx

By Woongjae Lee

React with TypeScript (4) - mobx

타입스크립트 한국 유저 그룹 리액트 스터디 201706

  • 2,142