MobX

MobX

  • API 참고서
  • 영어 문서
  • 후원자
  • GitHub

›Tips & Tricks

Introduction

  • MobX에 대하여
  • MobX 문서에 대하여
  • 설치 방법
  • MobX의 요지

MobX core

  • Observable state
  • Actions
  • Computeds
  • Reactions {🚀}

MobX and React

  • React 통합
  • React 최적화 {🚀}

Tips & Tricks

  • 데이터 스토어 정의
  • 반응성 이해하기
  • 서브클래싱
  • 반응 분석하기 {🚀}
  • 인수가 필요한 computed {🚀}
  • MobX-utils {🚀}
  • Custom observables {🚀}
  • Lazy observables {🚀}
  • Collection utilities {🚀}
  • Intercept & Observe {🚀}

Fine-tuning

  • 환경설정 {🚀}
  • 데코레이터 사용하기 {🚀}
  • MobX 4 또는 5에서 마이그레이션 하기 {🚀}
Edit

반응성 이해하기

MobX는 일반적으로 사용자가 기대하는 사항에 정확히 반응합니다. 즉, 사용 사례의 90% 정도는 MobX가 "바로 작동"해야 합니다. 그러나 어느 순간 예상했던 대로 되지 않는 경우가 발생할 수 있습니다. 그 시점에서 MobX가 무엇에 반응할지를 어떻게 결정하는지 이해하는 것이 중요합니다.

MobX는 추적된 함수의 실행 과정에서 읽히는 기존의 observable 속성에 반응합니다.

  • "읽는다"란 점 표기법(예 : user.name) 또는 괄호 표기법(예 : user['name'], todos[3])을 통해 객체 속성을 역참조하는 것입니다.
  • "추적된 함수"란 computed, observer인 React 함수 컴포넌트의 렌더링, React 클래스 컴포넌트 기반 observer에서의 render() 메서드 및 autorun, reaction, when의 첫 번째 파라미터로 전달되는 함수를 말합니다.
  • "과정에서"란 함수가 실행되는 동안 읽히는 observable만이 추적됨을 의미합니다. 해당 값이 추적된 함수에 의해 직접적으로 사용되는지 간접적으로 사용되는지는 중요하지 않습니다. 그러나 함수에서 '스폰(spawn)'된 항목은 추적되지 않습니다.(예 : setTimeout, promise.then, await 등)

다르게 말하면, MobX는 다음과 같은 항목에는 반응하지 않습니다.

  • observable로부터 얻어졌지만 추적된 함수 외부에 있는 값
  • 비동기적으로 호출된 코드 블록에서 읽어진 observable

MobX는 값이 아닌 속성 액세스를 추적합니다

위의 규칙을 예제로 자세히 설명하기 위해 다음과 같은 observable 인스턴스가 있다고 가정합니다.

class Message {
    title
    author
    likes
    constructor(title, author, likes) {
        makeAutoObservable(this)
        this.title = title
        this.author = author
        this.likes = likes
    }

    updateTitle(title) {
        this.title = title
    }
}

let message = new Message("Foo", { name: "Michel" }, ["Joe", "Sara"])

위 코드는 다음과 같이 표현될 수 있습니다. 녹색 상자는 observable 속성을 나타냅니다. 값 자체는 observable이 아닙니다!

MobX reacts to changing references

MobX가 기본적으로 하는 일은 함수에서 사용하는 화살표를 기록하는 것입니다. 이후에는 이러한 화살표 중 하나가 변경될 때(다른 항목을 참조하기 시작할 때)마다 재실행합니다.

예시

위에서 정의한 message 변수를 기반으로 여러 가지 예제를 통해 확인해봅시다.

옳은 예: 추적된 함수 내에서의 역참조

autorun(() => {
    console.log(message.title)
})
message.updateTitle("Bar")

위 코드는 예상대로 반응합니다. .title 속성은 autorun에서 역참조된 후 변경되었으므로 해당 변경 내용이 감지됩니다.

추적된 함수 내에서 trace()를 호출하면 MobX가 무엇을 추적하는지 확인할 수 있습니다. 위 함수의 경우 다음을 출력합니다.

import { trace } from "mobx"

const disposer = autorun(() => {
    console.log(message.title)
    trace()
})
// 출력 값:
// [mobx.trace] 'Autorun@2' tracing enabled

message.updateTitle("Hello")
// 출력 값:
// [mobx.trace] 'Autorun@2' is invalidated due to a change in: 'Message@1.title'
Hello

getDependencyTree를 사용하여 내부 종속성(또는 observer) 트리를 가져올 수도 있습니다.

import { getDependencyTree } from "mobx"

// disposer에 복제된 reaction의 종속성 트리(dependency tree)를 출력합니다.
console.log(getDependencyTree(disposer))
// 출력 값:
// { name: 'Autorun@2', dependencies: [ { name: 'Message@1.title' } ] }

옳지 않은 예: observable 속성이 아닌 참조의 변경

autorun(() => {
    console.log(message.title)
})
message = new Message("Bar", { name: "Martijn" }, ["Felicia", "Marcus"])

