MobX와 Redux의 차이 및 Mobx 학습 정리
by 진혀쿠
함께 프로젝트를 진행하고 있는 우진님께서 정리해주신 MobX에 관한 학습 자료입니다.
본격적인 프로젝트를 앞두고 어떤 상태관리 라이브러리를 사용할까 고민하다 Redux와 MobX로 의견을 좁히게 되었고 둘의 차이를 알아본 후 선택하자는 이야기가 나왔습니다.
우진님께서 이 부분을 담당해주셨고, 우진님의 허락을 받고 정리해주신 문서를 공유합니다.
MobX란?
- 상태관리 라이브러리
- React와 함께 사용하는 상태 관리 라이브러리중 Redux와 MobX가 가장 많이 사용되고 있다
- MobX는 Redux와 마찬가지로 상태 관리 라이브러리이지 React 전용 라이브러리는 아니다
- Redux는 react-redux, MobX는 mobx-react 라이브러리를 추가적으로 사용해 React에서 쉽게 사용할 수 있다
장점
- 기본적으로 ES6 Class 방식을 지원하고 권장하여 객체지향적으로 코드를 작성할 수 있다
- 상태관리를 위한 Store와 Component 사이에 연결을 위해 Redux와 달리 번잡한 보일러플레이트 코드를 데코레이터를 사용해서 코드를 깔끔하게 작성할 수 있다
- Redux는 상태를 변경하기 위해 reducer, action등 코드를 작성해주어야하고, 프로젝트에 규모가 커짐에 따라 이러한 코드들이 꼬여서 점점 스파게티 코드가 되어간다
- Mutable한 상태변경이 가능하다.
Redux vs MobX
- Redux는 함수형 프로그래밍에 영향을 받은 라이브러이다. MobX는 OOP를 권장하는 라이브러이다. OOP 익숙한 많은 개발자들이 쉽게 접근하고 사용할 수 있다.
- Redux는 구조상 Store와 Component의 연결을 위해 번잡한 코드들을 계속 작성해주어야한다. MobX는 이러한 코드를 데코레이터를 사용하여 깔끔하게 작성할 수 있다
- Redux는 Store의 상태를 Immutable하게 변경하기 때문에 항상 새로운 상태를 반환해야한다(Read Only). MobX는 Mutable하게 변경이 가능하다(Read and Write)
- Redux의 이러한 특성 때문에 상태를 Immutable하게 변경하기 위해 추가작업이 필요한데 Destructuring을 활용해 새로운 상태를 반환하거나, Immutable.js 라이브러리를 이용해서 Immutable한 데이터 구조를 활용하게 된다. (라이브러리 학습이 또 필요)
- 비동기 처리를 위해서 Redux는 별도의 라이브러리를 추가적으로 사용해야한다.(redux-thunk, redux-saga). MobX는 async action을 지원해서 async/await 문법을 사용해 깔끔하게 처리가 가능하다.
MobX 주요개념
MobX의 기본 동작방식은 위의 사진과 같다. 상태를 변경하기 위해서 event와 같은 action(행동)이 발생하면 상태(Observable)가 update되고, 상태가 변경되었음을 notify해서 필요에 따라 연산된 값(Compute Values)을 계산하거나 적절한 reaction(반응)을 수행한다.
- Observable State
- MobX 를 사용한 상태관리 데이터는 Observable하다.
- MobX에서 상태는 관찰 할 수 있는 상태이며, 관찰하고 있는 상태가 변경되었을 때 특정 작업을 수행할 수 있다.
import { observable } from "mobx"
//Observable 상태 만들기
//Properties, entire objects, arrays, Maps and Sets 모두 Observable하게 만들 수 있다
const calculator = observable({
a: 10,
b: 10
})
- Reactions
- Reactions는 Observable State의 값이 변경되었을 때 수행한다.
- Observable State 의 내부의 값이 바뀔 때 마다 자동으로 특정 작업(Side Effect)을 수행하도록 Reactions을 정의할 수 있다.
- 주의 : Observable State의 값을 변경할 때는 action을 사용하도록 강제하고 있으며 MobX의 설계상 action을 사용하는 것이 바람직해 보인다.
import { observable, reaction } from "mobx"
const calculator = observable({
a: 10,
b: 10
})
//특정 값이 바뀔 때 특정 작업 하기!
reaction(
() => calculator.a, //a 값을 관찰한다
(value, reaction) => {
console.log(`a 값이 ${value} 로 바뀌었네요!`);
}
);
reaction(
() => calculator.b, //b 값을 관찰한다
(value) => {
console.log(`b 값이 ${value} 로 바뀌었네요!`);
}
);
- Actions
- Action(행동)은 Observable state에 변화를 일으키는 것
- Obeservable State의 값을 변경하는 상태변경 함수를 이용해서 상태를 변경하고, 변경된 상태에 따라 자동으로 사이드 이펙트를 일으키는 것이 MobX의 핵심 동작이며 개념이다.
- 기본적으로 action을 사용하지 않고, 상태를 변경하는 것을 MobX에서는 허용하지 않는다.
- 이를 강제함으로써 상태변화을 일으키는(Side Effect) 부분을 명확하게 한다.
import { observable, reaction, action } from "mobx"
const calculator = observable({
a: 10,
b: 10
})
reaction(
() => calculator.a,
(value, reaction) => {
console.log(`a 값이 ${value} 로 바뀌었네요!`);
}
);
reaction(
() => calculator.b,
(value) => {
console.log(`b 값이 ${value} 로 바뀌었네요!`);
}
);
action(changeValues, (a, b) => { // 상태를 변경하는 action 함수를 정의
calculator.a = a;
calculator.b = b;
});
changeValues(1, 3);
//console 결과
//a 값이 1 로 바뀌었네요!
//b 값이 3 로 바뀌었네요!
- Computed Value
- 어플리케이션에서 유지하는 상태 값을 그대로 이용하는 경우도 있지만, 이 상태 값을 바탕으로 특정 값을 계산해야할 경우도 존재한다.
- Computed Value(연산된 값)은 필요에 의해 특정 상태 값을 기반으로 자동으로 연산되어지는 값을 의미한다.
- 즉, 특정 상태 값을 기반으로 Computed Value(연산된 값)를 자동으로 계산하여 사용할 수 있게 해주며, 연산에 사용된 특정 상태 값을 관찰하여 값이 변할 때 마다 자동으로 계산하여 사용할 수 있게 해준다.
- ex) 학생들의 성적(점수, 총합 점수, 평균)을 상태관리한다면 학생들의 점수가 변경될 때마다 총합점수, 평균 값은 다시 계산되어야한다.
- Computed Value(연산된 값)은 내부적으로 캐시하여 연산에 기반되는 값이 바뀔때만 새로 연산한다.
- 성능 최적화에도 사용할 수 있다. 가능한 한 사용하도록 MobX에서 권장하고 있다.
- Computed Value(연산된 값)는 관찰하고 있는 특정 상태 값이 변경 되었을 때 다시 계산하지만, 특정 상태 값이 변경 되었을 때 Computed Value(연산된 값)의 결과가 변하지 않는다면 다시 계산하지 않는다. (중요)
import { observable, reaction, action, computed } from "mobx";
const calculator = observable({
a: 10,
b: 10
});
reaction(
() => calculator.a,
(value, reaction) => {
console.log(`a 값이 ${value} 로 바뀌었네요!`);
}
);
reaction(
() => calculator.b,
(value) => {
console.log(`b 값이 ${value} 로 바뀌었네요!`);
}
);
const changeValues = action((a, b) => {
calculator.a = a;
calculator.b = b;
});
const sum = computed(() => { //computed value를 계산하기 위한 함수
console.log("계산중입니다.");
return calculator.a + calculator.b;
});
sum.observe_(() => calculator.a); // a, b 값의 상태를 관찰
sum.observe_(() => calculator.b);
changeValues(1, 3); //상태를 변경했을 때만 계산하고 캐싱
console.log(sum.get()); //캐싱 이후에는 아무리 호출하여도 계산하지 않는다
console.log(sum.get());
console.log(sum.get());
//console 결과
//계산중입니다.
//a 값이 1 로 바뀌었네요!
//계산중입니다.
//b 값이 3 로 바뀌었네요!
//4
//4
//4
- autorun
- autorun 으로 전달하는 콜백 함수에서 Obeservable State의 값이 사용되고 있으면 자동으로 그 값을 관찰하여 그 값이 바뀔 때 마다 콜백 함수가 실행된다.
import { observable, reaction, action, computed, autorun } from "mobx";
const calculator = observable({
a: 10,
b: 10
});
reaction(
() => calculator.a,
(value, reaction) => {
console.log(`a 값이 ${value} 로 바뀌었네요!`);
}
);
reaction(
() => calculator.b,
(value) => {
console.log(`b 값이 ${value} 로 바뀌었네요!`);
}
);
const changeValues = action((a, b) => {
calculator.a = a;
calculator.b = b;
});
const sum = computed(() => {
console.log("계산중입니다.");
return calculator.a + calculator.b;
});
sum.observe_(() => calculator.a);
sum.observe_(() => calculator.b);
autorun(() => {
console.log("자동실행합니다.", calculator.a, calculator.b);
});
changeValues(1, 3);
console.log(sum.get());
console.log(sum.get());
console.log(sum.get());
//console 결과
//계산중입니다.
//자동실행합니다. 10 10
//a 값이 1 로 바뀌었네요!
//계산중입니다.
//자동실행합니다. 1 3
//b 값이 3 로 바뀌었네요!
//4
- transaction
- action들을 transaction 단위로 묶어서 실행할 수 있다.
- transaction 단위로 묶인 모든 action이 실행된 이후, 최종 상태 값에 대해 Side Effect(reaction, autorun 등)가 발생한다.
import {
observable,
reaction,
action,
computed,
autorun,
transaction
} from "mobx";
const calculator = observable({
a: 10,
b: 10
});
reaction(
() => calculator.a,
(value, reaction) => {
console.log(`a 값이 ${value} 로 바뀌었네요!`);
}
);
reaction(
() => calculator.b,
(value) => {
console.log(`b 값이 ${value} 로 바뀌었네요!`);
}
);
const changeValues = action((a, b) => {
calculator.a = a;
calculator.b = b;
});
const sum = computed(() => {
console.log("계산중입니다.");
return calculator.a + calculator.b;
});
sum.observe_(() => calculator.a);
sum.observe_(() => calculator.b);
autorun(() => {
console.log("자동실행합니다.", calculator.a, calculator.b);
});
changeValues(1, 3);
console.log(sum.get());
console.log(sum.get());
console.log(sum.get());
transaction(() => { //트랜잭션으로 실행할 콜백함수를 인자로 넘겨서 트랜잭션 실행
changeValues(20, 20);
changeValues(30, 30);
changeValues(40, 40); // 최종적으로 상태로 update
});
//console 결과
//계산중입니다.
//자동실행합니다. 10 10
//a 값이 1 로 바뀌었네요!
//계산중입니다.
//자동실행합니다. 1 3
//b 값이 3 로 바뀌었네요!
//4
//a 값이 40 로 바뀌었네요!
//계산중입니다.
//자동실행합니다. 40 40
//b 값이 40 로 바뀌었네요!
이외에도 다양한 사용법과 기능, option이 존재한다. 위의 예제는 개념이해를 돕기위한 예제일 뿐, 실제 사용법은 공식문서를 참조해서 진행하자.
MobX with ES6 Class
- ES6 의 class 문법을 사용하면 조금 더 깔끔하게 코드 작성이 가능하다 (MobX에서도 권장)
- 편의점 장바구니 예제
- makeObservable을 활용하여 직관적이고, 쉽게 Observable State를 정의할 수 있다
- Observable State를 만드는 방법은 다양한 방법이 존재한다 (공식문서 참조)
import { action, autorun, computed, makeObservable, observable } from "mobx";
class GS25 {
constructor() {
this.basket = []; //장바구니
makeObservable(this, { //makeObservable을 활용
basket: observable,
totalPrice: computed,
select: action
});
//makeAutoObservable(this);
autorun(() => console.log(this.totalPrice));
}
get totalPrice() {
return this.basket.reduce((totalPrice, cur) => totalPrice + cur.price, 0);
}
select(name, price) {
this.basket.push({ name, price });
}
}
const gs25 = new GS25();
gs25.select("물", 800);
gs25.select("포카칩", 1500);
//console 결과
//0
//800
//2300
Async Action
- async/await를 활용하여 비동기로 Observable State 변경이 가능하다.
- 단, 상태를 변경하기 위해서는 action을 활용해서만 가능하다.
- API 요청과 같은 일회성 action은 runInAction을 활용해서 상태를 변경하는 콜백함수 인자로 바로 호출할 수 있다.
import {
action,
autorun,
computed,
makeObservable,
observable,
runInAction
} from "mobx";
class GS25 {
constructor() {
this.basket = [];
makeObservable(this, {
basket: observable,
totalPrice: computed,
select: action
});
autorun(() => console.log(this.totalPrice));
}
async initBasket() { //Fake Basket API를 호출하여 장바구니 상태 초기화
try {
const initialBasket = await this.getBaskets();
runInAction(() => {
this.basket = initialBasket.reduce(
(acc, cur) => acc.concat({ name: cur.name, price: cur.price }),
this.basket
);
console.log("done");
});
} catch (e) {
console.log("error");
}
}
async getBaskets() { //Fake Basket API
return new Promise((resolve, reject) => {
setTimeout(() => resolve([{ name: "빼빼로", price: 1000 }]), 5000);
});
}
get totalPrice() {
return this.basket.reduce((totalPrice, cur) => totalPrice + cur.price, 0);
}
select(name, price) {
this.basket.push({ name, price });
}
}
async function run() {
const gs25 = new GS25();
await gs25.initBasket();
gs25.select("물", 800);
gs25.select("포카칩", 1500);
}
run();
/** console 결과
0
done
1000
1800
3300
**/
MobX with Decorator
- Babel을 활용하면 JS 최신문법을 이용해서 더욱 간결하게 코드를 작성 할 수 있다.
- Obeservable State를 정의하기 위한 makeObservable과 같은 반복적인 코드 작성을 데코레이터를 이용해서 쉽게 처리가 가능하다
- 필요한 Babel 플러그인
- @babel/plugin Proposal Class Properties : class문법에서 프로퍼티를 간결하게 선언가능
- @babel/plugin Proposal Decorators : 데코레이터 문법 활용가능
import { action, autorun, computed, observable } from "mobx";
class GS25 {
@observable basket = [];
@computed
get totalPrice() {
return this.basket.reduce((totalPrice, cur) => totalPrice + cur.price, 0);
}
@action
select(name, price) {
this.basket.push({ name, price });
}
}
MobX with React
- mobx-react 라이브러리를 활용해서 MobX를 React와 쉽게 사용할 수 있다.
-
MobX를 활용하여 전역 상태를 관리하고, 특정 상태가 변경되었을 때, 특정 컴포넌트를 렌더링하고 싶을 때 mobx-react 라이브러리를 통해 쉽게 적용하는 것
- Store 정의
- Observable State를 만들어 Store를 정의한다.
import { action, autorun, observable } from "mobx";
export class SampleStore {
@observable states= [];
@action
addValue(newValue) {
this.states = [...this.states, newValue];
}
...
}
- Provider Wrapping
- 하위 컴포넌트에서 Store에 접근할 수 있도록 최상위 루트 컴포넌트를 mobx-react의 Provider 컴포넌트로 Wrapping 해준다.
//index.js
....
import { SampleStore} from "....";
import { Provider } from "mobx-react";
const sampleStore= new SampleStore();
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<Provider store=>
<App />
</Provider>
</StrictMode>,
rootElement
);
- Component 정의
- @inject(“store”) 데코레이터를 활용하면 Provider로 전달한 store를 props를 통해 접근 할 수 있다.
- @observer 데코레이터를 활용하여 컴포넌트를 store의 상태변화에 따라 자동으로 렌더링을 Trigger 할 수 있다.
- core-decorators의 autobind 데코레이터를 활용하면 class 스타일 React 컴포넌트에서 반복되는 this 바인딩 작업을 자동으로 해준다.
import "./SampleComponent .css";
import { inject, observer } from "mobx-react";
import { Component } from "react";
import { autobind } from "core-decorators";
@inject("store")
@observer
@autobind
class SampleComponent extends Component {
onClickListener() {
const newValue = prompt("Enter New Value");
const { sampleStore} = this.props.store;
sampleStore.addValue(newValue);
}
getChilds() {
const { states } = this.props.store.sampleStore;
return states.map((value) => (
<span>value</span>
));
}
render() {
return (
<div className="container">
<button type="button" onClick={this.onClickListener}>
Add Value
</button>
{this.getChilds()}
</div>
);
}
}
export default SampleComponent;
MobX with React + Hooks
- MobX는 class를 활용하여 객체지향 방식을 권장하고 있지만, React는 Hooks를 활용하여 함수형 방식으로 컴포넌트를 작성하는 것을 권장한다.
-
MobX의 Obsevable State는 똑같이 정의하고, 컴포넌트에서 Store 접근을 데코레이터를 활용하지 않고, Context API, useContext Hook을 활용하여 접근하면 된다.
- Store 정의
import { action, observable, makeObservable } from "mobx";
export class SampleStore {
states = [];
constructor(){
makeObservable(this, {
states: observable,
addValue: action
});
}
...
}
- Context 정의
import React from "react";
import { SampleStore } from "....";
export const StoreContext = React.createContext(new SampleStore());
export const StoreProvider = StoreContext.Provider;
export const useStores = () => React.useContext(StoreContext);
- Provider Wrapping
...
import { StoreProvider } from "......";
import { SampleStore } from ".....";
const sampleStore= new SampleStore();
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<StoreProvider value={sampleStore}>
<App />
</StoreProvider>
</StrictMode>,
rootElement
);
- Component 정의
- observer 함수는 HOC(고차 컴포넌트)로 React 컴포넌트가 Observable State를 자동으로 구독하게 해준다. Observable State가 변경되면 자동으로 렌더링이 트리거 되는 이유. (mobx-react도 동일)
- MobX가 자동으로 렌더링을 관리하여 렌더링 최적화를 위한 추가작업이 필요없다. (mobx-react도 동일)
import "./SampleComponent .css";
import { useStores } from "......";
import { observer } from "mobx-react";
export default observer(() => {
const { sampleStore } = useStores();
const onClickListener = () => {
const newValue = prompt("Enter New Value");
sampleStore.addValue(newValue);
};
const getChilds = () => {
const { states } = sampleStore;
return states.map((value) => (
<span>value</span>
));
};
return (
<div className="container">
<button type="button" onClick={onClickListener}>
Add Value
</button>
{getChilds()}
</div>
);
});
mobx-react-lite
- mobx-react 보다 가벼운 패키지
- 함수형 컴포넌트를 위한 기능만 지원하기 때문에 가볍다
- 몇 가지 유용한 hook API를 제공한다
- useLocalObservable : Local Obeservable State를 만들기 위한 hook
- https://github.com/mobxjs/mobx-react-lite (mobx-react-lite, GitHUB 저장소 참고)
- mobx-react v5 에서는 함수형 컴포넌트의 hook API를 지원하지 않았지만, v6 이후부터는 지원한다.
- mobx-react v5 이전에는 hook을 사용할 수 없었기 때문에 함수형 컴포넌트를 이용한다면 **mobx-react-lite를 이용했다
- https://github.com/mobxjs/mobx-react#observer (mobx-react, GitHUB 저장소 참고)
MobX 아키텍처
-
MobX는 Java Spring Framework유사한 Layered 아키텍처를 가지고 있으며, 실제로 그런식으로 분리하여 아키텍처를 구성하는 것을 권장한다.
- Store = Service
- Java Spring Service와 비슷한 역할
- BackEnd Server의 Service(Spring)에서는 다수의 요청자에 의해서 요청 되기 때문에 특별한 상황이 아니라면 상태를 가지고 있지 않다. (Stateless)
- Mobx Store는 observable한 state(상태)를 가지고 있다
- Client는 사용자와 1:1 이기 때문에 서버측의 Service에서 상태와는 상황이 다르기 때문.
@Autobind class RiderStore { @observable riderList = []; //state constructor(rootStore) { this.rootStore = rootStore; } @asyncAction //비동기 요청, service에서 repository를 활용해서 데이터를 가져온다 async *findAll(params) { const { data, status } = yield riderRepository.findAll(params); this.riderList = data.map(rider => new RiderModel(rider)); } @action removeRider(index) { this.riderList.splice(index, 1); } @computed get activeRiders() { return this.riderList.filter(rider => rider.isActive); } } export default RiderStore;
- Java Spring Service와 비슷한 역할
- Repository = Repository
- Mobx Repository는 Ajax로 데이터를 가져오는 부분.
- 데이터를 가져오는 부분도 Layer를 나누어 구성하는 것을 권장
- Backend Server에서의 DB 조회와 비슷
- 데이터와 비즈니스 로직 분리하고, Test 코드 작성 시 Mocking이 용이하다.
- Mobx Repository는 Ajax로 데이터를 가져오는 부분.
class RiderRepository {
URL = "/v1/api/riders";
constructor(url) {
this.URL = url || this.URL;
}
findAll(params) {
return axios.get(`${this.URL}`, { params });
}
findOne(riderAccountId) {
return axios.get(`${this.URL}/${agencyId}`);
}
}
export default new RiderRepository(); // 싱글톤
- Model = Entity or Dto
- Spring의 Entity/Dto 와 유사
- extendObservable :
- Object.assign 처럼 property와 값을 Target 오브젝트에 합쳐 준다
- Observable한 Property를 만들어 기존의 Observable State 을 동적으로 확장가능하다
- 합치려는 객체의 Property가 이미 선언 되어 있는 경우는 사용 할 수 없다
- https://mobx.js.org/api.html#extendobservable (MobX 공식문서)
//미리 선언된 Observable 프로퍼티가 존재하지 않을 때
//서버에서 받아온 JSON을 RiderModel로 생성하는 예시
@Autobind
class RiderModel {
constructor(data) {
extendObservable(this, data);
}
// 모델 자신의 비즈니스로직
// 라이더 이름과 지점명을 합친 결과를 리턴
@computed
get riderWithAgency() {
return `${this.riderName}(${this.agencyName})`;
}
@action
changeRiderName(riderName) {
this.riderName = riderName;
}
isActive() {
return this.status === 'ACTIVE';
}
}
export default RiderModel;
// 미리 선언된 Property가 있는 Model에 서버에서 가져온 JSON으로 객체를 생성하는 경우
// Mobx에서 제공하는 set api를 사용
@Autobind
class RiderModel {
@observable
riderName;
constructor(data) {
set(this, data);
}
@computed
get riderNameWithAgency() {
return `${this.riderName}(${this.agencyName})`;
}
@action
changeRiderName(riderName) {
this.riderName = riderName;
}
}
export default RiderModel;
- Model Layer가 없다면?
// Model이 없으니 이런형태로 데이터를 정의.
let riderList = [
{
name: '홀길동'
age: 24,
agencyName: '강남지점'
},
{
name: '이순신'
age: 34,
agencyName: '송파지점'
}
]
export default class SearchRider extends React.Component {
// Component가 Mount될 때 서버에서 라이더 리스트를 가져오는 로직..
componentDidMount() {
const riders = axios.get('http://www.rider.com/api/riders')
.then(function(response) {
response.data.map(rider => {
rider.riderNameWithAgency = `${this.riderName}(${this.agencyName})`;
return rider;
})
}
);
this.setState({riders});
}
// 특정 라이더의 이름을 변경하는 경우의 로직도 Component 메소드에..
chanageRiderName = (riderName, riderId) =>{
this.state.riders.map(rider => {
if (rider.riderId === riderId) {
rider.riderName = riderName;
}
return rider;
})
this.setState({
riders:this.state.riders
})
}
render(){
.........
}
}
- Presentation
- MobX의 상태가 변경되었다면 React 컴포넌트를 렌더링한다. ⇒ Presentation Layer
//store
@Autobind
export default class SearchRiderStore {
@observable
riderList = [];
constructor(rootStore) {
this.rootStore = rootStore;
}
@asyncAction
async *findAllRider(params) {
const { data, status } = yield riderRepositiry.findAll(params);
if (status === 200) {
this.riderList = data.map(rider => new RiderModel(data));
}
}
}
//Component
@inject("searchRiderStore")
@observer
@Autobind
export default class SearchRiderContainer extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() { // 컴포넌트가 마운트 되면 라이더를 가져온다 (Lazy)
const { searchRiderStore } = this.props;
searchRiderStore.findAllRider();
}
// 라이더 리스트를 테이블(Presentation Component)로 렌터링
// Container와 Presentation이 나뉘면 Test가 쉬워진다
render() {
const { riderList } = this.props.searchRiderStore;
return <RiderListTable riderList={riderList} />;
}
}
참고자료
- https://mobx.js.org/ (MobX, 공식문서)
- https://velog.io/@velopert/begin-mobx#reaction (벨로퍼트, MobX (1) 시작하기)
- https://woowabros.github.io/experience/2019/01/02/kimcj-react-mobx.html (우아한형제들 기술블로그, React에서 Mobx 경험기 (Redux와 비교기))
- https://www.robinwieruch.de/redux-mobx#difference (Redux와 MobX 차이점)
- https://github.com/mobxjs/mobx-react-lite (GitHUB, mobx-react-lite)
Subscribe via RSS