본문 바로가기

카테고리 없음

MobX 톺아보기 2 - MobX core - Actions

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

 


[ Updating state using actions : action을 사용하여 상태 업데이트 ]

 

사용법 :

  • action (annotation)
  • action (fn)
  • action (name, fn)

 

모든 애플리케이션에는 액션이 있다. 액션은 상태를 수정하는 모든 코드 조각이다. 원칙적으로 액션은 항상 이벤트에 대한 응답으로 발생한다. 예를 들어 버튼이 클릭되거나 입력이 변경되거나 웹소켓 메시지가 도착하는 등의 이벤트가 발생할 수 있다.

 

MobX에서는 액션을 선언해야 하지만, makeAutoObservable은 이 작업의 대부분을 자동화할 수 있다. 액션을 사용하면 코드를 더 잘 구조화하고 다음과 같은 성능상의 이점을 얻을 수 있다.

  1. 트랜잭션 내부에서 실행된다. 가장 바깥쪽 작업이 완료될 때까지 어떤 리액션도 실행되지 않으므로 작업 중에 생성된 중간 값이나 불완전한 값은 작업이 완료될 때까지 나머지 애플리케이션에서 볼 수 없다.
  2. 기본적으로 액션 외부에서 상태를 변경하는 것은 허용하지 않는다. 이렇게 하면 코드 베이스에서 상태 업데이트가 발생하는 위치를 명확하게 식별하는데 도움이 된다.

🤔 이전 톺아보기에서도 느꼈지만 MobX는 객체지향에 최적화된 것 같아 매력적으로 느껴진다.

 

 

action 어노테이션은 상태를 수정하려면 함수만 사용해야 한다. 정보를 도출하는 함수(조회 수행 또는 데이터 필더링)는 MobX가 호출을 추적할 수 있도록 action으로 표시해서는 안된다. action 어노테이션 멤버는 열거할 수 없다.

 

아래의 예시 코드를 참조하자.

 

 

*makeObservable

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

class Doubler {
    value = 0

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

    increment() {
        // 중간 State는 관찰자에게 표시되지 않는다.
        this.value++
        this.value++
    }
}

 

 

*makeAutoObservable

import { makeAutoObservable } from "mobx"

class Doubler {
    value = 0

    constructor(value) {
        makeAutoObservable(this)
    }

    increment() {
        this.value++
        this.value++
    }
}
makeAutoObservable을 사용하여 대부분의 어노테이션을 추론하고 있다.

 

 

*action(fn)

import { observable, action } from "mobx"

const state = observable({ value: 0 })

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

increment(state)

 

❓액션 어노테이션의 상태 수정을 위해 함수를 이용하는 예시인 것 같다.

 


[ Wrapping funtions using : action을 사용한 함수 래핑 ]

 

MobX의 트랜잭션 특성을 최대한 활용하려면 액션을 가능한 한 외부로 전달해야 한다. 클래스 메서드가 상태를 수정하는 경우 액션으로 표시하는 것이 좋다. 이벤트 핸들러는 가장 바깥쪽의 트랜잭션이므로 액션으로 표시하는 것이 더욱 좋다. 나중에 두 개의 액션을 호출하는 표시되지 않은 단일 이벤트 핸들러는 여전히 두 개의 트랜잭션을 생성한다.

 

 

액션 기반 이벤트 핸들러를 만드는 데 도움이 되도록 액션은 어노테이션일 뿐만 아니라 고차 함수로도 사용할 수 있다. 함수를 인수로 사용하여 호출할 수 있으며, 이 경우 동일한 서명을 가진 액션 래핑 함수를 반환한다.

 

예를 들어 리액트에서 onClick 핸들러는 아래와 같이 래핑 할 수 있다.

 

const ResetButton = ({ formState }) => (
    <button
        onClick={action(e => {
            formState.resetPendingUploads()
            formState.resetValues()
            e.preventDefault()
        })}
    >
        Reset form
    </button>
)

 

