MobX 2021

  • https://slides.com/woongjae/mobx2021
  • https://github.com/xid-mark/mobx2021

Lead Software Engineer @ProtoPie

Microsoft MVP

TypeScript Korea User Group Organizer

Marktube (Youtube)

Mark Lee

이 웅재

MobX

1) What is MobX ??

2) 프로젝트에 Decorator 설정하기

3) @observable (by mobx)

4) @observer (by mobx-react)

5) @computed  (by mobx)

6) @action  (by mobx)

7) @inject 와 Provider

8) mobx-devtools

9) stores

10) Asynchronous actions

What is MobX ??

Redux  32282   VS   9893  MobX

2017.07

Redux  37557   VS   12957  MobX

2018.01

MobX 주요 특징

  • 데코레이터를 적극 활용한다.

    • cra 에 데코레이터를 사용하는 법...

    • 스토어 객체에 붙이는 데코레이터가 있고, => @observable

    • 컴포넌트에서 사용하는 데코레이터가 있다. => @observer

  • TypeScript 가 Base 인 라이브러리이다

  • Redux 와 마찬가지로, 스토어에 필요한 부분과 리액트에 필요한 부분이 있다.

    • npm i mobx -D

    • npm i mobx-react -D

  • 리덕스와 다르게 단일 스토어를 강제하진 않는다.​

  • (리덕스보다) 쉽지 않습니까 ?
    • 리액티브 프로그래밍 > 함수형 프로그래밍
    • 간편한 비동기 처리 => 넘나 쉬워요
  • 데코레이터의 적극적인 사용으로 인해, 깔끔한 구성

  • 하지만, 단일 스토어가 아니다.

    • https://github.com/gothinkster/react-mobx-realworld-example-app

    • 결국 최상위 스토어를 만들고, props 로 공유해가는 방식으로

    • 스토어를 어떻게 사용지에 대한 적합한 해결을 모색해야 할 것

프로젝트에 Decorator 설정하기

npm i customize-cra react-app-rewired -D

config-overrides.js

const { override, addDecoratorsLegacy } = require('customize-cra');

module.exports = override(addDecoratorsLegacy());

jsconfig.json

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

mobx/BookStore.js

import { observable, action, makeObservable } from 'mobx';

class BookStore {
  @observable
  books = [];

  @observable
  loading = false;

  @observable
  error = null;

  constructor() {
    makeObservable(this);
  }

  @action
  start = () => {
    this.loading = true;
    this.books = [];
    this.error = null;
  };

  @action
  success = (books) => {
    this.loading = false;
    this.books = books;
    this.error = null;
  };

  @action
  fail = (error) => {
    this.loading = false;
    this.books = [];
    this.error = error;
  };
}

export default BookStore;

App.js

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

const bookStore = new BookStore();

function App() {
  return (
    <ErrorBoundary FallbackComponent={Error}>
      <Provider bookStore={bookStore}>
        <ConnectedRouter history={history}>
          <Switch>
            <Route path="/add" component={Add} />
            <Route path="/signin" component={Signin} />
            <Route path="/" exact component={Home} />
            <Route component={NotFound} />
          </Switch>
        </ConnectedRouter>
      </Provider>
    </ErrorBoundary>
  );
}

export default App;

MobxListContainer.jsx

import React from 'react';
import BookList from '../components/BookList';
import { inject, observer } from 'mobx-react';
import BookService from '../services/BookService';

@inject('bookStore')
@observer
class BookListContainer extends React.Component {
  render() {
    const { books, loading, error } = this.props.bookStore;

    return (
      <BookList
        books={books}
        loading={loading}
        error={error}
        getBooks={this.getBooks}
      />
    );
  }

  getBooks = async () => {
    const { bookStore } = this.props;
    const token = localStorage.getItem('token');
    try {
      bookStore.start();
      const books = await BookService.getBooks(token);
      bookStore.success(books);
    } catch (error) {
      bookStore.fail(error);
    }
  };
}

export default BookListContainer;

MobxListContainer.jsx

export default inject('bookStore')(
  observer(function BookListContainer({ bookStore }) {
    const { books, loading, error } = bookStore;

    const getBooks = useCallback(async () => {
      try {
        bookStore.start();
        const token = localStorage.getItem('token');
        const books = await BookService.getBooks(token);
        bookStore.success(books);
      } catch (error) {
        bookStore.fail(error);
      }
    }, [bookStore]);

    return (
      <BookList
        books={books}
        loading={loading}
        error={error}
        getBooks={getBooks}
      />
    );
  }),
);

@observable (by mobx)

observable 사용법 - 2가지 방식

  • observable(<value>)

    • 데코레이터 없이 사용하는 방식

    • @ 없이, 함수처럼 사용해서 리턴한 객체를 사용

  • @observable <클래스의 프로퍼티>

    • 데코레이터로 사용하는 법

    • 클래스 내부에 프로퍼티 앞에 붙여서 사용

    • 한 클래스 안에 여러개의 @observable 존재

observable 사용법 - 2가지 방식

