Simple, scalable state management

MobX는 프론트엔드를 위한 어플리케이션 상태 관리 라이브러리다. 주로 React에 바인딩되어 사용되고, 상태 관리 라이브러리라는 특성 때문에 종종 Redux와 비교된다. Redux가 함수형 아이디어를 적용했다면 MobX는 반응형 아이디어를 적용했다. MobX에서는 반응형 프로그래밍 패러다임에서 필수적으로 언급되는 옵저버(Observer) 패턴을 적극적으로 사용한다. 다만, MobX에서는 오직 상태(데이터)만 관찰 가능하며, 상태가 변경되었을 때 반응한다. 또한 상태를 변경시키는 것도 직접 해야한다. 한마디로 말해, MobX는 상태를 Observable하게 관리할 수 있도록 돕는 라이브러리다.

Getting Started

간단한 예제로 시작하자. 참고로 Result 탭을 누르면 컴파일 시간이 필요해서 그런지 동작까지 꽤 시간이 걸릴 수 있다.

위의 예제에서는 observable이라는 메소드를 통해 특정 객체를 Observable 객체로 만들었다. MobX에서 Observable 객체는, 그 객체의 프로퍼티 값이 변경되었을 때 그 객체를 관찰하고 있는 Observer에게 변경된 값을 통지한다. 이 경우에는 autorun을 통해서 관찰하는 것이고, 그 내부의 익명함수가 바로 Observer에 해당한다. 따라서 이 코드를 동작시키면, 1초마다 person.age가 1씩 증가하고, 이 값은 변경되었기 때문에 관찰하는 autorun 내부 코드에 의해서 자동으로 값이 업데이트된다. 이것을 MobX에서는 Reaction이라고 부른다.

API Overview

위에서 짤막하게 언급한 몇몇 API와 더불어서 MobX의 개념을 알아보기 위해 중요한 API만 간단히 둘러본다. 이 글에서는 우선 React는 배제하고 MobX에서 기본적으로 제공하는 API만 다룬다. 또한 개념 위주로 알아보고 디테일한 건 설명하지 않겠다.

(@)observable

observable은 넘겨받은 객체나 값 등을 Observable하게 만든다. 주로 객체를 넘기거나 클래스의 내부에서 데코레이터로 사용하게 된다.

위의 예제처럼 데코레이터 문법을 활용할 수 있다면 간단히 @observable을 프로퍼티에 선언해줌으로써 Observable한 값으로 만들 수 있다. 예제를 실행시키면 나이 값은 1초마다 업데이트되지만 이름 값은 업데이트되지 않는 것을 확인해 볼 수 있다.

observable을 사용하게 되면 프로퍼티를 읽거나 쓰는 것이 모두 값을 관찰하는 것과 연관된다. 즉, person.age같은 코드로 값을 얻는 것은 옵저버를 등록하고, 실제로 그 값을 관찰(observe)하는 행위가 된다. 반대로 person.age = 10같은 코드로 값을 할당하는 것은 그 값을 관찰하는 모든 옵저버들에게 통지(notify)하는 행위가 된다.

이게 가능한 이유는 observable로 만든 객체가 실제로 프로퍼티가 아닌 JavaScript의 Getter/Setter를 사용하기 때문이다.

(@)computed

computedobservable값이 파생되는, 특별히 계산된 결과가 필요한 경우에 사용한다. MobX의 개발자인 Michel Weststrate는 MobX를 스프레드시트에 비유하는 걸 좋아하는데, Observable 값이 스프레드시트에 있는 데이터이고, Computed는 일종의 수식이라는 것이다. 다음 예제를 통해 살펴보자.

computed는 이렇게 Observable 값에 대해서 적절한 계산이 필요할 때 사용하는 API다. Observable 값이 변경되면 그 값이 파생되어 Computed 값도 변경되고 이렇게 변경되는 값 역시 참조할 수 있다.

그런데 이 코드는 조금 이상해보인다. 결과에서도 볼 수 있듯이, @computed를 쓰든 쓰지않든 별 차이없이 값은 양 쪽 모두 변경되고 있다. 그럼 computed는 왜 존재하는 걸까?

이슈를 참고하면 해답을 구할 수 있다. 간단히 옮기면, 동작에는 큰 차이가 없지만 성능상 차이는 존재한다는 것이다. 위의 예제에서 getArea를 여러 번, 극단적으로 100번 정도 호출하는 경우를 상상해보자. width 혹은 height 값이 변경되는 경우 getArea가 100번 호출되고 그 안의 계산식 역시 100번 반복되고 이는 당연히 낭비다. 반대로 @computed를 사용했다면 계산 결과 값은 캐싱된다. 따라서 100번의 반복은 없고 @computed 내부의 동작이 무거울 수록 성능상 이점이 더 커진다.

Computed는 기본적으로 JavaScript의 Getter에만 사용할 수 있으며, 따라서 추가 인자를 받을 수가 없다. 입력 인자가 this로 제한되는 순수함수라고 생각하면 이해하기 편하다.(물론 순수함수는 아니지만)

autorun