디버깅을 위해 래핑 된 함수의 이름을 지정하거나 이름을 액션의 첫 번째 인수로 전달하는 것이 좋다.

 

 

참고 : 액션은 추적되지 않는다.

 

액션의 또 다른 특징은 추적되지 않는다는 것이다. 액션이 부작용이나 computed 값 내부에서 호출되는 경우(매우 드물지만!), 액션이 읽은 관찰 가능 항목은 파생의 종속성에 포함되지 않는다.

 

makeAutoObservable, extendObservable 및 observable 함수는 autoAction이라는 특수한 액션을 사용하며, 이 액션은 런타임에 함수가 파생 함수인지 액션인지 결정한다.

 

 

 

action.bound

 

사용법

action.bound (annotation)

 

*action.bound

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()

// 이 방법으로 increment 호출하면 이미 바인딩되어 있으므로 안전하다.
setInterval(doubler.increment, 1000)

 

action.bound 어노테이션을 사용하면 메서드를 올바른 인스턴스에 자동으로 바인딩하여 함수 내에서 항상 올바르게 바인딩되도록 할 수 있다.

 

 

Tip : 모든 action과 flow를 자동으로 바인딩하려면 makeAutoObsevable(o, {}, {autoBind : true})을 사용한다.

import { makeAutoObservable } from "mobx"

class Doubler {
    value = 0

    constructor(value) {
        makeAutoObservable(this, {}, { autoBind: true })
    }

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

    *flow() {
        const response = yield fetch("http://example.com/value")
        this.value = yield response.json()
    }
}

 

✏️ action을 함수 내부에서 항상 올바르게 바인딩되게 하려면 action.bound를 사용해야 하며, 자동으로 바인딩하기 위해서 makeAutoObsevable의 옵션으로 autoBind를 true로 전달하자. 

 

 

 

runInAction

 

사용법 : 

runInAction(fn)

 

runInAction 유틸리티를 사용하면 즉시 호출되는 임시 액션을 만들 수 있다. 비동기 프로세스에서 유용하게 사용할 수 있다. 

 

 

*runInAction(fn)

import { observable, runInAction } from "mobx"

const state = observable({ value: 0 })

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

 

✏️ runInAction 유틸리티는 메서드에 액션을 어노테이션 하지 않고, 임시 액션을 만들어 즉시 호출할 수 있다. 

 

 

 

- action 및 상속

 

프로토타입에 정의된 액션만 서브클래스로 재정의할 수 있다.

 

class Parent {
    // on instance
    arrowAction = () => {}

    // on prototype
    action() {}
    boundAction() {}

    constructor() {
        makeObservable(this, {
            arrowAction: action
            action: action,
            boundAction: action.bound,
        })
    }
}
class Child extends Parent {
    // THROWS: TypeError: 속성을 재정의 할수 없다: arrowAction
    arrowAction = () => {}

    // OK
    action() {}
    boundAction() {}

    constructor() {
        super()
        makeObservable(this, {
            arrowAction: override,
            action: override,
            boundAction: override,
        })
    }
}

 

단일 액션을 바인딩하기 위해서 화살표 함수 대신 action.boun를 사용할 수 있다.

 

 

- 비동기 작업

 

본질적으로 비동기 프로세스는 발생 시점에 관계없이 모든 리액션이 자동으로 업데이트되므로 MobX에서 특별한 처리가 필요하지 않다. 그리고 관찰 가능한 객체는 변경 가능하기 때문에 일반적으로 액션이 지속되는 동안 객체에 대한 참조를 유지하는 것이 안전하다. 그러나 비동기 프로세스에서 observable을 업데이트하는 모든 단계는 action으로 표시해야 한다. 

 

 

*Wrap handlers in '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

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"
    }
}

 

프로미스 핸들러가 클래스 필드인 경우, makeAutoObservable에 의해 자동으로 액션으로 래핑 된다.

 

 

*async / await + 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"
            })
        }
    }
}

 