import { observable } from 'mobx';

// array 에 사용
const list = observable([1, 2, 4]);

// boolean 에 사용
const isLogin = observable(true);

// literal 객체에 사용
const age = observable({
    age: 35
});

// 클래스의 멤버 변수에 데코레이터로 사용
class AgeStore {
    @observable
    private _age = 35;
}

const ageStore = new AgeStore();

@observer (by mobx-react)

observer 사용법 - 2가지 방식

  • observer(<컴포넌트>);

    • 데코레이터 없이 사용하는 방식

    • 함수 컴포넌트에 사용

  • <컴포넌트 클래스> 에 @observer 달아서 처리

    • ​클래스 컴포넌트에 사용

@computed  (by mobx)

computed 란

  • computed(내부에서 observable 을 사용하는 함수);

    • 데코레이터 없이 사용하는 방식

  • <observable 클래스> 의 getter 에 @computed 달아서 처리

    • ​스토어에 사용

    • getter 에만 붙일수 있다.

  • 함수가 아니라 리액티브 하다는 것에 주목

  • 실제 컴포넌트에서 사용하는 (게터)값들에 달아서 사용하면 최소 범위로 변경할 수 있기 때문에 유용하다.

    • 40살이 넘었을때만 나이를 올리면 40살 이하일때는 재랜더링 대상이 아닌 것과 같은 경우

    • 내부적으로 고도의 최적화 => 어떻게 ?

      • 매번 재계산을 하지 않는다

      • 계산에 사용할 observable 값이 변경되지 않으면 재실행하지 않음.

      • 다른 computed 또는 reaction 에 의해 호출되지 않으면 재실행하지 않음.

      • observable 이 변했는데 computed 가 변하지 않을때 랜더하지 않음.

@action  (by mobx)

@inject 와 Provider

Provider

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

mobx-devtools

npm i mobx-react-devtools -D

MobX Developer Tools

App.js

import DevTools from 'mobx-react-devtools';

class MyApp extends React.Component {
  render() {
    return (
      <div>
        ...
        <DevTools />
      </div>
    );
  }
}

stores

RootStore.js

import AuthStore from './AuthStore';
import BookStore from './BookStore';

class RootStore {
  constructor() {
    this.authStore = new AuthStore(this);
    this.bookStore = new BookStore(this);
  }
}

export default RootStore;

BookStore.js

import { observable, action, makeObservable } from 'mobx';

class BookStore {
  @observable
  books = [];

  @observable
  loading = false;

  @observable
  error = null;

  constructor(rootStore) {
    makeObservable(this);
    this.rootStore = rootStore;
  }

  @action
  start = () => {
    this.loading = true;
    this.books = [];
    this.error = null;
  };

  @action
  success = (books) => {
    this.loading = false;
    this.books = books;
    this.error = null;
  };

  @action
  fail = (error) => {
    this.loading = false;
    this.books = [];
    this.error = error;
  };
}

export default BookStore;

AuthStore.js

import { observable, action, makeObservable } from 'mobx';

class AuthStore {
  @observable
  token = [];

  @observable
  loading = false;

  @observable
  error = null;

  constructor(rootStore) {
    makeObservable(this);
    this.rootStore = rootStore;
  }

  @action
  start = () => {
    this.loading = true;
    this.token = null;
    this.error = null;
  };

  @action
  success = (token) => {
    this.loading = false;
    this.token = token;
    this.error = null;
  };

  @action
  fail = (error) => {
    this.loading = false;
    this.token = null;
    this.error = error;
  };
}

export default AuthStore;

App.js

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

const rootStore = new RootStore();

function App() {
  return (
    <ErrorBoundary FallbackComponent={Error}>
      <Provider {...rootStore}>
        <ConnectedRouter history={history}>
          <Switch>
            <Route path="/add" component={Add} />
            <Route path="/signin" component={Signin} />
            <Route path="/" exact component={Home} />
            <Route component={NotFound} />
          </Switch>
        </ConnectedRouter>
      </Provider>
    </ErrorBoundary>
  );
}

export default App;

Asynchronous actions

BookStore.js

import { observable, action, makeObservable } from 'mobx';

class BookStore {
  ...
  
  getBooks = async () => {
    this.loading = true;
    this.token = null;
    this.error = null;
    try {
      const books = await BookService.getBooks(this.rootStore.authStore.token);
      runInAction(() => {
        this.loading = false;
        this.books = books;
        this.error = null;
      });
    } catch (error) {
      runInAction(() => {
        this.loading = false;
        this.token = null;
        this.error = error;
      });
    }
  };
}

export default BookStore;

MobxBookListContainer.js

export default inject('bookStore')(
  observer(function BookListContainer({ bookStore }) {
    const { books, loading, error, getBooks } = bookStore;

    return (
      <BookList
        books={books}
        loading={loading}
        error={error}
        getBooks={getBooks}
      />
    );
  }),
);

MobX 2021

By Woongjae Lee

MobX 2021

몹엑스 2021

  • 1,531