MobX with React

이전 글에서는 React를 배제한 채로 MobX만 사용했었고, Angular 혹은 jQuery와 연계해서 사용할 수도 있지만 MobX는 React랑 함께 사용되는 게 가장 일반적이다. 이번 글에서는 MobX와 React를 연계해서 사용하는 내용에 대해서 다룬다. 만약 이전 글을 읽지 않았다면, 먼저 이 글을 읽기 전에 읽기를 권한다.

MobX와 React를 같이쓰려면 물론, MobX와 React 모듈이 필요하고, 거기에 더불어서 mobx-react라는 모듈을 설치하면 시작할 수 있다. 이 mobx-react라는 라이브러리는 API도 별로 없고 하는 일도 간단하다. React 컴포넌트를 Observable 데이터에 반응하도록 만들어주는 것이다.

observer

그 몇 안되는 API 중에 가장 자주 사용하게 될 것은 observer이다. 코드를 통해 간단히 어떻게 동작하는지 알아보자.

위 코드에서는 Observable 값으로 JavaScript 기본 자료형을 사용했기 때문에 값을 직접 변경하지 못하고, 별도의 get, set 메소드를 활용해야 했다.

결과를 확인해보면 이전 글에서 autorun을 사용했을 때 처럼 데이터가 변경될 때마다 뷰가 업데이트되고 있는 것을 확인할 수 있다. 실제로 MobX 문서의 설명에도, “mobx.autorun에 컴포넌트의 렌더 메소드를 래핑해서 컴포넌트를 렌더링 할 때 사용되는 데이터가 변경되었을 때 다시 렌더링하게 만든다”고 설명하고 있다. 즉 간단히 observer만 선언해주면 render 메소드가 autorun(render) 가 되는 것이다.

어떤 컴포넌트에 observer를 사용해야 하는가?

실제로 어플리케이션을 구현하다보면 observer를 상당히 많이, 거의 모든 컴포넌트에 선언하게 되는데, 이게 렌더링 성능에 악영향을 끼치지 않을까 걱정이 되기도 한다. MobX에서는 observer라는 함수 혹은 데코레이터를, Observable 데이터를 렌더하는 모든 컴포넌트에 선언하는 걸 추천한다. MobX에서 이를 최적화하기 때문이다. 만약 자식 컴포넌트에서 Observable 값을 표시하면서 자식 컴포넌트와 부모 컴포넌트가 동시에 observer를 가지고 있다고 해도, 해당 데이터를 자식 컴포넌트만 다시 렌더링된다. 그러니까 정말 필요한 컴포넌트만 반응(렌더)을 하도록 만든다고 한다.

Dumb Component에 observer를 선언하게 되면 더 이상 Dumb하지 않지 않는가? 라는 의견도 있었는데, MobX의 창시자인 Michel Weststrate의 의견에 따르면 observer를 선언하는 것이 Dumb와 Smart를 가르는 기준은 아니라고 한다. 이 스레드를 참조.

Stateless Functional Component와의 사용

위의 예제는 클래스를 이용해 React 컴포넌트를 만들어서 데코레이터로 감쌀 수 있었지만, 함수를 이용해 React 컴포넌트를 만들 경우에는, observer를 함수로 사용해서 컴포넌트를 감싸야 한다.

컴포넌트 내부 변수로 observable 사용하기

MobX를 React에 적용해서 좋은 점 중 하나는 더 이상 setState를 사용하지 않고도 컴포넌트에 내부 상태를 만들고, 변이시킬 수 있다는 점이다. 스토어를 새로 만들 필요도 없다. 그냥 컴포넌트 안에 Observable 값을 하나 선언해주기만 하면 끝이다.

코드를 보면 컴포넌트 안에 선언된 count라는 변수는 해당 예제에서 사실상 내부 상태로 활용되고 있는데, 순수 React만 사용해서 이런 걸 표현하려면 state를 활용해야 했다. MobX를 이용하면 내부상태를 표현할 때도 그냥 일반적인 클래스 사용하듯 너무 쉽게 코드를 짤 수 있다.

Michel Weststrate도 setState의 단점들과 그것을 MobX로 어떻게 해결했는지에 대해 을 남겼으니 참고하시기 바란다.

<Provider>inject

MobX가 Redux처럼 단일 스토어를 권장하지는 않지만, 특별히 중복을 좋아하는 게 아니라면 어떤 하나의 데이터를 관리할 스토어를 여러 개 만들 필요는 없을 것이다. 이는 결국 스토어를 특정 컨텍스트에서만 초기화하도록 만드는데, React의 컴포넌트 프로퍼티로 스토어를 일일히 내려주려면 이만저만 귀찮은 것이 아닐 것이다. mobx-react 패키지는 이를 위해서 간단한 의존성 주입을 위한 문법 설탕을 제공한다. 그것이 바로 <Provider>inject이다.