위 코드는 반응하지 않습니다. message가 변경되었지만 message는 observable 변수가 아니라 observable을 참조하는 변수일 뿐이며 변수(참조) 자체는 observable이 아니기 때문입니다.

옳지 않은 예: 추적된 함수 외부에서의 역참조

let title = message.title
autorun(() => {
    console.log(title)
})
message.updateTitle("Bar")

위 코드는 반응하지 않습니다. message.title은 autorun 밖에서 역참조되었으며, 역참조하는 순간에 message.title의 값(문자열 "Foo")만을 담고 있기 때문입니다. title은 observable이 아니므로 autorun에서 절대 반응하지 않습니다.

옳은 예: 추적된 함수 내에서의 역참조

autorun(() => {
    console.log(message.author.name)
})

runInAction(() => {
    message.author.name = "Sara"
})
runInAction(() => {
    message.author = { name: "Joe" }
})

위 코드는 두 가지 변화에 모두 반응합니다. 점 표기법으로 author와 author.name에 모두 접근했으므로 MobX가 해당 참조를 추적할 수 있습니다.

또한 action 외부에서의 변경을 허용하기 위해 runInAction을 사용했습니다.

옳지 않은 예: 추적 없이 observable 객체에 로컬 참조 저장하기

const author = message.author
autorun(() => {
    console.log(author.name)
})

runInAction(() => {
    message.author.name = "Sara"
})
runInAction(() => {
    message.author = { name: "Joe" }
})

message.author와 author가 동일한 객체이고 .name 속성은 autorun에서 역참조되었기 때문에 첫 번째 변경사항은 감지됩니다. 하지만 autorun에서 message.author 관계를 추적하지 않으므로 두 번째 변경사항은 감지되지 않습니다. autorun은 여전히 "이전의" author를 사용하고 있습니다.

흔한 함정: console.log

autorun(() => {
    console.log(message)
})

// 다시 트리거 되지 않습니다.
message.updateTitle("Hello world")

위의 예에서 업데이트된 message title은 autorun 내에서 사용되지 않기 때문에 출력되지 않습니다. autorun은 observable이 아닌 변수 message에만 의존합니다. 다르게 말하면, MobX는 autorun에서 title이 사용되지 않았다고 인식합니다.

웹브라우저 디버깅 도구에서 위와 같이 사용하면 결국 title의 업데이트된 값을 발견할 수 있겠지만 이는 오해의 소지가 있습니다. autorun은 처음 호출될 때 한 번 실행됩니다. title의 업데이트 된 값을 발견할 수 있는 문제는 console.log가 비동기 함수이고 객체가 나중에 포맷되기 때문에 발생합니다. 즉, 디버깅 툴바에서 title을 따라가면 업데이트된 값을 찾을 수 있습니다. 그러나 autorun은 업데이트를 추적하지 않습니다.

위 작업을 수행하는 방법은 불변 데이터(immutable data) 또는 방어적 복사본(defensive copy)을 항상 console.log에 전달하는 것입니다. 따라서 다음 방법들은 모두 message.title의 변경사항에 반응합니다.

autorun(() => {
    console.log(message.title) // `.title` observable이 명확히 사용되었습니다.
})

autorun(() => {
    console.log(mobx.toJS(message)) // toJS가 깊은(deep) 클론을 생성하기 때문에 message를 읽을 수 있습니다.
})

autorun(() => {
    console.log({ ...message }) // 프로세스의 `.title`을 사용하여 얕은(shallow) 클론을 생성합니다.
})

autorun(() => {
    console.log(JSON.stringify(message)) // 전체 구조를 읽습니다.
})

옳은 예: 추적된 함수의 배열 속성에 액세스하기

autorun(() => {
    console.log(message.likes.length)
})
message.likes.push("Jennifer")

위 코드는 예상대로 반응합니다. .length는 속성의 요소를 카운트합니다. 이 방법은 배열의 어떠한 변화에도 반응합니다. 배열은 observable 객체와 map처럼 인덱스∙속성별로 추적되는 것이 아니라 전체로서 추적됩니다.

옳지 않은 예: 추적된 함수의 범위를 벗어난 인덱스에 액세스하기

autorun(() => {
    console.log(message.likes[0])
})
message.likes.push("Jennifer")

배열 인덱스는 속성 액세스로 계산되기 때문에 위의 샘플 데이터와 반응합니다. 그러나 오직 제공된 index가 length보다 작은(index < length) 경우에만 해당됩니다. MobX는 아직 존재하지 않는 배열 인덱스를 추적하지 않습니다. 따라서 배열 인덱스를 기반으로 액세스하는 경우 항상 .length를 확인하세요.

옳은 예: 추적된 함수의 배열 함수에 액세스하기

autorun(() => {
    console.log(message.likes.join(", "))
})
message.likes.push("Jennifer")

위 코드는 예상대로 반응합니다. 배열을 변경하지 않는 모든 배열 함수는 자동으로 추적됩니다.


autorun(() => {
    console.log(message.likes.join(", "))
})
message.likes[2] = "Jennifer"

위 코드는 예상대로 반응합니다. index <= length인 경우에만 모든 배열 인덱스 할당이 감지됩니다.

