티스토리 뷰

이 글은 Mobx 공식문서를 토대로 재해석한 글입니다.

서론

Mobx의 철학은 "애플리케이션의 상태로 부터 파생될수 있는 모든것들은 파생되어야한다" 입니다. 

 

Mobx의 동작 흐름

 

액션

이벤트는 액션을 일으킵니다.

애플리케이션의 state는 오로지 action을 통해서만 변경해야합니다. 

action은 state를 변경할뿐 아니라, 때로는 Side Effect를 일으킵니다. 

Side Effect

프로그래밍에서 사이드 이펙트란 함수 내부에서 함수 외부에 접근하여 영향을 주는 행위를 의미합니다. 예를들어 함수 내부에서 Disk I/O가 일어난다던지 네트워크 요청을 한다든지 하는것은 영향 범위가 함수 내부를 벗어났으므로 Side Effect가 발생한것입니다.

 

 

Mobx의 동작 흐름

 

Mobx가 관리하는 State는 Observable(관찰가능한)하며 최소한으로 정의되어야합니다. Observable하다는것은 누군가 Stae를 변경했을때 State를 감시하고 있던 Observer가 그 State의 변경사항을 알아차릴수 있다는 것입니다.

 

스타크래프트의 옵저버

 

게임 스타크래프트에서는 이렇게 생긴 옵저버가 있습니다. 이 옵저버를 적진으로 보내서 감시하는 역할을하죠. Mobx에서의 Observer는 적진을 감시하는 대신 State의 변화를 감시합니다. 코드는 아래와 같이 생겼습니다.

@observable simsimjae  = [{
  title: "Mobx를 배우자",
  name: '심재철'
}]

@는 decorator라고 하는 실험적 문법입니다. (아직 정식 자바스크립트 스펙이 아닙니다.) 위 코드가 바벨로 변환 되면 아래와 같이 변경됩니다.

simsimjae  = observable([{
  title: "Mobx를 배우자",
  name: '심재철'
}]);

그냥 함수로 감싸주는건데 @ decorator를 사용하면 좀 더 직관적이어서 좋아하시는분들이 많습니다.

 

이 State에는 그래프, 클래스, 배열, refs 등등의 수많은 자료구조들이 전부 들어갈 수 있습니다.

Computed Values(계산된 값)

computed values는 state의 변화로 인해 계산된 값입니다. computed는 일종의 캐싱입니다. computed 내부에서 사용하는 state가 변경되었을때만 새로 계산해서 계산값을 저장해놓고 사용합니다. 만약에 computed내부 state가 변경되지 않았으면 기존에 계산해놨던 캐싱값을 그대로 다시 사용합니다.

@computed simsimjae = () => {
	return state.a + state.b;
}

이경우 state의 a 혹은 b가 변경되었을때만 값이 재계산됩니다. 그리고 변경된 이후에 다시 simsimjae()를 해서 값을 얻어온다고 하더라도 그 값은 캐싱된 값입니다. state가 변했을때 다시 simsimjae()를 호출해서 캐싱값을 수동으로 변경할 필요가 없습니다. Mobx가 자동으로 재계산해줍니다.

 

만약 computed value가 그 어디에서도 사용되지 않는다면 Mobx가 자동으로 garbage collecting을 해줍니다.

Computed를 사용하는 방법

import { observable, computed } from "mobx"

class OrderLine {
    @observable price = 0
    @observable amount = 1

    constructor(price) {
        this.price = price
    }

    @computed get total() {
        return this.price * this.amount
    }
}

데코레이터를 사용하면 위와 같이 사용할 수 있습니다. price, amount 두가지 observable state가 변경될 경우 total값이 재 계산되어 재 캐싱됩니다.

import { decorate, observable, computed } from "mobx"

class OrderLine {
    price = 0
    amount = 1

    constructor(price) {
        this.price = price
    }

    get total() {
        return this.price * this.amount
    }
}
decorate(OrderLine, {
    price: observable,
    amount: observable,
    total: computed
})

데코레이터를 사용하고 싶지 않다면 이렇게 사용하시면 됩니다.

const orderLine = observable.object({
    price: 0,
    amount: 1,
    get total() {
        return this.price * this.amount
    }
})

만약에 클래스를 사용하지 않고 자바스크립트의 plain object를 사용하고 싶으면 위와 같이 하면 됩니다. 주목할만한 점은, get을 함수 앞에 붙여주면 Mobx가 자동으로 computed 속성으로 추론합니다. 어차피 값을 가져오기 위한 용도의 함수니까 미리 캐싱을 해두는거죠.

Computed values는 getters가 아닙니다.

사전지식. Reaction이란?

observable state를 변경하면 그에 따른 파생값(computed)이 계산됩니다. 만약에 이 파생값들을 사용하고 있는 곳이 있다면? 거기도 자동으로 변경되어야겠죠? 그걸 Reaction이라고 부릅니다. 예를들어, 파생값을 사용하는 컴포넌트를 다시 렌더링한다거나 그 파생값을 로그로 남긴다거나 하는 작업들이 Reaction이죠.

 

