MobX에서 observable, action 만들기

yejineee·2021년 3월 2일
5

MobX

목록 보기
2/2

MobX는 구조를 만드는 방법이 하나만 존재하는 것이 아니라, 여러 방법을 제시하고 있다.

여기서는 observable, action을 만드는 여러 방법에 대해 알아볼 것이다.

🍎 Observable State

Observable은 makeObservable 을 사용하여 observable이라고 표기해야 한다. observable을 만드는 방법은 세 가지가 있다.
1. makeObservable
2. makeAutoObservable
3. observable

  • makeObservable

    makeObservable을 사용하여 하나씩 notation하는 방법

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

class Doubler {
    value // observable

    constructor(value) {
        makeObservable(this, {
            value: observable,
            double: computed,
            increment: action,
            fetch: flow
        })
        this.value = value
    }

    get double() { // copmuted
        return this.value * 2
    }

    increment() { // action
        this.value++
    }

    *fetch() { // flow
        const response = yield fetch("/api/value")
        this.value = response.json()
    }
}
  • makeAutoObservable

    makeAutoObservable은 모든 프로퍼티들을 추론하여 action, computed, observable 등을 정한다.

    makeAutoObservable을 사용하면, 코드가 더 짧아질 수 있다. 또한, 새로운 멤버가 추가되어도, makeObservable에 추가하지 않아도 되기 때문에, 관리하기도 쉽다.

    아래 예제는 함수형으로 makeAutoObservable을 사용했지만, 클래스에서도 사용할 수 있다.
    단, makeAutoObservable은 super를 갖고 있거나(상속받은 경우), subclass를 갖고 있는 경우(상속하는 경우)에는 사용할 수 없다.

import { makeAutoObservable } from "mobx"

function createDoubler(value) {
    return makeAutoObservable({
        value,
        get double() {
            return this.value * 2
        },
        increment() {
            this.value++
        }
    })
}
  • observable

    observable 메서드를 사용하면, 전체 object를 한 번에 observable로 만들어준다. observable이 되는 대상은 복제된 다음, 그 멤버들이 전부 observable이 된다.

    observable이 리턴하게 되는 object는 Proxy가 된다. Proxy가 된다는 말은, 나중에 그 object에 추가되는 프로퍼티들 또한 observable이 된다는 뜻이다.

    observable 메서드는 배열, Maps, Sets와 같은 collection type과 함께 호출될 수 있다.

    makeObservable과는 다르게, observable 메서드는 객체에 새로운 필드를 추가하거나 삭제하는것을 지원한다.


import { observable } from "mobx"

const todosById = observable({
    "TODO-123": {
        title: "find a decent task management system",
        done: false
    }
})

todosById["TODO-456"] = {
    title: "close all tickets older than two weeks",
    done: true
}

const tags = observable(["high prio", "medium prio", "low prio"])
tags.push("prio: for fun")

🍎 Actions

action은 state를 변경하는 코드이다. 원칙적으로 action은 항상 어떠한 이벤트에 의해 일어나게 된다. 예를 들면, 버튼 클릭, 인풋 변경, 웹소켓 메시지 도착 등등의 이벤트에 대한 응답으로 action이 일어나게 된다.

makeAutoObservable을 사용하는 경우는 예외지만, 그 외에는 action임을 MobX에게 알려주어야 한다. 그렇게 했을 때의 성능상 이점은 다음과 같다. action을 사용하는 것이 코드를 더 잘 구조화하게 해주고, 성능상 이점을 가져다준다.

  1. action은 transaction 안에서 동작하게 된다.

    action이 끝나기 전까지는 observer들이 update되지 않는다. action이 실행되는 중에 생기는 불완전한 값들은 어플리케이션의 다른 것들에 의해 보이지 않는다는 것이다.

  2. action 밖에서 state를 바꾸는 것이 허용되지 않는다.

    이는 코드의 어떤 부분에서 state가 바뀌는지를 명확하게 알 수 있게 해준다.

