Posts [Mobx] Action
Post
Cancel

[Mobx] Action

Actions

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

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

  • 1)action은 transaction 안에서 동작하게 된다.
    • action이 끝나기 전까지는 observer들이 update되지 않는다. action이 실행되는 중에 생기는 불완전한 값들은 어플리케이션의 다른 것들에 의해 보이지 않는다는 것이다. 예를 들어, a = 1, b = 2를 foo라는 action 함수 내에서 실행되면 해당 함수가 모두 끝나게 변경사항이 한 번에 컴포넌트에 반영된다.
  • 2)action 밖에서 state를 바꾸는 것이 허용되지 않는다.
    • 이는 코드의 어떤 부분에서 state가 바뀌는지를 명확하게 알 수 있게 해준다.

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

Action 표기의 5가지 방법

action을 만드는 방법에는 5가지가 있다.

  1. makeObservable
  2. makeAutoObservable
  3. action.bound
  4. action(fn)
  5. runInAction(fn)

makeObservable

makeObservable안에서 action으로 쓰이는 함수에 action이라고 표기한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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된다. 이를 명확하게 이해하기 위해 action 과 action.bind 의 차이를 비교해보자.

먼저 그냥 action을 사용하여 아래와 같이 실행해보자.

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
import { action, makeAutoObservable, makeObservable, observable } from 'mobx';

class CountClass {
  number:number = 0;

  constructor() {
    makeObservable(this, {
      number: observable,
      increase: action,
      decrease: action,
      print: action,
    })
  }

  increase = () => {
    this.number++;
  }

  decrease = () => {
    this.number--;
  }

  print() {
    console.log("number: " + this.number);
  }
}

const countClass = new CountClass();

setInterval(countClass.print, 1000);

그러면 다음과 같이 undefined로 값이 출력되는걸 확인할 수 있다. 자바스크립트의 this는 호출하는 컨텍스트입장에서 정해지는데 countClass를 호출하는 컨텍스트의 this.number 값이 undefined이기 때문이다.

image

이를 action.bound로 변경할 경우엔 정상적으로 this.number값이 출력된다. 아마 action.bound의 경우 자바스크립트의 bind()를 사용하여 해결하지 않을까 싶다.

image

action(fn)

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

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

1
2
3
4
5
6
7
8
9
10
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으로 만들어준다.

1
2
3
4
5
6
7
8
import { observable, runInAction } from "mobx"

const state = observable({ value: 0 })

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

비동기 Action

비동기 처리 과정에서 observable을 업데이트하는 모든 step은 action임을 표기해주어야 한다.

이를 처리하기 위해, 위에서 action을 표기하는 방법을 활용할 것이다.

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

Wrap handlers in ‘action’

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

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

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

Promise 핸들러가 클래스의 메서드일 경우, makeAutoObservable에 의해 자동으로 action으로 감싸져서 처리된다.

  • 클래스 안에서 Promise 처리와 에러 처리가 따로 메서드로 나온다면, 어떤 비동기 처리의 Promise 핸들러인지 알기가 어려울 것 같다.
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
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으로 감싸주어야 한다.

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

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
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를 사용

flow + generator function flow를 사용하는 것은 async/await과는 다르게 action으로 더 감싸줄 필요가 없다. -> 코드가 깔끔해진다.

  • 1)비동기 함수를 flow로 감싼다.
  • 2)async 대신 function*를 사용한다.
  • 3)await 대신 yield를 사용한다.
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
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())

mobx action에 대한 궁금점

만약 action 함수 내부에서 다른 함수(action이 아닌)를 호출하여 observable state를 변경한다면?

위와 같은 의문점이 든 이유는 호출된 함수까지 action이 적용되는지 궁금해서였다. 위와 같은 케이스를 직접 실제 코드로 작성하여 테스트해보았다. actionTest() 함수는 action으로 설정해주고 double() 함수는 action으로 설정해주지 않았다. 그리고 actionTest() 함수를 호출하였다.

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
32
class ActionTest {

  value: number = 1;

  constructor() {
    makeObservable(this, {
      value: observable,
      increase: action,
      decrease: action,
      actionTest: action,
    })
  }

  increase = () => {
    this.value++;
  }

  decrease = () => {
    this.value--;
  }

  actionTest = () => {
    console.log("before this.value: " + this.value);
    this.double();
    console.log("after this.value: " + this.value);
  }

  double = () => {
    this.value = this.value * 2;
    console.log("[double]: " + this.value);
  }
}