옳지 않은 예: observable의 어떠한 속성에도 액세스하지 않고 "사용"하기

autorun(() => {
    message.likes
})
message.likes.push("Jennifer")

위 코드는 예상대로 반응하지 않습니다. 단순히 likes 배열 자체가 autorun에서 사용되지 않고 배열에 대한 참조만 사용되고 있기 때문입니다. 반대로 message.likes = ["Jennifer"]는 잘 반응할 것입니다. 해당 문은 배열을 수정하는 것이 아니라 likes 속성 자체를 수정하기 때문입니다.

옳은 예: 아직 존재하지 않는 map 엔트리 사용하기

const twitterUrls = observable.map({
    Joe: "twitter.com/joey"
})

autorun(() => {
    console.log(twitterUrls.get("Sara"))
})

runInAction(() => {
    twitterUrls.set("Sara", "twitter.com/horsejs")
})

위 코드는 반응할 것입니다. observable map은 존재하지 않을 수 있는 엔트리를 관찰하도록 도와줍니다. 처음에는 undefined가 출력됩니다. twitterUrls.has("Sara")를 사용하면 엔트리의 존재 여부를 먼저 확인할 수 있습니다. 따라서 동적 키 수집에 대한 프록시 지원이 없는 환경에서는 항상 observable map을 사용하세요. 프록시 지원이 있는 경우 observable map도 사용할 수 있지만 plain 객체를 사용할 수도 있습니다.

MobX는 비동기적으로 액세스된 데이터를 추적하지 않습니다

function upperCaseAuthorName(author) {
    const baseName = author.name
    return baseName.toUpperCase()
}
autorun(() => {
    console.log(upperCaseAuthorName(message.author))
})

runInAction(() => {
    message.author.name = "Chesterton"
})

위 코드는 반응합니다. author.name이 autorun에 전달된 함수에서 자체적으로 역참조되지는 않았지만, MobX는 upperCaseAuthorName에서 발생하는 역참조를 추적할 것입니다. 해당 역참조는 autorun이 실행되는 동안 발생하기 때문입니다.


autorun(() => {
    setTimeout(() => console.log(message.likes.join(", ")), 10)
})

runInAction(() => {
    message.likes.push("Jennifer")
})

위 코드는 반응하지 않습니다. autorun 실행 중에는 어떠한 observable도 액세스 되지 않으며, 비동기 함수인 setTimeout을 실행하는 동안에만 액세스 되기 때문입니다.

비동기 action 섹션을 함께 확인해보세요.

observable이 아닌 객체 속성 사용하기

autorun(() => {
    console.log(message.author.age)
})

runInAction(() => {
    message.author.age = 10
})

위 코드는 프록시를 지원하는 환경에서 React를 실행하는 경우 반응합니다. 이 작업은 observable 또는 observable.object로 생성된 객체에 대해서만 수행됩니다. 클래스 인스턴스의 새 속성은 자동으로 observable이 되지 않습니다.

프록시를 지원하지 않는 환경

위 코드는 반응하지 않습니다. MobX는 observable 속성만 추적할 수 있으며 'age'는 위에서 observable 속성으로 정의되지 않았습니다.

그러나 MobX에서 지원하는 get 및 set 메서드를 사용하면 다음과 같은 작업을 수행할 수 있습니다.

import { get, set } from "mobx"

autorun(() => {
    console.log(get(message.author, "age"))
})
set(message.author, "age", 10)

[프록시 지원이 없을 때] 옳지 않은 예: 아직 존재하지 않는 observable 객체 속성 사용하기

autorun(() => {
    console.log(message.author.age)
})
extendObservable(message.author, {
    age: 10
})

위 코드는 반응하지 않습니다. MobX는 추적이 시작될 때 존재하지 않았던 observable 속성에 반응하지 않습니다. 두 문장이 바뀌거나 다른 observable로 인해 autorun이 재실행되면 autorun에서 age를 추적하기 시작합니다.

[프록시 지원이 없을 때] 옳은 예: MobX 유틸리티를 사용해서 객체를 읽거나(read) 쓰기(write)

프록시 지원이 없는 환경에서 observable 객체를 동적 컬렉션(dynamic collection)으로 사용하려면 MobX의 get 및 set API를 사용하세요.

다음 코드도 반응합니다.

import { get, set, observable } from "mobx"

const twitterUrls = observable.object({
    Joe: "twitter.com/joey"
})

autorun(() => {
    console.log(get(twitterUrls, "Sara")) // `get` can track not yet existing properties.
})

runInAction(() => {
    set(twitterUrls, { Sara: "twitter.com/horsejs" })
})

더 자세한 내용은 Collection utilities API에서 확인하세요.

요약

MobX는 추적된 함수의 실행 과정에서 읽히는 기존의 observable 속성에 반응합니다.

← 데이터 스토어 정의서브클래싱 →
  • MobX는 값이 아닌 속성 액세스를 추적합니다
  • 예시
MobX
Docs
About MobXThe gist of MobX
Community
GitHub discussions (NEW)Stack Overflow
More
Star