autorun은 Observable 값이 파생된다는 점은 computed와 같지만, 용도는 전혀 다르다. 아까 위에서 Reaction에 대해서 언급한 적이 있을 것이다. 기본적으로 파생된 값을 가지고 View를 업데이트 한다거나 로그를 찍는다거나 하는, 사이드 이펙트를 내포하고 있는 동작을 MobX에서는 Reaction이라고 부른다. autorun은 Reaction을 하는 방법 중 하나다. React와 같이 쓰는 경우에는 다른 API로 Reaction을 할 수 있으므로 autorun을 사용할 일이 별로 없지만, React 없이 MobX만 사용하는 경우에는, autorun이 필수적이다. 그래서 앞에 등장한 모든 예제에 autorun이 등장했던 것이다.

autorun에 넘긴 익명 함수는 참조하고 있는 Observable 값이 변할때마다 반복해서 실행된다. 위에서도 언급했지만 Observer에 해당하는 것이다.

(@)action

action은 Observable 값을 변경하는(Setter)에 사용하는 API다. 기본적으로 MobX에서는 Observable 값을 변경하는 메소드에는 action을 달아줄 것을 권장하지만 쓰지 않아도 정상적으로 동작한다. 위의 예제들에서도 action 없이 값을 계속해서 업데이트해도 동작에는 문제가 없지 않는가?

그렇다면 자연스럽게 action은 또 무슨 이유로 사용해야 되는지 의문이 들 것이다. computed와 마찬가지로, action을 사용하는 이유도 성능이다. 간단한 예제를 통해 알아보자.

위의 예제에서는 incWidthAndHeight라는, 내부의 width 값과 height 값을 동시에 1씩 올리는 메소드를 만들었다. 그리고 내부 동작은 완벽히 같지만 그 함수의 action 버전도 만들었다. 1초에 한 번씩 메소드를 실행시키면서 Reaction 함수(autorun으로 감싸진 함수)가 각각 몇 번씩 실행되는지 볼 수 있게끔 했다. Result 탭에서 결과를 보면 알겠지만, getArea()의 출력결과는 같은 동시에, 각각의 Reaction 함수의 실행 횟수는 2배의 차이가 나는 것을 확인할 수 있다.

이는 action 없이 Observable 값을 업데이트 했을 떄는, widthheight 각각의 값이 업데이트 되는 시점에서 Reaction 함수가 호출되지만, action을 사용했을 때는 그 두 값이 모두 업데이트 된 뒤에야, Reaction 함수가 호출된다는 차이에서 기인한다. 이렇게 Observable 값을 업데이트하는 동작을 묶어 일괄 실행하고 모든 동작이 끝났을 때 통지하는 것을 MobX에서는 트랜잭션(Transaction)이라고 부른다. 개념은 조금 다르지만 데이터베이스의 그 용어와 같다.

이처럼 MobX에서는 트랜잭션을 사용하는 것과 사용하지 않는 것은 큰 성능차이를 불러일으킬 수 있기 때문에, 항상 Observable 값을 변경할 때마다 action을 강제할 수도 있다. 바로 useStrict 모드를 사용하는 것이다. useStrict 모드를 사용하면 Observable 값 변화에 반드시 action을 사용해야 한다.

Wrap up

MobX Flow Diagram

위의 다이어그램은 위에서 설명한 API의 생명 주기를 요약해서 나타내고 있다. Action으로 인한 상태(State) 변경은 곧, 계산 결과(Computed)를 업데이트한다. 계산 결과가 업데이트되면 곧 이를 구독하는 반응(Reaction)을 야기한다.

겨우 1주일 정도 써봤지만, 나는 MobX에 대해서, Redux에 비해서도 진입장벽이 낮고 쉽게 코드를 짜고 금방 사용할 수 있는데 비해서 제대로 활용하려면 애매하고 헷갈리는 부분이 많아 쉽지가 않다는 것을 느꼈다. 단순히 Best Practice를 찾은 뒤에 Copy & Paste하는 방식만으로는 이해도 잘 안되고 한계가 금방 온다. 따라서 처음부터 라이브러리에 기본 개념에 대해서 숙지하고 넘어가는 것이 좋다. 그래서 이 글을 쓰고 있는 것이기도 하고.

MobX는 얼핏 보기에 뭔가 성능상 문제가 생길 것 같아보인다. 원래부터 우려가 많았는지, MobX에서는 이를 React와 비교하면서 굉장히 최적화가 잘 되어있다고 변호한다.

React와 MobX는 모두 어플리케이션 개발에 있어서 겪는 일반적인 문제에 대해 굉장히 최적화된 특별한 해법을 제시합니다. React는 가상 DOM을 이용해서 DOM 변화를 감소시키는 최적의 렌더링 메커니즘을 제공합니다. MobX는 엄격하게 필요할 때만 업데이트 되는 반응적인 가상 의존성 상태 그래프를 이용해서 어플리케이션 상태를 React 컴포넌트와 동기화하는 최적의 메커니즘을 제공합니다.

진실은 나도 모르지만, 아무튼 상당한 자신감이 있는 모양이다. 실제로도 이미 사용하고 있는 프로젝트도 꽤 있고 검증은 적당히 된 상황이다. 물론 Redux에 비해서는 조금 갈 길이 멀어보이지만 일단은 응원하는 입장이다. 현재까지 단점이라고는 로고가 상당히 못 생겼다는 것과 레퍼런스가 별로 없다는 것 외에는 딱히 없었다.

다음 글은 MobX-React 바인딩에 대해서 써 볼 예정이다.