본문 바로가기

React/상태관리하기

MobX 톺아보기 3 - MobX core - Computed

MobX 공식 문서의 MobX core Computed를 톺아보자.

 


[ computed 된 정보 도출하기 ]

 

사용법 :

computed (annotation)
computed(options) (annotation)
computed(fn, options?)

 

computed 값은 다른 observable들에서 정보를 도출하는 데 사용할 수 있다. 느리게 평가하여 출력을 캐싱하고 기본 observable 변수 중 하나가 변경된 경우에만 다시 computed 된다. 아무것도 관찰되지 않으면 완전히 중단된다.

 

개념적으로는 스프레드시트의 수식과 매우 유사하며 과소평가해서는 안된다. 저장해야 하는 상태의 양을 줄이는 데 도움이 되며 고도로 최적화되어 있다. 가능하면 수식을 사용하자.

 

예제

computed 값은 자바스크립트 getter에 computed를 어노테이션으로 달아 생성할 수 있다. getter를 computed로 선언하려면 makeObservable을 사용한다. 대신 모든 getter가 자동으로 계산되도록 하려면 makeAutoObservable, observable 또는 extendObservable을 사용할 수 있다. computed getter는 나열할 수 없다.

 

computed 값의 요점을 설명하기 위해 아래 예시는 Reaction 고급 섹션의 autorun을 사용한다.

 

import { makeObservable, observable, computed, autorun } from "mobx"

class OrderLine {
    price = 0
    amount = 1

    constructor(price) {
        makeObservable(this, {
            price: observable,
            amount: observable,
            total: computed
        })
        this.price = price
    }

    get total() {
        console.log("Computing...")
        return this.price * this.amount
    }
}

const order = new OrderLine(0)

const stop = autorun(() => {
    console.log("Total: " + order.total)
})
// Computing...
// Total: 0

console.log(order.total)
// (No recomputing!)
// 0

order.amount = 5
// Computing...
// (No autorun)

order.price = 2
// Computing...
// Total: 10

stop()

order.price = 3
// Neither the computation nor autorun will be recomputed.

 

위의 예시는 computed 값이 캐싱 포인트 역할을 하는 이점을 잘 보여준다. price를 변경하면 amout가 다시 계산되도록 트리거 되지만 amout가 출력에 영향을 받지 않은 것으로 감지하므로 autorun이 트리거 되지 않으므로 autorun을 업데이트할 필요가 없다.

 

이에 비해 total에 어노테이션 하지 않으면 autorun이 price와 amount에 직접적으로 의존하기 때문에 autorun이 3번 실행된다.

 

위의 예시에서 생성되는 종속성 그래프이다.

 

 

 

규칙

 

계산된 값을 사용할 때 따라야 할 몇 가지 모범 사례가 있다.

  • 부작용을 일으키거나 다른 observable을 업데이트하지 않아야 한다.
  • 새로운 observable을 생성하고 반환하면 안 된다.
  • 관찰할 수 없는 값에 의존해서는 안된다.

 

 

Tip

 

  • 계산된 값은 관찰되지 않으면 일시 중단된다.

computed 프로퍼티를 생성했지만 Reaction의 어느 곳에서도 사용하지 않으면 메모리에 저장되지 않고 필요 이상으로 자주 재계산된 것처럼 보이기 때문에 Reselect와 같은 라이브러리에 익숙한 MobX를 처음 사용하는 사람들은 가끔 혼동할 수 있다. 예를 들어, 위의 예제에서 console.log(order.total)을 두 번 호출하는 것으로 확장하면 stop()를 호출한 후 값이 두 번 다시 계산된다.

 

이를 통해 MobX는 활발하게 사용되지 않는 computed를 자동으로 일시 중단하여 액세스 되지 않는 계산된 값에 대한 불필요한 업데이트를 방지할 수 있다. 그러나 computed 프로퍼티가 어떤 Reaction에 의해 사용되지 않는 경우, 값이 요청될 때마다 계산된 표현식이 평가되므로 일반 프로퍼티처럼 작동한다.

 

computed 프로퍼티만 만지작거리면 효율적이지 않아 보일 수 있지만 observer, autorun 등을 사용하는 프로젝트에 적용하면 매우 효율적이다.

 

다음 코드는 해당 문제를 나타낸다.

 

// OrderLine에 computed property `total`이 있다.
const line = new OrderLine(2.0)

// Reation 외부에서 `line.total`에 액세스하면 매번 다시 계산된다.
setInterval(() => {
    console.log(line.total)
}, 60)

 

keepAlive 옵션으로 어노테이션을 설정하거나, 필요하면 나중에 깔끔하게 정리할 수 있는 autorun(() => { someObject.someComputed })를 생성하여 재정의할 수 있다. 두 해결책 모두 메모리 누수를 일으킬 위험이 있다. 여기서 기본 동작을 변경하는 것은 패턴을 방지하는 것이다.

 

또한 reactive context 외부에서 계산된 값에 액세스 할 때 오류를 보고하도록 computedRequiresReaction 옵션으로 MobX를 구성할 수도 있다.

 

 

  • 계산된 값에는 setter가 있을 수 있다.

