Lead Software Engineer @ProtoPie
Microsoft MVP
TypeScript Korea User Group Organizer
Marktube (Youtube)
이 웅재
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
2017.07
2018.01
데코레이터를 적극 활용한다.
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 로 공유해가는 방식으로
스토어를 어떻게 사용지에 대한 적합한 해결을 모색해야 할 것
npm i customize-cra react-app-rewired -Dconst { override, addDecoratorsLegacy } = require('customize-cra');
module.exports = override(addDecoratorsLegacy());
{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}
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;
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;
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;
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(<value>)
데코레이터 없이 사용하는 방식
@ 없이, 함수처럼 사용해서 리턴한 객체를 사용
@observable <클래스의 프로퍼티>
데코레이터로 사용하는 법
클래스 내부에 프로퍼티 앞에 붙여서 사용
한 클래스 안에 여러개의 @observable 존재
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(<컴포넌트>);
데코레이터 없이 사용하는 방식
함수 컴포넌트에 사용
<컴포넌트 클래스> 에 @observer 달아서 처리
클래스 컴포넌트에 사용
computed(내부에서 observable 을 사용하는 함수);
데코레이터 없이 사용하는 방식
<observable 클래스> 의 getter 에 @computed 달아서 처리
스토어에 사용
getter 에만 붙일수 있다.
함수가 아니라 리액티브 하다는 것에 주목
실제 컴포넌트에서 사용하는 (게터)값들에 달아서 사용하면 최소 범위로 변경할 수 있기 때문에 유용하다.
40살이 넘었을때만 나이를 올리면 40살 이하일때는 재랜더링 대상이 아닌 것과 같은 경우
내부적으로 고도의 최적화 => 어떻게 ?
매번 재계산을 하지 않는다
계산에 사용할 observable 값이 변경되지 않으면 재실행하지 않음.
다른 computed 또는 reaction 에 의해 호출되지 않으면 재실행하지 않음.
observable 이 변했는데 computed 가 변하지 않을때 랜더하지 않음.
npm i mobx-react-devtools -Dimport DevTools from 'mobx-react-devtools';
class MyApp extends React.Component {
  render() {
    return (
      <div>
        ...
        <DevTools />
      </div>
    );
  }
}import AuthStore from './AuthStore';
import BookStore from './BookStore';
class RootStore {
  constructor() {
    this.authStore = new AuthStore(this);
    this.bookStore = new BookStore(this);
  }
}
export default RootStore;
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;
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;
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;
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;
export default inject('bookStore')(
  observer(function BookListContainer({ bookStore }) {
    const { books, loading, error, getBooks } = bookStore;
    return (
      <BookList
        books={books}
        loading={loading}
        error={error}
        getBooks={getBooks}
      />
    );
  }),
);