computed values는 reaction에 의해서 사용될때만 캐싱이 됩니다. 일반적인 상황에서는 캐싱이 되지 않습니다.

const orderLine = new OrderLine(2.00)

setInterval(() => {
  console.log(orderLine.total);
}, 60);

이렇게 reaction이 아닌곳에서 computed properties였던 total에 접근하게 되면 캐싱된 값이 사용되는게 아니라 매번 접근할때마다 함수가 실행되어 재계산됩니다. (성능상 이슈) 만약에 computed 내부가 굉장히 복잡하다면 심각한 성능 하락으로 이어질 수 있습니다. 그래서 Mobx의 computedRequiresReaction 옵션을 켜서 Reaction이 아닌곳에서 직접적으로 computed에 접근할 때 에러를 내게 하는것이 좋습니다.

configure({
    computedRequiresReaction: true
})

즉 Mobx의 자연스러운 데이터 흐름일때만,

Observable State의 변경 -> Computed values 계산 혹은 캐싱 -> Reaction

Reaction에서 사용된 computed values만 정상 동작한다는것입니다.

 

이 Reaction은 어떤 Input이 들어왔을때 Output을 내는게 아닌 함수 외부에 영향을 주는 행위 이므로 항상 Side Effect입니다.

Reaction의 또 다른 형태, autorun

아까 위에서 살펴봤던 주문라인 예제를 다시 한번 변형해보았습니다. autorun이라는것은 이름에서도 추론할 수 있다시피, computed values가 변경되었을때 자동으로 동작하는 reaction입니다. computed values가 변경될 때마다 autorun에 전달한 콜백함수가 실행되죠.

class OrderLine {
    @observable price = 0
    @observable amount = 1

    constructor(price) {
        this.price = price
        
        // 2. 재 계산된 computedTotal에 의해 Reaction이 발생하여 total이 업데이트 된다.
        autorun(() => {
            this.total = this.computedTotal
        })
    }
	
    // 1. price와 amount의 변화에 의해 computedTotal이 재 계산된다.
    @computed get computedTotal() {
        return this.price * this.amount
    }
}

const orderLine = new OrderLine(2.0)

setInterval(() => {
    console.log(orderLine.total)  // 3. 이제 이렇게 직접적으로 접근해도 정상 동작한다.
}, 60)

 

이제 이렇게 직접적으로 total에 접근해도 정상적으로 캐싱됩니다. 왜냐면, total은 computedTotal인데 computedTotal은 reaction인 autorun에서 접근한 computed values이기 떄문입니다.

Computed KeepAlive 옵션

KeepAlive는 직역해보면 살아있게 유지한다라는 뜻입니다. 이 옵션은 computed 속성이 항상 reaction에 의해서 읽혀서 캐싱되도록 해주는 옵션입니다.

class OrderLine {
    @observable price = 0
    @observable amount = 1

    constructor(price) {
        this.price = price
    }

    @computed({ keepAlive: true })
    get total() {
        return this.price * this.amount
    }
}

이렇게 keepAlive옵션을 true로 주면 이제 total값은 reaction이 아닌곳에서 읽혀도 reaction에 의해 읽힌것처럼 됩니다. 원리는 위에서 들었던 예제에서 computedValues를 autorun으로 감싼것입니다.

 

그래서, 아까 아래와 같은 경우에 캐싱이 안되서 주의하라고 말씀드렸던 부분에서도 keepAlive옵션이 켜져있으면 정상 동작합니다.

const orderLine = new OrderLine(2.00)

setInterval(() => {
  // reaction이 아닌곳에서 읽힌 computed values도 캐싱된다.
  // 왜냐면, computed values인 total이 keep Alive이므로
  console.log(orderLine.total); 
}, 60);

결론은, computedRequiresReaction config를 설정하거나 keepAlive옵션을 사용하세요.

Computed Values에 대한 Setter

class Foo {
    @observable length = 2
    @computed get squared() {
        return this.length * this.length
    }
    set squared(value) {
    	// this.squared = 123; 이런식으로 직접적으로 computed를 바꿀 수 없습니다.
        this.length = Math.sqrt(value) // 일종의 액션입니다.
    }
}

computed values에 대한 setter는 computed values를 직접적으로 수정하지 않습니다. 대신, setter내부에서 observable state를 변경하고 그 변경된 state에 의한 computed values 재계산을 일으키는거죠. 직접적으로 못하니까 돌아가는 느낌입니다.

Computed를 함수로 사용하기

import { observable, computed } from "mobx"

// "simjae"라는 primitive value(string)을 observable state로 선언
var name = observable.box("simjae") 

// observable box인 name에 있는 string을 대문자로 변경하고 캐싱.
var upperCaseName = computed(() => name.get().toUpperCase())

// observable box인 name이 변경되면 콘솔에 변경된 값을 찍음.
// 물론 기본적으로 computed는 재계산됨.
var disposer = upperCaseName.observe(change => console.log(change.newValue))

// simsimjae가 콘솔에 출력됨.
name.set("simsimjae")

computed를 이런식으로도 사용할 수 있지만 흔한 방법은 아닙니다. boxed된 computed value가 필요할때 사용하면 유용합니다.

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함