await 이후의 모든 단계는 동일한 단계에 속하지 않으므로 액션 래핑이 필요하다. 여기서는 runInAction을 활용할 수 있다.

 

 

 

 

async / await 대신 flow 사용

 

사용법 :

flow (annotation)
flow(function* (args) { })

 

flow 래퍼는 async / await에 대한 선택적 대안으로, MobX 액션으로 작업하기 쉽게 해 준다. flow는 제네레이터 함수를 유일한 입력으로 받는다. 제네레이터 내부에서 일 부 프로미스를 반환하여 피로미스를 연결할 수 있다.(일부 프로미스를 await 하는 대신 일부 프로미스를 yield 하는 일부 프로미스를 작성한다.) 그러면 flow 메커니즘이 제네레이터가 계속 진행하거나 반환된 프로미스가 해결될 때 thorw 하도록 한다.

 

따라서 flow는 추가 액션 래핑이 필요 없는 async / await 대신 사용할 수 있다. 다음과 같이 적용할 수 있다.

  1. 비동기 함수에 flow를 래핑 한다.
  2. async 대신 function*을 사용한다.
  3. await 대신 yield를 사용한다.

 

*'flow' + generator function

import { flow, makeAutoObservable, flowResult } from "mobx"

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

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

    // 별표에 주목하자, 이것은 제네레이터 함수이다.
    *fetchProjects() {
        this.githubProjects = []
        this.state = "pending"
        try {
            // await 대신 Yield 사용.
            const projects = yield fetchGithubProjectsSomehow()
            const filteredProjects = somePreprocessing(projects)
            this.state = "done"
            this.githubProjects = filteredProjects
            return projects
        } catch (error) {
            this.state = "error"
        }
    }
}

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

 

위의 flow + generator function 예제는 실제로 어떤 모습인지 보여준다.

 

flowResult 함수는 타입스크립트를 사용할 때만 필요하다는 점을 유의하자. 메서드를 flow로 꾸미면 반환된 제네레이터를 프로미스로 감싸기 때문이다. 그러나 타입스크립트는 이러한 변환을 인식하지 못하므로 flowResult는 타입스크립트가 해당 유형 변경을 인식하도록 한다.

 

 

참고 : 객체 필드에서 flow 사용

 

flow는 액션과 마찬가지로 함수를 직접 래핑 하는 데 사용할 수 있다. 위의 예시는 다음과 같이 작성할 수 도 있다.

import { flow } from "mobx"

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

    fetchProjects = flow(function* (this: Store) {
        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 store.fetchProjects()

장점은 더 이상 flowResult가 필요하지 않다는 점이지만, 단점은 타입이 올바르게 추론되었는지 확인하기 위해 입력해야 한다는 점이다.

 

 

 

flow.bound

 

사용법 :

flow.bound (annotation)

 

flow.bound 어노테이션을 사용하면 메서드를 올바른 인스턴스에 자동으로 바인딩하여 함수 내에서 항상 올바르게 바인딩되도록 할 수 있다. 액션과 마찬가지로, flow도 기본적으로 자동 바인딩 옵션을 사용하여 바인딩할 수 있다.

 

 

 

 

flow 취소하기

 

flow의 또 다른 깔끔한 장점은 취소가 가능하다는 것이다. flow의 반환값은 결국 제네레이터 함수에서 반환되는 값으로 해결되는 프로미스이다. 반환된 프로미스에는 실행 중인 제네레이터를 중단하고 취소할 수 있는 cancel() 메서드가 추가로 있다. try / finally 절은 계속 실행된다.

 

 

 

 

필수 action 비활성화하기

 

기본적으로 MobX 6 이상에서는 상태를 변경하려면 액션을 사용해야 한다. 그러나 이 액션을 비활성화하도록 MobX를 구성할 수 있다. 예를 들어 경고가 항상 큰 의미가 없는 단위 테스트 설정에서 매우 유용할 수 있다.