2. 리액트 없이 MobX 쓰기
리덕스와 마찬가지로, MobX 는 리액트 종속적인 라이브러리가 아닙니다. 그냥 따로 쓸 수도 있어요. UI 프레임워크 / 라이브러리 없이 쓰셔도 되고, Vue, Angular 등이랑 써도 전혀 무방합니다.
MobX 를 자세하게 이해하려면, 라이브러리 없이 사용을 해보는게 가장 좋습니다!
이번 실습은 CodeSandbox 에서 진행하겠습니다.
이번에는 딱히 HTML 은 건들이지 않고 JavaScript 만 수정해보도록 할게요!
import { observable, reaction, computed, autorun } from 'mobx';
우선, mobx 에서 함수 몇가지들을 불러옵니다. 이 함수들은 하나하나 사용해보면서 설명을 드리도록 하겠습니다.
observable
observable 함수는 Observable State 를 만들어줍니다.
한번, 덧셈을 해주는 계산기 객체를 만들어보겠습니다.
import { observable, reaction, computed, autorun } from 'mobx';
// **** Observable State 만들기
const calculator = observable({
a: 1,
b: 2
});
이렇게 Observable State 를 만들고나면 MobX 가 이 객체를 "관찰 할 수" 있어서 변화가 일어나면 바로 탐지해낼수있습니다.
reaction
특정 값이 바뀔 때 어떤 작업을 하고 싶다면 reaction 함수를 사용합니다.
한번 a 나 b 가 바뀔 때 console.log 로 바뀌었다고 알려주도록 코드를 작성해보겠습니다.
import { observable, reaction, computed, autorun } from 'mobx';
// Observable State 만들기
const calculator = observable({
a: 1,
b: 2
});
// **** 특정 값이 바뀔 때 특정 작업 하기!
reaction(
() => calculator.a,
(value, reaction) => {
console.log(`a 값이 ${value} 로 바뀌었네요!`);
}
);
reaction(
() => calculator.b,
value => {
console.log(`b 값이 ${value} 로 바뀌었네요!`);
}
);
calculator.a = 10;
calculator.b = 20;
콘솔쪽을 보시면 다음과 같이 나타날 것입니다.
a 값이 10 로 바뀌었네요!
b 값이 20 로 바뀌었네요!
computed
computed 함수는 연산된 값을 사용해야 할 때 사용됩니다. 특징은, 이 값을 조회할 때 마다 특정 작업을 처리하는것이 아니라, 이 값에서 의존하는 값이 바뀔 때 미리 값을 계산해놓고 조회 할 때는 캐싱된 데이터를 사용한다는 점 입니다.
import { observable, reaction, computed, autorun } from 'mobx';
// Observable State 만들기
const calculator = observable({
a: 1,
b: 2
});
// **** 특정 값이 바뀔 때 특정 작업 하기!
reaction(
() => calculator.a,
(value, reaction) => {
console.log(`a 값이 ${value} 로 바뀌었네요!`);
}
);
reaction(
() => calculator.b,
value => {
console.log(`b 값이 ${value} 로 바뀌었네요!`);
}
);
// **** computed 로 특정 값 캐싱
const sum = computed(() => {
console.log('계산중이예요!');
return calculator.a + calculator.b;
});
sum.observe(() => calculator.a); // a 값을 주시
sum.observe(() => calculator.b); // b 값을 주시
calculator.a = 10;
calculator.b = 20;
//**** 여러번 조회해도 computed 안의 함수를 다시 호출하지 않지만..
console.log(sum.value);
console.log(sum.value);
// 내부의 값이 바뀌면 다시 호출 함
calculator.a = 20;
console.log(sum.value);
결과를 확인해볼까요?
계산중이예요!
a 값이 10 로 바뀌었네요!
계산중이예요!
b 값이 20 로 바뀌었네요!
계산중이예요!
30
30
a 값이 20 로 바뀌었네요!
계산중이예요!
40
autorun
autorun 은 reaction 이나 computed 의 observe 대신에 사용 될 수 있는데, autorun 으로 전달해주는 함수에서 사용되는 값이 있으면 자동으로 그 값을 주시하여 그 값이 바뀔 때 마다 함수가 주시되도록 해줍니다. 여기서 만약에 computed 로 만든 값의 .get() 함수를 호출해주면, 하나하나 observe 해주지 않아도 됩니다.
import { observable, reaction, computed, autorun } from 'mobx';
// Observable State 만들기
const calculator = observable({
a: 1,
b: 2
});
// computed 로 특정 값 캐싱
const sum = computed(() => {
console.log('계산중이예요!');
return calculator.a + calculator.b;
});
// **** autorun 은 함수 내에서 조회하는 값을 자동으로 주시함
autorun(() => console.log(`a 값이 ${calculator.a} 로 바뀌었네요!`));
autorun(() => console.log(`b 값이 ${calculator.b} 로 바뀌었네요!`));
autorun(() => sum.get()); // su
calculator.a = 10;
calculator.b = 20;
// 여러번 조회해도 computed 안의 함수를 다시 호출하지 않지만..
console.log(sum.value);
console.log(sum.value);
calculator.a = 20;
// 내부의 값이 바뀌면 다시 호출 함
console.log(sum.value);
결과를 확인해볼까요?
a 값이 1 로 바뀌었네요!
b 값이 2 로 바뀌었네요!
계산중이예요!
a 값이 10 로 바뀌었네요!
계산중이예요!
b 값이 20 로 바뀌었네요!
계산중이예요!
30
30
a 값이 20 로 바뀌었네요!
계산중이예요!
40
class 문법을 사용해서 조금 더 깔끔하게
ES6 의 class 문법을 사용하면 조금 더 깔끔하게 코드를 작성 할 수 있습니다. 기존의 코드를 날리고, 이번엔 편의점 장바구니를 만들어보겠습니다. class 로 장바구니를 구현 후, decorate 함수를 통하여 MobX 를 적용해주겠습니다.
import { decorate, observable, computed, autorun } from 'mobx';
class GS25 {
basket = [];
get total() {
console.log('계산중입니다..!');
// Reduce 함수로 배열 내부의 객체의 price 총합 계산
// https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
return this.basket.reduce((prev, curr) => prev + curr.price, 0);
}
select(name, price) {
this.basket.push({ name, price });
}
}
// decorate 를 통해서 각 값에 MobX 함수 적용
decorate(GS25, {
basket: observable,
total: computed,
});
const gs25 = new GS25();
autorun(() => gs25.total);
gs25.select('물', 800);
console.log(gs25.total);
gs25.select('물', 800);
console.log(gs25.total);
gs25.select('포카칩', 1500);
console.log(gs25.total);
결과는 다음과 같습니다.
계산중입니다..!
계산중입니다..!
800
계산중입니다..!
1600
계산중입니다..!
3100
action
우리가 이전에 상태에 변화를 일으키는것을 action 이라고 부른다고 언급했습니다. 만약에, 이 변화를 일으키는 함수에 MobX 의 action 을 적용하면 무엇을 할 수 있는지 알아보겠습니다. 우선, 코드 상단에서 action 함수를 불러오고, decorate 쪽에 select 가 action 이라는 것을 명시해줄게요.
// **** 액션 불러옴
import { decorate, observable, computed, autorun, action } from 'mobx';
class GS25 {
basket = [];
get total() {
console.log('계산중입니다..!');
// Reduce 함수로 배열 내부의 객체의 price 총합 계산
// https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
return this.basket.reduce((prev, curr) => prev + curr.price, 0);
}
select(name, price) {
this.basket.push({ name, price });
}
}
decorate(GS25, {
basket: observable,
total: computed,
select: action // **** 액션 명시
});
const gs25 = new GS25();
autorun(() => gs25.total);
gs25.select('물', 800);
console.log(gs25.total);
gs25.select('물', 800);
console.log(gs25.total);
gs25.select('포카칩', 1500);
console.log(gs25.total);
이 action 을 사용함에 있어서의 이점은 나중에 개발자도구에서 변화의 세부 정보를 볼 수 있고, 변화를 한꺼번에 일으켜서 변화가 일어날 때 마다 reaction 들이 나타나는것이 아니라, 모든 액션이 끝나고 난 다음에서야 reaction 이 나타나게끔 해줄 수 있습니다.
액션을 한꺼번에 일으키는건, transaction 을 통해 할 수 있습니다.
우선 다음 예제의 콘솔 결과를 확인해보겠습니다.
import {
decorate,
observable,
computed,
autorun,
action,
transaction // *** transaction 불러옴
} from 'mobx';
class GS25 {
basket = [];
get total() {
console.log('계산중입니다..!');
// Reduce 함수로 배열 내부의 객체의 price 총합 계산
// https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
return this.basket.reduce((prev, curr) => prev + curr.price, 0);
}
select(name, price) {
this.basket.push({ name, price });
}
}
decorate(GS25, {
basket: observable,
total: computed,
select: action
});
const gs25 = new GS25();
autorun(() => gs25.total);
// *** 새 데이터 추가 될 때 알림
autorun(() => {
if (gs25.basket.length > 0) {
console.log(gs25.basket[gs25.basket.length - 1]);
}
});
gs25.select('물', 800);
gs25.select('물', 800);
gs25.select('포카칩', 1500);
console.log(gs25.total);
계산중입니다..!
계산중입니다..!
Object {name: "물", price: 800}
계산중입니다..!
Object {name: "물", price: 800}
계산중입니다..!
Object {name: "포카칩", price: 1500}
3100
계산의 경우, 가장 처음 한번 호출이 되고, 데이터가 추가 될 때마다 계산이 되고 있습니다. 한번 이걸 transaction 으로 감싸보겠습니다.
import {
decorate,
observable,
computed,
autorun,
action,
transaction
} from 'mobx';
class GS25 {
basket = [];
get total() {
console.log('계산중입니다..!');
// Reduce 함수로 배열 내부의 객체의 price 총합 계산
// https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
return this.basket.reduce((prev, curr) => prev + curr.price, 0);
}
select(name, price) {
this.basket.push({ name, price });
}
}
decorate(GS25, {
basket: observable,
total: computed,
select: action
});
const gs25 = new GS25();
autorun(() => gs25.total);
// 새 데이터 추가 될 때 알림
autorun(() => {
if (gs25.basket.length > 0) {
console.log(gs25.basket[gs25.basket.length - 1]);
}
});
transaction(() => {
gs25.select('물', 800);
gs25.select('물', 800);
gs25.select('포카칩', 1500);
})
console.log(gs25.total);
계산중입니다..!
계산중입니다..!
Object {name: "포카칩", price: 1500}
3100
transaction 을 통하여 계산 작업은 가장 처음 한번, 그리고 transaction 끝나고 한번 호출이 되었고, 새 데이터 추가 될 때마다 알리는 부분도 3개를 다 추가하고 나서야 딱 한번 콘솔에 마지막 객체가 나타났습니다.
액션을 사용하면, 이렇게 성능 개선도 이뤄낼 수 있고 나중에 MobX 개발자 도구를 사용하게 될 때 변화에 대한 더 자세한 정보를 알 수 있게 해줍니다.
decorator 문법으로 더 편하게!
decorator 문법은 일종의, 자바스크립트 사투리 라고 생각하시면됩니다. 정규 문법은 아니지만, babel 플러그인을 통하여 사용 할 수 있는 문법입니다. 이 문법을 사용하면 decorate 함수가 더 이상 필요하지 않고, 다음과 같이 작성 해 줄 수 있답니다.
import { observable, computed, autorun, action, transaction } from 'mobx';
class GS25 {
@observable basket = [];
@computed
get total() {
console.log('계산중입니다..!');
// Reduce 함수로 배열 내부의 객체의 price 총합 계산
// https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
return this.basket.reduce((prev, curr) => prev + curr.price, 0);
}
@action
select(name, price) {
this.basket.push({ name, price });
}
}
const gs25 = new GS25();
autorun(() => gs25.total);
// 새 데이터 추가 될 때 알림
autorun(() => {
if (gs25.basket.length > 0) {
console.log(gs25.basket[gs25.basket.length - 1]);
}
});
transaction(() => {
gs25.select('물', 800);
gs25.select('물', 800);
gs25.select('포카칩', 1500);
});
console.log(gs25.total);
결과는 아까와 똑같습니다.
MobX 를 사용하는 대부분의 예제는 이 Decorator 를 사용합니다. 아무래도, 사용하는 편이 훨씬 편하긴 하지만 없이도 사용 하는 것에는 지장이 안갑니다 (참고).