예제 코드를 보면 특별히 <Counter> 컴포넌트에 프로퍼티를 선언해 준 것도 아닌데 스토어가 this.props에 들어와서 컴포넌트 내부에서 사용하고 있는 것을 볼 수 있다. <Counter> 상위에 <Provider>를 선언해주고, inject를 데코레이터로 사용해 간단히 의존성을 주입했다. <Provider>는 반드시 최상단에 위치하거나 부모일 필요는 없지만 의존성 주입이 필요한 컴포넌트보다는 상위에 존재해야 한다. 그리고 컴포넌트의 상단에 inject로 필요한 의존성을 선언하기만 하면 된다.

주입될 수 있는 의존성은 반드시 MobX 스토어일 필요가 없다. JavaScript 기본 자료형이든 뭐든 넘길 수 있다. 다만 이 값은 더 이상 변경되지 않아야 하는 값이다. 값이 변경된다고 해서 새로운 값이 주입되지는 않는다.

또 하나의 주의할 점은 @inject@observer보다 상단에 위치해야만 한다는 것이다. 그러니까 @observer가 보다 안쪽의 데코레이터가 되어야 한다. 이것은 데코레이터가 아닌 함수 문법의 경우도 동일하다.

성능 최적화

observer는 기본적으로 render 메소드에 autorun을 씌운 것이다보니, 조금만 잘못짜도 render가 비효율적으로 동작할 수 있게된다.

위의 예제에서는 1초가 지날때마다 seconds 값이 1번 변하고, tenth값은 10번 변하므로 1초에 약 11번의 렌더링이 일어난다. 변하는 값을 뷰에 업데이트 해야하기 때문에 1초에 11번의 렌더는 사실 어쩔 수 없다. 하지만 렌더되는 영역은 좀 비효율적이다.

seconds 값이 변했을 때, 진짜 렌더가 필요한 영역은 오직 28번째 줄에 있는 코드뿐인데, <Timer> 컴포넌트에서 렌더하는 모든 영역이 다시 렌더되고 있다. 이는 tenth 값이 변했을 떄도 마찬가지다. tenth 값이 변했을 때 seconds 값이 표현되는 뷰가 다시 렌더링되고, seconds값이 변했을 떄 tenth 값이 표현되는 뷰가 다시 렌더링되는 것이다.

MobX에서는 이런 케이스를 위해서 컴포넌트를 굉장히 잘게 쪼개라고 가이드한다. 진짜 필요한 부분만 다시 렌더링하도록 만들라는 것이다. 이제 아래의 개선된 버전을 보자.

개선된 버전에서는 더 이상 <Timer> 컴포넌트의 렌더 메소드가 secondstenth 값의 영향을 받지 않는다. 따라서 <Timer>는 딱 1회만 렌더되는 것이다.

물론, 여전히 <SecondsTimer><TenthTimer>는 수시로 렌더링되지만, 더 이상 그 값의 변화가 다른 컴포넌트의 렌더링에 영향을 미치지 않는다. 따라서 각 컴포넌트의 렌더링 횟수도 통째로 렌더링 되었을 때보다는 적다.

여기에 있는 예제는 굉장히 축소된 예제로, 실제로 렌더 비용이 높은 컴포넌트가 있는데 이 컴포넌트가 <input> 값 등에 의해 수시로 렌더링이 다시 된다면 그게 얼마나 끔찍하게 비효율적인지는 다들 공감할 수 있을 것이다.

이런 사항은 MobX를 사용하지 않더라도 충분히 일어날 수 있는 일이지만 MobX에서는 render가 묵시적으로 일어나고, 일으키기가 매우 쉽기 때문에 MobX를 쓸 때 더 쉽게 범하는 실수가 된다.

이외에도 다른 성능 최적화 기법들이 있는데 이 링크를 참고하기 바란다.

사족

mobx-react 자체가 그리 많은 API를 가지고 있지 않다보니 생각보다 어렵지 않게 MobX와 React를 같이 시작해볼 수 있을 것이다. 하지만 이것을 잘 사용하고, 디자인하는 것은 그냥 시작하는 것보다 훨씬 더 고차원의 어려움이 따른다. MobX는 꽤 자유로운 라이브러리다보니 위처럼 최적화에 실패한 케이스라던지, 잘못된 디자인의 스토어를 짜게 되는 경우도 생긴다. 설상가상으로 레퍼런스도 아직은 많이 부족한 편이다.

그렇기에 내가 실제로 지금 진행하는 프로젝트에서 처음 MobX를 접하고 일주일 정도는 스토어의 전체적인 디자인을 수시로 갈아엎었다. 지금은 대충 안정적으로 사용하고 있지만, 언제 또 갈아 엎게 될지 모르겠어서 불안하다. MobX에서도 이를 어느정도 인지하고 있는지, Best Practice 문서를 제공하고 있다. 실제 프로젝트에서 반드시 참고하기를 권한다.