결과는 아래 이미지와 같이 action 함수 내부에서 호출한 다른 함수(action이 아닌)에서 observable state를 변경하더라도 action 속성이 적용이되어 2로 변경된 것을 확인할 수 있다. action 없이 state를 변경했다는 warning 메시지도 안보이는 것을 확인할 수 있다.

image

action(fn)과 runInAction의 차이는 무엇인가?

action(fn) 은 함수를 감싸서 함수 전체를 트랜잭션하에 상태 변경 처리하고, runInAction은 함수 내부에서 특정 부분을 감싸서 트랜잭션하에 상태 변경을 처리한다. 위에서 설명한 것처럼 runInAction 은 await 이후에 상태를 변경할때 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Product {

  price: number = 100;

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

  ...

  changePrice = action(() => (price: number) {
      this.price = price;
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Product {

  price: number = 100;

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

  ...

  changePrice(price: number) {
      const foo = await doSomething()....
      runInAction() {
        this.price = price;
      }
  })
}

왜 await 이후에는 runInAction을 사용해야 하는지?

해당 부분에 대해서 공식 레퍼런스에는 같은 단계(같은 tick) 에 있지 않기 때문에, action으로 감싸주어야 한다고 설명되어있는데 본질적인 이유를 알기 위해선 추후 내부 코드를 보면서 분석해보자..!

flow란?

flow wraaper 는 async/await 에 대한 선택적 대안이다.(mobx의 액션을 더 쉽게 사용하도록 하기위한) flowgenerate function에 적용이 된다.

generator 내부적으로 yield 키워드를 사용함으로써 프로미스 체인을 형성할 수 있다.(await somPromise 대신 yield somPromise) flow 메커니즘은 promise가 resolve될 때 generator 가 지속되거나 throw 되는 것을 보장해준다고 한다.

override 어노테이션의 사용 이유?

mobx 공식 레퍼런스에선 부모 클래스를 상속한 자식 클래스에서는는 부모 클래스의 프로퍼티들을 override로 정의하는 것을 가이드하고 있다. 예시로는 아래와 같이 Child 클래스에서 부모 Parent 클래스의 mobx 어노테이션을 override 로 선언하는 것처럼 말이다.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class Child {

  value1: number = 1;

  constructor() {
    makeObservable(this, {
      value1: observable,
      increase1: action,
      decrease1: action,
      clearData1: action,
    })
  }

  increase1() {
    this.value1++;
    console.log("[OverrideTest] increase");
  }

  decrease1() {
    this.value1--;
  }

  clearData1() {

  }
}

class Parent extends Child {

  value2: number = 1;

  constructor() {
    super();
    makeObservable(this, {
      value1: override, // 부모 클래스의 프로퍼티들을 자식 클래스에 override로 정의
      increase1: override,
      decrease1: override,
      value2: observable,
      increase2: action,
      decrease2: action,
    })
  }

  increase() {
    this.value1++;
    console.log("[Parent] invoked increase");
  }

  increase2() {
    this.value2++;
  }

  decrease2 = () => {
    this.value2--;
  }

  clearData() {
    this.value = 0;
  }
}

이걸 안붙이면 어떻게 될까? 라는 궁금증에 아래와 같이 해당 선언 부분을 제외하과 테스트를 해보았더니 정상적으로 동작하였다.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Child {

  value1: number = 1;

  constructor() {
    makeObservable(this, {
      value1: observable,
      increase1: action,
      decrease1: action,
      clearData1: action,
    })
  }

  increase1() {
    this.value1++;
    console.log("[OverrideTest] increase");
  }

  decrease1() {
    this.value1--;
  }

  clearData1() {

  }
}

class Parent extends Child {

  value2: number = 1;

  constructor() {
    super();
    makeObservable(this, {
      value2: observable,
      increase2: action,
      decrease2: action,
    })
  }

  increase() {
    this.value1++;
    console.log("[Parent] invoked increase");
  }

  increase2() {
    this.value2++;
  }

  decrease2 = () => {
    this.value2--;
  }

  clearData() {
    this.value = 0;
  }
}

부모 클래스(Parent)를 상속한 자식 클래스(Child)에서 increase() 함수를 오버라이딩하고 부모 클래스의 observable status(value1) 을 변경하였음에도 정상적으로 warning 메시지 없이 처리되었다. override 어노테이션의 실질적인 역할은 추후 알게되는데로 정리를 해보겠다.

출처

This post is licensed under CC BY 4.0 by the author.

[Webpack] Source Map

[Mobx] observable vs makeAutoObersable vs makeObservable