계산된 값에 대한 setter도 정의할 수 있다. 이러한 setter는 computed 프로퍼티의 값을 직접 변경하는 데 사용할 수는 없지만 파생의 "inverse"로 사용할 수 있다. setter는 자동으로 액션으로 표시된다.

 

class Dimension {
    length = 2

    constructor() {
        makeAutoObservable(this)
    }

    get squared() {
        return this.length * this.length
    }
    set squared(value) {
        this.length = Math.sqrt(value)
    }
}

 

 

  • output을 구조적으로 비교하기 위한 computed.struct

이전 계산과 구조적으로 동일한 계산된 값의 출력이 관찰자에게 알릴 필요가 없는 경우, computed.struct를 사용할 수 있다. 이 함수는 관찰자에게 알리기 전에 참조 동일성  검사 대신 구조적 비교를 먼저 수행한다.

 

class Box {
    width = 0
    height = 0

    constructor() {
        makeObservable(this, {
            width: observable,
            height: observable,
            topRight: computed.struct
        })
    }

    get topRight() {
        return {
            x: this.width,
            y: this.height
        }
    }
}

 

기본적으로 computed의 출력은 참조로 비교된다. 위 예시에서 topRight는 항상 새로운 결과 객체를 생성하므로 이전 출력과 동일한 것으로 간주되지 않는다. computed.struct가 사용되지 않는 한 말이다.

 

하지만 위의 예시에서는 실제로 computed.struct가 필요하지 않다. 계산된 값은 일반적으로 뒷값이 변경될 때만 다시 평가한다. 그렇기 때문에 topRight는 너비나 높이가가 변경될 때만 반응한다. 이 중 하나라도 변경되면 어차피 다른 topRight 좌표를 얻게 되므로 computed.struct는 캐시에 영향을 미치지 않고 노력 낭비가 될 수 있으므로 필요하지 않다.

 

실제로 computed.struct는 생각보다 유용하지 않다. 기본 observer들을 변경해도 여전히 동일한 출력을 얻을 수 있는 경우에만 사용하자. 예를 들어 좌표를 먼저 반올림하여 기본값이 반올림되지 않더라도 반올림된 좌표가 이전에 반올림된 좌표와 같을 수 있다.

 

 

 

  • computed를 사용하여 독립형 계산된 값 만들기

computed를 함수로 직접 호출할 수 있다. observable.box가 독립 실행형 계산된 값을 생성하는 것처럼 말이다. 반환된 객체에서 .get()을 사용하여 계산의 현재 값을 가져온다. 이 형식의 computed는 자주 사용되지는 않지만 박스형 계산된 값을 전달해야 하는 경우에 유용할 수 있다.

 

 

 

 

options

computed는 일반적으로 원하는 방식으로 작동하지만 options 인수를 전달하여 동작을 사용자 지정할 수 있다.

 

 

  • name

이 문자열은 Spy event listener와 Mox developer tools에서 디버그 이름으로 사용된다.

 

 

  • equals

기본적으로 comparer.default로 설정된다. 이전 값과 다음 값을 비교하는 비교 함수 역할을 한다. 이 함수가 값이 같다고 간주하면 관찰자를 다시 평가하지 않는다.

 

이 함수는 다른 라이브러리의 구조 데이터 및 타입으로 작업할 때 유용하다. 예를 들어, 계산된 moment 인스턴스에서는

(a, b) => a.isSame(b)를 사용할 수 있다. 구조적/얕은 비교를 사용하여 새 값이 이전값과 다른지 여부를 결정하고 그 결과를 관찰자에게 알리려는 경우 comparer.srtuctual 및 comparer.shallow가 유용하게 사용된다.

 

 

built-in comparers

 

MobX는 computed 옵션의 equals 옵션에 대한 대부분의 요구사항을 충족하는 네 가지 내장 comparer 메서드를 제공한다.

  • comparer.identity : === 연산자를 사용해 두 값이 같은지 판단한다.
  • comparer.default : comparer.identity와 동일하지만 NaN을 NaN과 같은 것으로 간주한다.
  • comparer.structural : 심층 구조 비교를 수행하여 두 값이 동일한지 확인한다.
  • comparer.shallow : 얕은 구조 비교를 수행하여 두 값이 동일한지 확인한다.

이러한 메서드에 액세스 하기 위해 mobX에서 comparer를 가져올 수 있다. 이 메서드들은 reaction에도 사용이 가능하다.

 

 

  • requiresReaction

매우 복잡한 계산 값에 대해 이 값을 true로 설정하는 것이 좋다. 캐시 되지 않은 reactive context 외부에서 값을 읽으려고 하면 복잡한 재평가를 수행하는 대신 계산된 값이 던져진다.

 

 

  • keepAlive

이렇게 하면 아무것도 관찰하지 않을 때 계산된 값이 일시 중단하는 것을 방지할 수 있다. reaction에 대해 설명한 것과 유사한 메모리 누수가 발생할 수 있다.