action은 state를 변경하는 함수에서만 써야 한다. 단순히 정보를 만들어내는 함수(state에서 무언가를 찾는다던가, 데이터를 필터링한다던가)에서는 action이라고 표기하면 안된다.

Action 표기의 5가지 방법

다음으로는 action을 표기하는 방법에 대해 알아보겠다. action을 만드는 방법에는 5가지가 있다.
1. makeObservable
2. makeAutoObservable
3. action.bound
4. action(fn)
5. runInAction(fn)

  • makeObservable
    makeObservable안에서 action으로 쓰이는 함수에 observable이라고 표기한다.
import { makeObservable, observable, action } from "mobx"

class Doubler {
    value = 0

    constructor(value) {
        makeObservable(this, {
            value: observable,
            increment: action
        })
    }

    increment() {
        // Intermediate states will not become visible to observers.
        this.value++
        this.value++
    }
}
  • makeAutoObservable

    알아서 notiation을 추론해주는 makeAutoObservable.

import { makeAutoObservable } from "mobx"

class Doubler {
    value = 0

    constructor(value) {
        makeAutoObservable(this)
    }

    increment() {
        this.value++
        this.value++
    }
}
  • action.bound

    action.bound는 메서드를 알맞은 instance에 bind 시켜준다. 따라서 this가 항상 함수 내부에서 알맞게 bind된다.

import { makeObservable, observable, action } from "mobx"

class Doubler {
    value = 0

    constructor(value) {
        makeObservable(this, {
            value: observable,
            increment: action.bound
        })
    }

    increment() {
        this.value++
        this.value++
    }
}

const doubler = new Doubler()

// Calling increment this way is safe as it is already bound.
setInterval(doubler.increment, 1000)

이게 무슨말인지 모르겠어서, action.bound를 넣었을 때와 넣지 않았을 때를 비교해보았다.

  • action.bound 를 표기했을 경우
import { makeObservable, observable, action } from "mobx";

class Doubler {
  value = "value";
  constructor(value) {
    makeObservable(this, {
      value: observable,
      say: action.bound
    });
  }
  say() {
    console.log(this.value);
  }
}

const doubler = new Doubler();
setInterval(doubler.say, 1000); 

실행시켰을 때, setInterval 내부에서도 this가 doubler에 바인드되어 'value' 제대로 나오는걸 확인할 수 있었다.

  • action.bound가 아닌 action으로 표기했을 경우

위와 같은 코드에서 say에 action으로 표기하면, undefined가 출력된다.

import { makeObservable, observable, action } from "mobx";

class Doubler {
  value = "value";
  constructor(value) {
    makeObservable(this, {
      value: observable,
      say: action
    });
  }
  say() {
    console.log(this.value);
  }
}

const doubler = new Doubler();
setInterval(doubler.say, 1000);

왜 이런식으로 되는지에 대해서는 다른 곳에 더 자세히 글을 남기겠다!

  • action(fn)

    state를 변경시키는 코드를 부르는 쪽에서는 acton으로 감싸서 최대한 transaction을 지원하는 MobX의 기능의 효과를 높여야 한다. action으로 감싸는 부분은 가능한한 멀리-!!

    To leverage the transactional nature of MobX as much as possible, actions should be passed as far outward as possible.

import { observable, action } from "mobx"

const state = observable({ value: 0 })

const increment = action(state => {
    state.value++
    state.value++
})

increment(state)
  • runInAction(fn)

    즉시 불려져야 하는 일시적인 액션을 만들 때, runInAction을 사용한다. 비동기처리에서 유용하다.

    runInAction을 사용하므로써 굳이 action을 따로 선언하여 사용할 필요없이, 바로 state를 변경하는 코드를 action으로 만들어준다.

import { observable, runInAction } from "mobx"

const state = observable({ value: 0 })

runInAction(() => {
    state.value++
    state.value++
})

비동기 Action

