프론트엔드

상태관리 라이브러리 - Mobx 업그레이드 자료조사(Mobx@4 -> @5 vs @6 )

space.developher 2023. 2. 20. 16:39
반응형

개요

현재 회사에서 사용중인 상태 관리 라이브러리는 Mobx@4Node@14 를 기반으로 세팅되어 있다.
최근 Node 버전을 14 -> 18 로 올리는 중인데, 기존에 사용되던 Mobx 와의 버전이 맞지 않아서 Mobx 도 업그레이드를 진행하게 되었다.

Node@18Mobx@5Mobx@6 을 지원하기 때문에 어떤 버전으로 업그레이드 하는 것이 좋은지 자료조사를 하게 되었다.

각 버전별 변경사항

Mobx@5

가장 큰 변화는 Proxy 를 지원하는 것이다.

Mobx@6

makeObservable

가장 큰 변화는 기존에 사용되던 decoratordeprecated 되어 더이상 사용할 수 없으며, 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 메서드와 유사하지만 더 장점이 많은 메서드이다.

  1. 프로퍼티 및 메서드 등의 속성 추론
  2. overrides 를 통한 기본 동작을 재정의 가능
  3. 단, 부모 클래스 (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 는 사용자가 임의로 생성한 데코레이터 까지 변환시켜버린다.
현재 회사에서 사용중인 litproperty 를 통해 상태값을 관리하는데, 이것은 데코레이터 기반이다.

위의 내용을 보면 알 수 있듯이 propertymobxobservable 등의 속성이 아님에도 변환된 것을 알 수 있다.
위의 상황에서 해결방법은 npx 를 동작할 때 디렉토리 경로를 설정하여 mobx 가 동작하는 파일만 변환되도록 해야한다.

npx mobx-undecorate <path>

회사에서는 MobxStore 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

변경시 아래의 코드처럼 생성자 constructormakeObservable 이 선언되어 변환된다.

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)

확인해보니 observableArrayProxy 객체 로 관리되고 있었다.
이에 따라 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 를 사용하는 경우에는 computedobservable 프로퍼티 를 캐싱해서 toJS 로 반환하면 일반 Array 처럼 사용할 수 있게된다.

두번째도 Proxy.slice() 메서드를 이용하는 것이다.
정확히는 ObservableOArrayProxy 객체가 되고 이 객체 내에 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 를 사용할 경우 ObservableObjectdeep 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 는 상위 storeobservable 을 바라보며 computedaction 를 생성하기 때문에 별도로 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

반응형