MobX 공식 문서를 본격적으로 톺아보자.
[ Createing observable state : 관찰 가능한 상태 만들기 ]
프로퍼티, 전체 객체, 배열, Map 및 Set은 모두 관찰 가능하게 만들 수 있다. 객체를 관찰 가능하게 만드는 기본 방법은 makeObservable을 사용하여 프로퍼티별로 어노테이션을 지정하는 것이다. 가장 중요한 어노테이션은 다음과 같다.
- observable : 상태를 저장하는 추적 가능한 필드를 정의한다.
- action : 메서드를 상태를 수정하는 액션으로 표시한다.
- computed : 상태로부터 새로운 사실을 도출하고 그 출력을 캐시 하는 getter를 표시한다.
makeObservable
makeObservable(target, annotations?, options?)
makeObservable은 기존 객체 속성을 관찰 가능하게 만드는 데 사용한다. 모든 자바스크립트 객체(클래스 인스턴스 포함)를 target에 전달할 수 있다. 일반적으로 makeObservable은 클래스 생성자에서 사용되며, 첫번째 인수는 this이다. annotations 인수는 어노테이션을 각 멤버에 매핑한다. 어노테이션이 있는 멤버만 영향을 받는다. 만약 decorator를 사용한다면 annotations 인수를 생략할 수 있다.
정보를 도출하고 인수를 받는 메서드 (예 : findUsersOlderThan(age: number) : User [])는 computed 된 것으로 어노테이션 할 수 없으며, 리액션에서 호출될 때 읽기 연산은 계속 추적되지만 메모리 누수를 방지하기 위해 output은 메모화되지 않는다. 이러한 메서드를 메모화하려면 MobX-utils computedFnd을 사용할 수 있다.
* class + makeObservable
import { makeObservable, observable, computed, action, flow } from "mobx"
class Doubler {
value
constructor(value) {
makeObservable(this, {
value: observable,
double: computed,
increment: action,
fetch: flow
})
this.value = value
}
get double() {
return this.value * 2
}
increment() {
this.value++
}
*fetch() {
const response = yield fetch("/api/value")
this.value = response.json()
}
}
✏️ 앞서 언급한대로 makeObservable을 클래스 생성자에 사용하고 있다. makeObservable의 첫 번째 인수로 this를 전달하고 있으며, 어노테이션을 각 멤버 변수에 매핑해주고 있다.
value의 경우 관찰 가능하도록 observable, double의 경우 상태로부터 새로운 값을 도출하는 computed, increment의 경우 상태를 수정하는 메서드이기 때문에 action으로 설정되었다.
makeAutoObservable
makeAutoObservable(target, overrides?, options?)
*factory function + makeAutoObservable
import { makeAutoObservable } from "mobx"
function createDoubler(value) {
return makeAutoObservable({
value,
get double() {
return this.value * 2
},
increment() {
this.value++
}
})
}
클래스에서도 makeAutoObservable을 활용할 수 있다. 예제의 차이는 MobX가 다양한 프로그래밍 스타일에 어떻게 적용될 수 있는지를 보여줄 뿐이다.
기본적으로 모든 프로퍼티를 추론하기 때문에 makeAutoObservable는 스테로이드 버전의 makeObservable과 같다. 그러나 오버라이드 매개변수를 사용하여 특정 어노테이션으로 기본 동작을 재정의할 수 있으며, 특히 false를 사용하면 속성이나 메서드가 처리되지 않도록 완전히 제외할 수 있다.
makeAutoObservable 함수는 새로운 멤버 변수를 명시적으로 언급할 필요가 없으므로 makeObservable을 사용하는 것보다 더 간결하고 유지 관리하기가 쉽다. 그러나 super가 있거나 subClass가 있는 클래스에는 makeAutoObservable을 사용할 수 없다.
✏️ makeAutoObservable 함수를 사용하면 어노테이션으로 멤버 변수를 래핑 해주지 않아도 기본적으로 추론하기 때문에 보다 간결하고 유지 관리가 쉽다는 것 같다. 하지만 상속받는 클래스나 서브클래스가 있는 클래스에서는 사용이 불가능하다는 것 같다.
참조 규칙
- 모든 자체 프로퍼티가 관찰 가능해진다.
- 모든 getter는 computed 된다.
- 모든 setter는 ation이 된다.
- 모든 함수는 autoAcion이 된다.
- 모든 제너레이터 함수는 flow가 된다. (일부 트랜스파일러 구성에서는 제너레이터 함수를 감지할 수 없으므로 flow가 예상대로 작동하지 않는 경우 flow를 명시적으로 지정해야한다.)
- 오버라이드 인수에 false로 표시된 멤버에는 어노테이션 추가가 불가능하다. 예를 들어 식별자와 같은 읽기 전용 필드에 사용할 수 없다.
observable
observable(source, overrides?, options?)
*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")
첫 번째 예제에서 makeObservable을 사용한 것과 달리, observable은 객체에 필드를 추가 및 제거를 지원한다. 따라서 동적으로 키가 지정된 객체, 배열, Map, Set과 같은 컬렉션에 observable을 유용하게 사용이 가능하다.
observable 어노테이션은 함수로 호출하여 객체를 한 번에 관찰 가능하게 만들 수 있다. source 객체가 복제되고 모든 멤버가 관찰 가능하게 만들어지는데, 이는 makeAutoObservable로 수행하는 방식과 유사하다. 마찬가지로 overrides map을 제공하여 특정 멤버의 어노테이션을 지정할 수 있다.
observable로 반환되는 객체는 프록시가 되므로 나중에 객체에 추가되는 프로퍼티도 선택되어 관찰 가능하게 된다.(프록시 사용이 비활성화된 경우는 제외한다.)
observable 메서드는 배열, Map, Set과 같은 컬렉션 유형으로도 호출이 가능하다. 이러한 유형도 복제되어 관찰 가능한 해당 유형으로 변환된다.
아래의 추가 예제를 살펴보자.
import { observable, autorun } from "mobx"
const todos = observable([
{ title: "Spoil tea", completed: true },
{ title: "Make coffee", completed: false }
])
autorun(() => {
console.log(
"Remaining:",
todos
.filter(todo => !todo.completed)
.map(todo => todo.title)
.join(", ")
)
})
// Prints: 'Remaining: Make coffee'
todos[0].completed = false
// Prints: 'Remaining: Spoil tea, Make coffee'
todos[2] = { title: "Take a nap", completed: false }
// Prints: 'Remaining: Spoil tea, Make coffee, Take a nap'
todos.shift()
// Prints: 'Remaining: Make coffee, Take a nap'
해당 예시는 관찰 가능한 항목을 만들고 autorun을 사용하여 관찰하는 예시이다. Map 및 Set 컬렉션으로 작업하는 것도 비슷하게 작동한다.
관찰 가능한 배열에는 몇 가지 멋진 유틸리티 함수가 추가로 있다.
- clear() : 배열에서 현재 항목을 모두 제거한다.
- replace(newItems) : 배열의 모든 기존 항목을 새 항목으로 변경한다.
- remove(value) : 배열에서 value 별로 단일 항목을 제거한다. 항목을 찾아서 제거하면 true를 반환한다.
참고 : primitives와 클래스 인스턴스는 observable로 변환되지 않는다.
primitives 값은 자바스크립트에서 불변이기 때문에 MobX에서 관찰 가능하게 만들 수 없다.(단, 박스로 묶을 수는 있다.) 일반적으로 라이브러리 외부에서는 이 메커니즘을 사용하지 않는다.
클래스 인스턴스는 observable로 전달하거나 관찰 가능한 프로퍼티에 할당한다고 해서 자동으로 관찰 가능하게 만들 수 없다. 클래스 멤버를 관찰 가능하게 만드는 것은 클래스 생성자의 책임으로 간주한다.
Tip : observable(proxied) vs makeObservable(unproxied)
make(Auto)Observable과 observable의 주요 차이점은 make(Auto)Observable은 첫 번째 인자로 전달한 객체를 수정하는 반면 observable은 관찰 가능한 복사본을 생성한다는 점이다.
두 번째 차이점은 observable은 객체를 동적 조회 map으로 사용하는 경우 향후 프로퍼티 추가를 트래핑할 수 있도록 프록시 객체를 생성한다는 점이다. 관찰 가능하게 만들려는 객체가 모든 멤버를 미리 알고 있는 규칙적인 구조인 경우, 프록시되지 않은 객체가 조금 더 빠르고 디버거 및 console.log에서 검사하기 쉽기 때문에 makeObservable을 사용하는 것이 좋다.
따라서 make(Auto)Observable은 팩토리 함수에 사용할 것을 권장하는 API이다. 프록시되지 않은 복사본을 얻기 observable 옵션으로 {proxy : false}를 전달할 수 있다.
[ Available annotations : 사용 가능 어노테이션 ]
Annotation | Description | |||
observable observable.deep |
상태를 저장하는 추적 가능한 필드를 정의한다. 가능하면 observable에 할당된 모든 값은 유형에 따라 (deep) observable, autoAction 또는 flow으로 자동 변환된다. 일반 객체, 배열, Map, Set, 제네레이터 함수만 변환할 수 있다. 클래스 인스턴스 및 기타 유형은 그대로 유지된다. |
|||
observable.ref | observable과 비슷하지만 재할당만 추적한다. 할당된 값은 완전히 무시되며 observable / autoAction / flow로 자동 변환되지 않는다. 예를 들어, 변경 불가능한 데이터를 관찰 가능한 필드에 저장하려는 경우 이 방법을 사용하자. | |||
observable.shallow | observable.ref와 비슷하지만 컬렉션을 위한 것이다. 할당된 모든 컬렉션은 관찰 가능하지만 컬렉션의 콘텐츠 자체는 관찰할 수 없게 된다. | |||
observable.struct | observable과 비슷하지만, 구조적으로 현재 값과 동일한 할당된 값은 무시 된다는 점이 다르다. | |||
action | 메서드를 상태를 수정하는 action으로 표시한다. 쓰기가 불가능하다. 자세한 것은 MobX core - action에서 톺아보자. | |||
action.bound | action과 비슷하지만 action을 인스턴스에 바인딩하여 항상 설정되도록 한다. 쓰기가 불가능하다. | |||
computed | getter에서 사용하여 캐시할 수 있는 파생값으로 선언할 수 있다. 자세한 것은 MobX core - computed에서 톺아보자. | |||
computed.struct | 재계산 후 이전 결과의 구조적으로 동일한 경우 관찰자에게 알림이 전송되지 않는 점을 제외하면 computed와 동일하다. | |||
true | 최적의 어노테이션을 추론한다. | |||
false | 속성에 명시적으로 어노테이션을 달지 않는다. | |||
flow | 비동기 프로세스를 관리하기 위한 flow를 만든다. 자세한 것은 MobX core - action에서 톺아보자. 타입스크립트에서 추론된 반환 유형이 꺼져 있을 수 있다. 쓰기가 불가능 하다. | |||
flow.bound | flow와 비슷하지만, flow를 인스턴스에 바인딩하여 항상 설정되도록 한다. 쓰기가 불가능하다. | |||
override | 서브 클래스에 의해 재정의된 상속된 action, flow, computed, action.bound에 적용이 가능하다. | |||
autoAction | 명시적으로 사용해서는 안 되지만, 호출 컨텍스트에 따라 action 또는 파생으로 작동할 수 있는 메서드를 표시하기 위해 makeAutoObservable에서 내부적으로 사용된다. 함수가 파생 함수인지 액션인지는 런타임에 의해 결정된다. | |||
[ Limitations : 제한 사항 ]
- make(Auto)Observable은 이미 정의된 프로퍼티만 지원한다. 컴파일러 구성이 올바른지 확인하거나, 해결 방법으로 make(Auto)Observable을 사용하기 전에 모든 프로퍼티에 값이 할당되었는지 확인하자. 올바른 구성이 없으면 선언되었지만 초기화되지 않은 필드(예 :class X {y;})가 올바르게 선택되지 않는다.
- makeObservable은 superclass 정의에 의해 선언된 프로퍼티에만 어노테이션을 달 수 있다. 서브 클래스나 super 클래스가 관찰 가능한 필드를 도입하는 경우, 해당 프로퍼티 자체에 대해 makeObservable을 호출해야 한다.
- options 인수는 한 번만 제공할 수 있다. 전달된 options는 고정되어 나중에 변경할 수 없다.(예 : 서브클래스)
- 모든 필드는 한 번만 어노테이션 할 수 있다.(재정의 제외) 필드 어노테이션 또는 구성은 하위 클래스에서 변경할 수 없다.
- 일반 객체(클래스)가 아닌 모든 어노테이션이 달린 필드는 구성할 수 없다. configure({safeDescriptors : false})로 비활성화할 수 있다.
- 모든 관찰할 수 없는 (state가 없는) 필드 (action, flow)는 쓰기가 불가능하다. configure({safeDescriptors : false})로 비활성화 할 수 있다.
- 프로토타입에 정의된 action, computed, flow, action.bound만 서브클래스로 재정의할 수 있다.
- 기본적으로 타입스크립트에서는 private 필드에 어노테이션을 달 수 없다. 다음과 같이 관련 private 필드를 일반 인수로 명시적으로 전달하면 해당 문제를 해결할 수 있다. ➡️ makeObservable<MyStore, "privateField" | "privateField2">(this, {privateField : observable, privateField2 : observable})
- 추론 결과를 캐시 할 수 있으므로 make(Auto)Observable을 호출하고 어노테이션을 제공하는 것은 무조건 수행해야 한다.
- make(Auto)Observable을 호출한 후 프로토타입을 수정하는 것은 지원하지 않는다.
- EcmaScript private 필드는 지원하지 않는다. 타입스크립트를 사용하는 경우 private 수정자를 사용하는 것이 좋다.
- 단일 상속 체인 내에서 어노테이션과 데코레이터를 혼합하는 것은 지원되지 않는다.(예 : super 클래스에는 데코레이터를, 서브 클래스에서는 어노테이션을 사용할 수 없다.)
- 다른 내장 observable 유형(observableMap, observableSet, observableArray 등)에는 makeObservable, extendObservable을 사용할 수 없다.
- makeObservable(Object.create(prototype))은 프로토타입에서 생성된 객체로 프로퍼티를 복사하여 관찰할 수 있게 만든다. 이 동작은 예상치 못한 잘못된 동작이므로 더 이상 사용되지 않으며 향후 버전에서 변경될 가능성이 높다. 이 함수에 더 이상 의존하지 말자.
[ options : 옵션 ]
위의 API는 다음 옵션을 지원하는 객체인 선택적 옵션 인수를 받는다.
- autoBind : true는 기본적으로 action / flow가 아닌 action.bound / flow.bound를 사용한다. 명시적으로 어노테이션 된 멤버에는 영향을 주지 않는다.
- deep : false는 기본적으로 observable 대신 observable.ref를 사용한다. 명시적으로 어노테이션이 달린 멤버에는 영향을 주지 않는다.
- name : <string>은 오류 메시지 및 리플렉션 API에 출력되는 디버그 이름을 객체에 부여한다.
- proxy : false는 observable(thing)이 프록시가 아닌 비프록시 구현을 사용하도록 강제한다. 프록시되지 않은 객체가 디버깅하기 쉽고 빠르므로 시간이 지나도 객체의 모양이 변하지 않는 경우 좋은 옵션이다.
참고 : 옵션은 고정이며 한 번만 제공할 수 있다.
옵션 인수는 아직 관찰할 수 없는 target에 대해서만 제공할 수 있다. 관찰 가능한 객체가 초기화되면 옵션을 변경할 수 없다.
옵션은 target에 저장되며 후속 makeObservable/extendObservable 호출에 의해 존중된다. 서브 클래스에 다른 옵션을 전달할 수 없다.
[ observable을 다시 vailla JavaScript로 변환하기 ]
관찰 가능한 데이터 구조를 바닐라 자바스크립트 데이터 구조로 다시 변환해야 할 때가 있다. 예를 들어 관찰 가능한 객체를 관찰 가능한 객체를 추적할 수 없는 리액트 컴포넌트에 전달하거나 더 이상 변경해서는 안 되는 복사본을 얻으려는 경우이다.
컬렉션을 얕은 복사를 하려면 일반적인 자바스크립트 메커니즘을 적용하자.
const plainObject = { ...observableObject }
const plainArray = observableArray.slice()
const plainMap = new Map(observableMap)
데이터 트리를 일반 객체로 재귀적으로 변환하려면 toJS 유틸리티를 사용할 수 있다. 클래스의 경우 JSON.stringify에 의해 선택되므로 toJSON() 메서드를 구현하는 것이 좋다.
[ 학습에 대한 간단한 참고 사항 ]
지금까지 대부분의 예제는 클래스 구문으로 기울어져 있다. MobX는 원칙적으로 이에 대해 반대하지 않으며, 일반 객체를 사용하는 MobX 사용자도 많을 것이다. 그러나 클래스의 약간의 이점은 타입스크립트와 같은 API를 더 쉽게 찾을 수 있다는 것이다. 또한 instanseof 체크는 타입 추론에 매우 강력하며 클래스 인스턴스는 프록시 객체로 래핑 되지 않으므로 디버거에서 더 나은 환경을 제공한다. 마지막으로 클래스는 모양을 예측할 수 있고 메서드가 프로토타입에서 공유되므로 많은 엔진 최적화의 이점을 누릴 수 있다. 그러나 무거운 상속 패턴은 쉽게 발목을 잡을 수 있으므로 클래스를 사용하는 경우 클래스를 단순하게 유지하자. 따라서 클래스를 사용하는 것을 선호하더라도 자신에게 더 적합하다면 이 스타일에서 벗어날 것을 권장한다.
'React > 상태관리하기' 카테고리의 다른 글
MobX 톺아보기 4 - MobX core - Reactions (0) | 2023.02.14 |
---|---|
MobX 톺아보기 3 - MobX core - Computed (0) | 2023.02.14 |
MobX 톺아보기 0 - 컨셉, 원칙 (0) | 2023.02.13 |
Redux 톺아보기 2 - (비동기 로직) (0) | 2023.02.11 |
Redux 톺아보기 1 - (redux-toolkit) (0) | 2023.02.10 |