상태관리 라이브러리 - Mobx 업그레이드 자료조사(Mobx@4 -> @5 vs @6 )
개요
현재 회사에서 사용중인 상태 관리 라이브러리는 Mobx@4
로 Node@14
를 기반으로 세팅되어 있다.
최근 Node
버전을 14 -> 18 로 올리는 중인데, 기존에 사용되던 Mobx
와의 버전이 맞지 않아서 Mobx
도 업그레이드를 진행하게 되었다.
Node@18
은 Mobx@5
와 Mobx@6
을 지원하기 때문에 어떤 버전으로 업그레이드 하는 것이 좋은지 자료조사를 하게 되었다.
각 버전별 변경사항
Mobx@5
가장 큰 변화는 Proxy
를 지원하는 것이다.
Mobx@6
makeObservable
가장 큰 변화는 기존에 사용되던 decorator
가 deprecated
되어 더이상 사용할 수 없으며, makeObservalbe
메서드를 통해서 class 내부의 프로퍼티 값들을 observable
로 변경해야한다.
코드가 여러군데로 산개되며 사용하기 기존보다 사용하기 어려운 면이 있다.
Mobx@4
class Developer {
@observable
name = `space.developher`;
@computed
get sayName(){
return `${this.name} hello`
}
@action
changeName(name){
this.name = name
}
}
Mobx@6
class Developher {
constructor(){
makeObservable(this, {
name: observable,
changeName: action,
sayName: computed
})
}
name = `space.developher`;
get sayName(){
return `${this.name} hello`
}
changeName(name){
this.name = name
}
}
단, 상속 extends
할 경우에는 부모 클래스super class
에서 makeObservable
을 선언하며, 자식 클래스 sub class
에서 새로운 observable
프로퍼티를 추가할 경우에는 자식 클래스 내부에서 makeObservable
을 통해 매핑 해줘야한다.
class SuperClass {
constructor(){
makeObservable(this, {name: observable})
}
name = `name`
}
class SubClass extends SuperClass {
constructor(){
makeObservable(this {classType: observable })
}
name = `subName`
classType = `sub`
}
makeAutoObservable
makeAutoObservable
메서드는 makeObservable
메서드와 유사하지만 더 장점이 많은 메서드이다.
- 프로퍼티 및 메서드 등의 속성 추론
overrides
를 통한 기본 동작을 재정의 가능- 단, 부모 클래스 (super class) 나 자식 클래스 (sub class) 에서는 사용할 수 없다.
class Developher {
constructor(){
makeAutoObservable(this)
}
name = `space.developher`;
get hello() {
return `${this.name} Hello`;
}
changeName(name) {
this.name = name;
}
}
예제에서 알 수 있듯 makeObservable
보다 사용하기 간편하다.
하지만 상속 extends
을 받지 못한 점은 아쉽다.
상속받을 경우 다음과 같은 에러가 발생한다.
class Developher {
constructor(){
makeAutoObservable(this)
}
name = `origin space.developher`;
get hello() {
return this.name;
}
changeName(name) {
this.name = name;
}
}
class Developher2 extends Developher {
constructor(){
makeAutoObservable(this)
}
name = `space.developher`;
get hello() {
return `${this.name} Hello`;
}
changeName(name) {
this.name = name;
}
}
Uncaught Error: [MobX] 'makeAutoObservable' can only be used for classes that don't have a superclass
Mobx@4 -> Mobx@6 방법
데코레이터 변경
앞서 이야기한대로 Mobx@6
에서는 데코레이터를 지원하지 않는다.
데코레이터는 별도의 모듈인 mobx-undecorate
를 통해서 자동 변환이 가능하다.
npx mobx-undecorate
주의사항
mobx-undecorate
는 사용자가 임의로 생성한 데코레이터
까지 변환시켜버린다.
현재 회사에서 사용중인 lit
은 property
를 통해 상태값을 관리하는데, 이것은 데코레이터 기반이다.
위의 내용을 보면 알 수 있듯이 property
가 mobx
의 observable
등의 속성이 아님에도 변환된 것을 알 수 있다.
위의 상황에서 해결방법은 npx
를 동작할 때 디렉토리 경로를 설정하여 mobx
가 동작하는 파일만 변환되도록 해야한다.
npx mobx-undecorate <path>
회사에서는 Mobx
를 Store class
단위로 생성 및 호출하여 Store
를 변환작업 시켜주었다.
npx mobx-undecorate ./src/stores
Processing 187 files...
Spawning 7 workers...
Sending 27 files to free worker...
Sending 27 files to free worker...
Sending 27 files to free worker...
... 생략
All done.
Results:
0 errors
0 unmodified
96 skipped
91 ok
Time elapsed: 2.832seconds
주의사항2
테스트를 위해서 현재 사용하고 있는 하나의 컴포넌트를 변환해보았다.
해당 컴포넌트가 사용하는 store
에서 값이 존재하지 않는 필드가 존재하는데, mobx-undecorate
변환 과정에서는 별도의 문제가 발생하지 않았으나, 실제 페이지에 접근하자 에러가 발생한 것을 확인했다.
mobx.esm.js:85 Uncaught Error: [MobX] Cannot apply 'observable' to 'store@1.disableButton': Field not found.
at die (mobx.esm.js:85:1)
at ObservableObjectAdministration.make_ (mobx.esm.js:4686:1)
at mobx.esm.js:3347:1
at Array.forEach (<anonymous>)
at makeObservable (mobx.esm.js:3346:1)
at new StoreActivation (store.ts:23:1)
at new PageActivation (component.ts:16:1)
확인해보니 에러가 발생한 store
내의 disableButton
프로퍼티는 값이 초기값이 할당되지 않은 필드였다.
// 프로퍼티가 선언된 필드. 초기화 되지 않은 필드이다.
disableButton: boolean;
아무래도 초기값이 없는 경우에 변환 이후 에러가 발생하는 것으로 판단되며, 일괄적으로 mobx-undecorate
를 하기에는 위험이 있음을 인지하였다.
페이지와 스토어를 맞춰서 테스트하며 진행해야할 것 같다.
데코레이터 유지한 상태로 mobx@6으로 올리기
npx mobx-undecorate
모듈의 플래그 옵션 값으로 --keepsDecorators
가 존재한다.
데코레이터를 유지한 상태에서 Mobx
를 사용하는 클래스의 필드값을 observable
로 변환시킬 수 있다.
명령어는 다음과 같다.
npx mobx-undecorate --keepDecorators
변경시 아래의 코드처럼 생성자 constructor
에 makeObservable
이 선언되어 변환된다.
class Developher {
constructor(){
makeObservable(this)
}
@observable
name = `space.developher`;
@computed
get _name(){
return this.name
}
@action
onChangeName(name){
this.name = name
}
}
주의사항3
변환 후 observableArray
와 관련한 에러가 발생하였다.
mobx.esm.js:2223 [mobx] Encountered an uncaught exception that was thrown by a reaction or observer component, in: 'Reaction[SideBar.update()]' Error: [MobX] [mobx] `observableArray.sort()` mutates the array in-place, which is not allowed inside a derivation. Use `array.slice().sort()` instead
at die (mobx.esm.js:85:1)
at Proxy.sort (mobx.esm.js:3780:1)
at Store.dataToMenu (store-menu.ts:175:1)
at get globalMenu (store-menu.ts:38:1)
at trackDerivedFunction (mobx.esm.js:1676:1)
at ComputedValue.computeValue_ (mobx.esm.js:1472:1)
at ComputedValue.trackAndCompute (mobx.esm.js:1449:1)
at ComputedValue.get (mobx.esm.js:1418:1)
at ObservableObjectAdministration.getObservablePropValue_ (mobx.esm.js:4559:1)
at Store.get (mobx.esm.js:5054:1)
확인해보니 observableArray
는 Proxy 객체
로 관리되고 있었다.
이에 따라 Array
의 빌트인 메서드를 사용할 수 없었다.
아래의 경우는 Array
의 빌트인 메서드인 reverse
사용을 시도하였다.
// Store
class Test(){
constructor(){
makeObservable(this);
}
@observable
array = [1, 2, 3];
}
// Render Component
...
class Component extends MobxLitElement {
test = new Test()
render(){
return html`<div>${this.test.array.reverse().map(el => html`${el}`)}</div>`
}
}
당연하게도 에러가 발생한다.
[mobx] Encountered an uncaught exception that was thrown by a reaction or observer component, in: 'Reaction[PageNodes.update()]' Error: [MobX] [mobx] `observableArray.reverse()` mutates the array in-place, which is not allowed inside a derivation. Use `array.slice().reverse()` instead
at die (mobx.esm.js:85:1)
at Proxy.reverse (mobx.esm.js:3771:1)
at PageNodes.render (page.ts:23:1)
at PageNodes.update (lit-element.js:6:1)
at trackDerivedFunction (mobx.esm.js:1676:1)
at Reaction.track (mobx.esm.js:2194:1)
at PageNodes.update (mixin.js:50:1)
at PageNodes.performUpdate (reactive-element.js:6:1)
at PageNodes.scheduleUpdate (reactive-element.js:6:1)
at PageNodes._$Ej (reactive-element.js:6:1)
해결방법은 두 가지가 있다.
첫번째로 toJS
모듈을 사용하는 것이다.
Proxy
로 변환된 Object
값을 인수로 전달할 때 toJS 로 감싸주면 정상적으로 프로퍼티에 접근할 수 있다.
@observable
menus = null;
@computed
get _menus(){
return toJS(this.menus)
}
따라서 observableArray
를 사용하는 경우에는 computed
로 observable 프로퍼티
를 캐싱해서 toJS
로 반환하면 일반 Array
처럼 사용할 수 있게된다.
두번째도 Proxy.slice()
메서드를 이용하는 것이다.
정확히는 ObservableOArray
는 Proxy
객체가 되고 이 객체 내에 slice
메서드가 존재한다.
해당 메서드를 사용하면 일반 Array
처럼 사용할 수 있게된다.
// Store
@observable
array = [1, 2, 3];
// Render Component
render() {
// slice 메서드 적용 후에는 Array 에서 사용가능한 빌트인 메서드 등을 사용할 수 있다.
return html`${this.test.array
.slice()
.reverse()
.map(el => html`${el}`)}`;
}
주의사항4
npx mobx-undecorate
의 플래그 옵션인 --keepsDecorators
를 사용할 경우 ObservableObject
의 deep Depth
의 변경사항을 observe
하지 못하는 상황이 발생하였다.
ObservableObject
내의 Deep Depth
에 존재하는 프로퍼티 중 배열을 순회해서 UI 를 구현하는 내용이 존재한다.
배열에 내용을 추가/삭제
하도록 기능이 구현되어 있는데, 추가/삭제를 하여도 화면에 렌더링이 되지 않는 현상이 발생했다.
현재는 강제적으로 렌더링 엔진을 업데이트하는 메서드를 호출하는 형태로 해결하였지만, 별도로 해결책을 찾아야할 것 같다.
단, --keepsDecorators
플래그 옵션을 제거하여 변환을 할 경우 추가/삭제 기능은 정상적으로 동작한다.
개발자 도구에서의 경고 문구
action
이 이뤄지는 구간에서 경고문구가 등장하였다.
[MobX] Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed. Tried to modify: store@9.eventAction
동작은 정상적으로 이뤄지나 경고문구가 발생하기 때문에 수정이 필요해보인다.
observable
을 변경할 때는 action
을 무조건 사용해야하며, 비동기적으로 값을 변경할 때에는 해당 할당 구간에 runInAction
메서드를 통해서 할당해야한다.
@observable 이 아닌 데코레이터는 변환되지 않는 문제점
데코레이터를 변환하려는 store
중에는 상위 store 를 상속 extends
받아 computed
의 캐싱값을 사용하는 store
가 존재한다. (그래프 관련 스토어)
해당 store
는 상위 store
의 observable
을 바라보며 computed
와 action
를 생성하기 때문에 별도로 observable
이 존재하지 않는다.
나는 전역으로 npx 를 동작하였고, mobx 데코레이터라면 변환되기를 기대했으나, 실제로는 변환되지 않았다.
npx mobx-undecorate src/stores/graph-store
npx: 325개의 패키지를 10.294초만에 설치했습니다.
Processing 2 files...
Spawning 2 workers...
Sending 1 files to free worker...
Sending 1 files to free worker...
All done.
Results:
0 errors
0 unmodified
2 skipped
2개의 파일이 skipped
된 것을 볼 수 있다.
추측하건데 @observable
이 존재하는 파일을 탐지해서 변환시켜주는 것으로 보인다.
undecorating
후 전역으로 검색해서 mobx 데코레이터가 존재하는지 확인하는 것을 추천한다.
레퍼런스
Mobx-Changelog
Mobx@5 Proxy Docs
Mobx@6 observable
Mobx@6 Observable.Array.slice()
stackoverflow: npx-prefix
stackoverflow: mobx object deep change
stackoverflow: mobx-action-warning
npx directory path specify