MobX 공식 문서의 MobX core Reations를 톺아보자.
[ 부수효과와 함께 reaction 실행 ]
reaction은 MobX의 모든 것이 모이는 곳이기 때문에 이해해야 할 중요한 개념이다. reaction이 목표는 자동으로 발생하는 부수효과를 모델링하는 것이다. 관찰 가능한 상태에 대한 consumer를 생성하고 관련 사항이 변경될 때마다 자동으로 부수효과를 실행하는 데 그 중요성이 있다.
하지만 이 점을 염두에 두고 여기서 설명하는 API는 거의 사용되지 않는다는 점을 인식하는 것이 중요하다. 이들은 종종 다른 라이브러리(예 : mobx-react)나 애플리케이션에 특정한 추상화에서 추상화되어 있다.,
하지만 MobX를 이해하기 위해 reaction이 어떻게 생성되는지 살펴보자. 가장 간단한 방법은 autorun 유틸리티를 사용하는 것이다. 그 외에도 reaction과 when이 있다.
Autorun
사용법
autorun(effect: (reaction) => void, options?)
autorun 함수는 관찰하는 항목이 변경될 때마다 실행되어야 하는 하나의 함수를 허용한다. 또한 autorun 자체를 만들 때 한 번 실행된다. 이 함수는 관찰 가능한 상태, observable 또는 computed로 어노테이션 된 항목의 변경에만 응답한다.
작동방식
autorun은 reactive context에서 effect를 실행하는 방식으로 작동한다. 제공된 함수가 실행되는 동안 MobX는 이펙트가 직간접적으로 읽은 모든 observable과 computed value를 추적한다. 함수가 완료되면 MobX는 읽은 모든 관찰 가능한 값을 수집하여 구독하고, 관찬 가능한 값 중 하나라도 다시 변경될 때까지 기다린다. 변경되면 autorun이 다시 트리거 되어 전체 프로세스가 반복된다.
아래 예제는 다음과 같이 작동한다.
import { makeAutoObservable, autorun } from "mobx"
class Animal {
name
energyLevel
constructor(name) {
this.name = name
this.energyLevel = 100
makeAutoObservable(this)
}
reduceEnergy() {
this.energyLevel -= 10
}
get isHungry() {
return this.energyLevel < 50
}
}
const giraffe = new Animal("Gary")
autorun(() => {
console.log("Energy level:", giraffe.energyLevel)
})
autorun(() => {
if (giraffe.isHungry) {
console.log("Now I'm hungry!")
} else {
console.log("I'm not hungry!")
}
})
console.log("Now let's change state!")
for (let i = 0; i < 10; i++) {
giraffe.reduceEnergy()
}
// Energy level: 100
// I'm not hungry!
// Now let's change state!
// Energy level: 90
// Energy level: 80
// Energy level: 70
// Energy level: 60
// Energy level: 50
// Energy level: 40
// Now I'm hungry!
// Energy level: 30
// Energy level: 20
// Energy level: 10
// Energy level: 0
위 출력의 처음 두 줄에서 볼 수 있듯이 두 autorun 함수는 초기화될 때 한 번 실행된다. 이것이 for 루프가 없으면 표시되는 전부이다.
reduceEnergy acion으로 energyLevel를 변경하기 위해 for 루프를 실행하면, autorun 함수가 observable state의 변화를 감지하는 '모든 순간' 새로운 로그를 출력한다.
- 함수 'Energy level' 측면에서 '모든 순간'이란 observable 속성을 가진 energyLevel이 변경되는 10회이다.
- 함수 'Now I'm hungry'의 측면에서 '모든 순간'이란 computed 속성을 가진 isHungry가 변경되는 1회이다.
✏️ autorun에 전달된 함수에서 사용하는 observable state가 변경될 때마다 전달된 함수를 실행시킨다.
Reaction
사용방법
reaction(() => value, (value, previousValue, reaction) => { sideEffect }, options?)
reaction은 autorun과 유사하지만 추적할 observable에 대해 보다 세밀하게 제어할 수 있다. reaction은 다음과 같은 두 개의 함수를 취한다. 첫 번째 data 함수는 트래킹 되어 두 번째 effect 함수에 대한 input으로 사용되는 데이터를 반환한다. 부수효과는 오직 data 함수에서 액세스 된 데이터에만 반응하며, 이는 effect 함수에 실제로 사용되는 데이터보다 적을 수 있다는 점에 유의해야 한다.
일반적인 패턴은 data 함수에서 부수 효과에 필요한 항목을 생성하여 effect가 트리거 되는 시점을 보다 정확하게 제어하는 것이다. 기본적으로 effect 함수가 트리거 되기 위해서는 data 함수의 결과가 변경되어야 한다. autorun과 달리 부수효과는 초기화될 때 실행되지 않으며, 데이터 표현이 처음으로 새로운 값을 반환할 때에만 실행된다.
예시
하단의 예시에서 reation은 isHungry가 바뀔 때만 한 번 트리거 된다. effect 함수에서 사용된 giraffe.energyLevel의 변경 사항은 effect 함수를 실행시키지 않는다. 이때에도 reactin이 반응하기를 원한다면, data 함수에도 giraffe.energyLevel에 액세스 하여 반환해야 한다.
import { makeAutoObservable, reaction } from "mobx"
class Animal {
name
energyLevel
constructor(name) {
this.name = name
this.energyLevel = 100
makeAutoObservable(this)
}
reduceEnergy() {
this.energyLevel -= 10
}
get isHungry() {
return this.energyLevel < 50
}
}
const giraffe = new Animal("Gary")
reaction(
() => giraffe.isHungry,
isHungry => {
if (isHungry) {
console.log("Now I'm hungry!")
} else {
console.log("I'm not hungry!")
}
console.log("Energy level:", giraffe.energyLevel)
}
)
console.log("Now let's change state!")
for (let i = 0; i < 10; i++) {
giraffe.reduceEnergy()
}
// Now let's change state!
// Now I'm hungry!
// Energy level: 40
When
사용방법
when(predicate: () => boolean, effect?: () => void, options?)
when(predicate: () => boolean, options?): Promise
when은 true를 반환할 때까지 주어진 predicate 함수를 관찰하고 실행한다. true가 반환되면 지정된 effect 함수를 실행하고 자동 실행기를 삭제(dispose)한다.
when 함수는 disposer를 반환하므로 두 번째 effect 함수를 전달하지 않는 한 수동으로 취소할 수 있으며, 이 경우 Promise가 반환된다.
반응적으로 dispose 하기
when은 무언가를 반응적으로 dispose 하거나 취소하는 데 굉장히 유용하다.
import { when, makeAutoObservable } from "mobx"
class MyResource {
constructor() {
makeAutoObservable(this, { dispose: false })
when(
// 만약...
() => !this.isVisible,
// ...그러면
() => this.dispose()
)
}
get isVisible() {
// 해당 항목이 visible인지 식별한다.
}
dispose() {
// 리소스를 정리한다.
}
}
isVisible이 false가 되자마자 dispose를 호출하여 MyResource를 정리한다.
await when(...)
effect 함수가 제공되지 않으면 when은 Promise를 반환한다. async / await과 함께 사용하면 observable state의 변경 사항을 기다릴 수 있게 해 준다.
async function() {
await when(() => that.isVisible)
// 등등...
}
when을 조기에 취소하고 싶다면 자체적으로 반환된 Promise에 .cancel()을 호출하면 된다.
규칙
reactive context에 적용되는 몇 가지 규칙이 있다.
- observable이 변경될 경우 영향을 받는 reaction이 기본적으로 즉시(동기적으로) 실행한다. 그러나 현재의 가장 바깥쪽 (trans) action이 완료되기 전까지는 실행되지 않는다.
- autorun은 제공된 함수가 동기적으로 실행되는 동안 읽어지는 observable만을 트래킹 한다. 비동기적으로 발생하는 것은 트래킹 하지 않는다.
- action은 트래킹 대상이 아니기 때문에, autorun은 autorun 내의 action에서 읽어지는 observable을 트래킹 하지 않는다.
항상 reaction dispose 하기
autorun, reaction 그리고 when에 전달되는 함수는 관찰하는 모든 객체가 자체적으로 가비지 수집을 하는 경우에만 가비지 수집이 된다. 일반적으로 이 함수들은 사용하는 observable이 새롭게 변경될 때까지 계속 대기한다. 이러한 대기 상태를 멈추기 위해, 모든 함수는 '대기 상태를 중단하고 함수에서 사용한 observable의 구독을 취소하는 역할'의 disposer 함수를 반환한다.
const counter = observable({ count: 0 })
// autorun을 세팅하고 0을 출력.
const disposer = autorun(() => {
console.log(counter.count)
})
// 출력 값: 1
counter.count++
// autorun을 중단.
disposer()
// 아무 것도 출력하지 않음.
counter.count++
메서드의 부수효과가 더 이상 필요하지 않은 경우 즉시 해당 메서드에서 반환되는 disposer 함수를 사용하는 것이 좋다. 그렇지 않으면 메모리 누수가 발생할 수 있다.
reaction과 autorun의 effect 함수에 대해 두 번째 인수로 전달된 reaction 인수는 reation.dispose()를 호출하여 reation을 조기 정리하는 데에도 활용할 수 있다.
reaction을 조금만 사용하자!
이미 언급했지만 reaction을 생성할 일은 드물 것이다. 애플리케이션에서는 이러한 API를 직접적으로 사용하지 않을 것이며, reaction을 구성하는 유일한 방법은 Mobx-react 바인딩의 'observer'와 같은 간접적인 것이다.
reaction을 세팅하기 전에 하단의 원칙을 준수하고 있는지를 먼저 확인하자.
- 원인(cause)과 영향(effect) 사이에 직접적인 관계가 없는 경우에만 reaction을 사용하자. 부수 효과가 제한된 일련의 event ∙ action에 대응하여 발생하는 경우, 특정 action에서 effect를 직접적으로 트리거 하는 편이 종종 더 명확하다. 예를 들어 폼 제출 버튼을 누르면 네트워크 요청이 게시될 경우, 간접적으로 reaction을 사용하는 것보다는 해당 effect를 onClick 이벤트에 대한 응답으로써 직접적으로 트리거 하는 것이 더 명확하다. 폼 상태에 대한 변경사항이 자동으로 로컬 저장소에 있어야 하는 경우, reaction을 사용하면 모든 개별 onChange 이벤트에서 해당 effect를 트리거 하지 않아도 되므로 유용할 수 있다.
- reaction은 다른 observable을 업데이트하면 안 된다. reaction으로 다른 observable을 수정할 건가? 만약 그렇다면, 일반적으로 업데이트할 observable은 computed 값으로 주석을 달아야 한다. 예를 들어 todo 컬렉션이 변경된 경우 remainingTodos의 양을 계산하기 위해 reaction을 사용하는 것이 아니라, remainingTodos를 computed 값으로 어노테이션 해야한다. 그러면 코드를 훨씬 더 명확하고 쉽게 디버깅할 수 있다. reaction은 새로운 데이터를 계산하는 것이 아니라, effect를 유발하는 용도로 사용되어야 한다.
- reaction은 독립적이어야 한다. 코드가 먼저 실행되어야 하는 다른 reaction에 의존하는가? 이 경우 첫 번째 규칙을 위반했을 수 있으며, 의존하고 있는 reaction에 새로 생성하려는 reaction을 병합해야 한다. MobX는 reaction이 실행되는 순서를 보장하지 않는다.
실제로 작업을 하다 보면 상단의 원칙과 부합하지 않는 경우가 있을 수 있다. 위 목록은 법칙이 아닌 원칙이다. 하지만 예외는 드물기 때문에 원칙을 위반하는 것은 최후의 수단으로 사용하자
Options
autorun, reaction, when의 동작은 위의 사용 방법과 같이 options 인수를 전달함으로써 더욱 미세하게 조정될 수 있다.
- name
이 문자열은 Spy Event listener 및 Mox developer tools에서 reaction에 대한 디버깅 이름으로 사용된다.
- fireImmediately (reaction)
data 함수의 첫 번째 실행 후 effect 함수가 즉시 트리거 되어야 함을 나타내는 boolean이다. 기본 값은 false이다.
- delay (autorun, reaction)
effect 함수를 조정하는 데 사용할 수 있는 시간(밀리초)이다. 0(기본값)이면 쓰로틀링(throttling)이 수행되지 않는다.
- timeout (when)
when이 대기하는 제한 시간을 설정한다. 시간 초과 시 when은 reject 또는 throw 한다.
- onError
기본적으로 reaction 내부에서 throw 된 모든 예외는 로그에 찍히지만, 더 이상 throw 되지는 않는다. 이는 한 reaction의 예외가 다른(관련되지 않은) reaction의 예정된 실행을 방해하지 않도록 하기 위한 것이다. 이를 통해 reaction이 예외로부터 복구(recover) 될 수도 있다. 예외를 throw 해도 MobX의 트래킹은 중단되지 않으므로 예외의 원인이 제거되면 후속 reaction이 다시 정상적으로 실행될 수 있다. 이 옵션을 사용하면 해당 동작을 오버라이딩할 수 있다. configure를 사용하여 전역 오류 처리기(global error handler)를 설정하거나 오류 탐지 기능을 완전히 비활성화할 수 있다.
- scheduler (autorun, reaction)
사용자 지정 스케줄러를 설정하여 autorun 함수 재실행 예약 방법을 결정한다. { scheduler: run => { setTimeout(run, 1000) }}과 같이 나중에 호출되는 함수가 필요하다.
- equals: (reaction)
기본적으로 comparer.default로 설정된다. 구체적으로 명시된 경우 이 비교 함수는 data 함수에 의해 생성된 이전 값과 다음 값을 비교하는 데 사용된다. effect 함수는 이 함수가 false를 반환하는 경우에만 호출된다.
'React > 상태관리하기' 카테고리의 다른 글
MobX 톺아보기 3 - MobX core - Computed (0) | 2023.02.14 |
---|---|
MobX 톺아보기 1 - MobX core - observable state (0) | 2023.02.13 |
MobX 톺아보기 0 - 컨셉, 원칙 (0) | 2023.02.13 |
Redux 톺아보기 2 - (비동기 로직) (0) | 2023.02.11 |
Redux 톺아보기 1 - (redux-toolkit) (0) | 2023.02.10 |