비동기 처리 과정에서 observable을 업데이트하는 모든 step은 action임을 표기해주어야 한다.
이를 처리하기 위해, 위에서 action을 표기하는 방법을 활용할 것이다.

예를 들어, 프라미스를 처리하는 부분에서, state를 변경시키는 핸들러는 action이 되어야 한다.

  • Wrap handlers in 'action'

    프라미스가 resolve되는 곳에서 action으로 감싸주어야 한다.

    Promise resolution handlers are handled in-line, but run after the original action finished, so they need to be wrapped by action


import { action, makeAutoObservable } from "mobx"

class Store {
    githubProjects = []
    state = "pending" // "pending", "done" or "error"

    constructor() {
        makeAutoObservable(this)
    }

    fetchProjects() {
        this.githubProjects = []
        this.state = "pending"
        fetchGithubProjectsSomehow().then(
            action("fetchSuccess", projects => {
                const filteredProjects = somePreprocessing(projects)
                this.githubProjects = filteredProjects
                this.state = "done"
            }),
            action("fetchError", error => {
                this.state = "error"
            })
        )
    }
}
  • Handle updates in separate actions

    프라미스 핸들러가 클래스의 메서드일 경우, makeAutoObservable에 의해 자동으로 action으로 감싸져서 처리된다.
    -> 클래스 안에서 프라미스 처리와 에러 처리가 따로 메서드로 나온다면, 어떤 비동기 처리의 프라미스 핸들러인지 알기가 어려울 것 같다.

If the promise handlers are class fields, they will automatically be wrapped in action by makeAutoObservable:

import { makeAutoObservable } from "mobx"

class Store {
    githubProjects = []
    state = "pending" // "pending", "done" or "error"

    constructor() {
        makeAutoObservable(this)
    }

    fetchProjects() {
        this.githubProjects = []
        this.state = "pending"
        fetchGithubProjectsSomehow().then(this.projectsFetchSuccess, this.projectsFetchFailure)
    }

    projectsFetchSuccess = projects => {
        const filteredProjects = somePreprocessing(projects)
        this.githubProjects = filteredProjects
        this.state = "done"
    }

    projectsFetchFailure = error => {
        this.state = "error"
    }
}
  • async/await + runInAction

    await 이후의 과정은 같은 tick에 있지 않기 때문에, action으로 감싸주어야 한다.

    Any steps after await aren't in the same tick, so they require action wrapping. Here, we can leverage runInAction


import { runInAction, makeAutoObservable } from "mobx"

class Store {
    githubProjects = []
    state = "pending" // "pending", "done" or "error"

    constructor() {
        makeAutoObservable(this)
    }

    async fetchProjects() {
        this.githubProjects = []
        this.state = "pending"
        try {
            const projects = await fetchGithubProjectsSomehow()
            const filteredProjects = somePreprocessing(projects)
            runInAction(() => {
                this.githubProjects = filteredProjects
                this.state = "done"
            })
        } catch (e) {
            runInAction(() => {
                this.state = "error"
            })
        }
    }
}
  • flow + generator function flow를 사용하는 것은 async/await과는 다르게 action으로 더 감싸줄 필요가 없다. -> 코드가 깔끔해진다.
    1. 비동기 함수를 flow로 감싼다.
    2. async 대신 function*를 사용한다.
    3. await 대신 yield를 사용한다.
import { flow, makeAutoObservable, flowResult } from "mobx"

class Store {
    githubProjects = []
    state = "pending"

    constructor() {
        makeAutoObservable(this, {
            fetchProjects: flow
        })
    }

    // Note the star, this a generator function!
    *fetchProjects() {
        this.githubProjects = []
        this.state = "pending"
        try {
            // Yield instead of await.
            const projects = yield fetchGithubProjectsSomehow()
            const filteredProjects = somePreprocessing(projects)
            this.state = "done"
            this.githubProjects = filteredProjects
        } catch (error) {
            this.state = "error"
        }
    }
}

const store = new Store()
const projects = await flowResult(store.fetchProjects())

출처

0개의 댓글