MobX
는 간단하면서도 뛰어난 확장성을 가진 상태 관리 솔루션입니다. 본 튜토리얼은 MobX의 가장 중요한 개념들을 10분 안에 이해할 수 있도록 작성되었습니다.
MobX는 독립적인 라이브러리지만 대부분 React와 함께 사용되고 있으므로, 본 튜토리얼도 해당 조합에 초점을 맞추어 진행합니다.
핵심개념
상태(state)는 모든 애플리케이션의 핵심이며, 애플리케이션을 관리하기 어렵게 만드는 지름길은 '일관되지 않는' 또는 '산재된 지역 변수들과 어울리지 않는' state를 생성하는 것입니다. 때문에 많은 상태 관리 솔루션들은 state를 변경 불가하게 만드는 등, 수정하지 못하도록 제한하는 방법을 시도했습니다. 하지만 그러한 방법은 또다시 새로운 문제를 발생시킵니다. 데이터는 정규화되어야 하며 참조 무결성(referential integrity) 또한 보장할 수 없고, 데이터가 필요한 경우에 클래스와 같은 효과적인 개념을 사용하는 것이 불가능해집니다.
MobX는 쉬운 state 관리를 위해 근본적인 문제를 해결했습니다. 바로 일관되지 않는 state가 애초에 생성되지 않도록 하는 것입니다. 전략은 간단합니다. 애플리케이션 내의 state에서 파생 가능한 모든 것이 파생되도록 합니다. 그것도 자동으로.
개념적으로 MobX는 애플리케이션을 스프레드시트처럼 다룹니다.
- 우선적으로 애플리케이션 state가 있습니다. 애플리케이션의 모델을 구성하는 객체, 배열, 원시 값(primitive), 참조(reference)의 그래프죠. 이 값들이 애플리케이션의 '데이터 셀'입니다.
- 두 번째로 derivation이 있습니다. 기본적으로 애플리케이션 내의 state로부터 자동으로 계산될 수 있는 모든 값입니다. 이러한 derivation 값, 또는 computed 값은 '완료되지 않은 todo 개수'와 같이 간단한 값부터 'todo에 관한 시각적 HTML 표현'과 같이 복잡한 것까지 모든 범위에 해당할 수 있습니다. 스프레드시트로 묘사하자면 이것들이 바로 애플리케이션의 수식과 차트입니다.
- reaction은 derivation과 굉장히 유사합니다. 가장 큰 차이점을 말하자면, reaction 함수는 값을 생성하지 않습니다. 대신 자동으로 특정 테스크를 수행합니다. 일반적으로 이것은 I/O와 관련이 있습니다. reaction은 DOM이 업데이트되었는지, 또는 네트워크 요청이 제때 자동적으로 이루어졌는지 확인합니다.
- 마지막으로 action입니다. action은 state를 변경하는 모든 것입니다. MobX는 action으로 인해 발생하는 모든 애플리케이션 state 변화가 derivation과 reaction에 기반해 자동적으로 이루어졌는지 확인해 줄 것입니다. 동기식으로, 결함 없이 말이죠.
간단한 TodoStore
이론은 충분했고, 상단의 글을 유심히 읽어보는 것보다는 직접 실행되는 모습을 보는 게 더 이해가 빠를 것입니다.
독창성을 위해 아주 간단한 TodoStore부터 시작해봅시다.
하단의 모든 코드들은 수정 가능하며 실행 버튼을 눌러서 결과를 확인할 수 있습니다.
아래는 todo 리스트를 포함하고 있는 간단한 TodoStore
입니다.
아직 MobX가 적용되지는 않았습니다.
이렇게 todo 리스트를 포함한 TodoStore
를 만들었습니다.
이제 이 TodoStore에 객체를 채울 차례입니다.
변화에 따른 결과를 확인하기 위해, 각각의 변화가 일어난 직후 todoStore.report
를 호출하고 로그를 찍었습니다.
리포트에서는 의도적으로 첫 번째 테스크만 출력됩니다. 때문에 본 예제가 약간 인위적으로 느껴질 수 있지만 나중에는 MobX의 의존성 추적(dependency tracking)이 그만큼 뛰어나다는 사실을 잘 보여줄 것입니다.
반응형으로 만들기
지금까지는 특별할 게 없는 코드입니다. 하지만 report
를 일일이 호출할 필요 없이 '각각의 관련(relevant) state가 변경되는 경우에 report를 호출한다'라고 선언할 수 있다면 어떨까요? 그렇게 되면 결과에 영향을 미칠 수도 있는 모든 부분에서 report
를 호출해야 할 필요가 없어집니다.
우리는 가장 최신의 결과가 출력되기를 원합니다. 하지만 그렇게 구성하는 데 시간을 뺏기고 싶지는 않죠.
운 좋게도 그게 바로 MobX가 해줄 수 있는 일입니다. state에 기반한 코드를 자동으로 실행합니다.
스프레드시트의 차트처럼 report
함수가 자동으로 업데이트 되도록 말이죠.
그러기 위해 TodoStore
는 발생한 모든 변화를 MobX가 감지할 수 있도록 observable 상태가 되어야 합니다.
이를 적용해보기 위해 클래스를 수정합시다.
또한 completedTodosCount
는 todo 리스트에서 자동으로 파생될 수 있을 것입니다.
observable
과 computed
주석을 사용함으로써 객체에 observable 속성을 도입할 수 있습니다.
하단의 예제에서는 주석을 명확하게 보여주기 위해 makeObservable
을 사용하지만 더 단순한 절차를 위해 makeAutoObservable(this)
를 사용할 수도 있습니다.
바로 이겁니다! 특정 속성을 observable
상태로 만들어서 MobX에게 '앞으로 이 값들이 변경될 수 있음'을 알렸습니다.
계산(computation)은 근원적인 state 변화가 일어나지 않는 한 해당 값들이 state 및 캐시에서 파생될 수 있음을 식별하기 위해 computed
로 꾸며집니다.
pendingRequests
와 assignee
속성은 아직까지 사용되지 않았지만, 본 튜토리얼 후반부에서 사용됩니다.
생성자에서 report
를 출력하는 작은 함수를 만들었고 그 함수를 autorun
으로 감쌌습니다. autorun은 한 번 실행되는 reaction을 만들고 이후에 함수 안에서 사용된 observable 데이터가 변경될 때마다 자동으로 reaction을 재실행합니다. report
가 observable한 todos
속성을 사용하기 때문에 필요할 때마다 report를 출력할 것입니다. 이는 하단에 설명되어 있습니다. 실행 버튼을 눌러보세요.
흥미롭지 않나요? report
는 자동으로, 동기적으로 그리고 중간값 유출 없이 인쇄되었습니다.
로그를 유심히 분석해보면 다섯 번째 줄은 로그가 찍히지 않은 것을 알 수 있습니다.
테스크의 이름을 변경함으로써 백업 데이터가 변경되었지만 실질적으로 report가 변경되지는 않았기 때문입니다.
반면 인덱스 1번 todo의 이름은 report에서 활성화되어있었기 때문에 해당 이름을 변경하면 report가 업데이트됩니다.
이는 autorun
이 todos
배열뿐만 아니라 todo 항목 내의 개별 속성까지 감지하고 있었음을 잘 설명해 줍니다.
반응형 React 만들기
좋습니다. 지금까지는 조금 우스운 반응형 report를 만들었습니다. 이제부터 똑같은 TodoStore에 반응형 유저 인터페이스를 입혀봅시다.
React 컴포넌트들은 그 이름에도 불구하고 즉시 반응적이지는 않습니다.
mobx-react-lite
패키지의 observer
HoC 래퍼는 기본적으로 React 컴포넌트를 autorun
으로 감싸서 반응형으로 만듭니다.
자동으로 컴포넌트가 state와 일치하도록 유지하는 겁니다. 개념적으로 위에서 report
로 했던 것과 다르지 않습니다.
하단의 코드에서는 몇 개의 React 컴포넌트를 정의합니다.
유일한 MobX 특유의 코드는 observer
래핑입니다.
이것만으로도 각각의 컴포넌트들은 관련 데이터가 변경될 때 각자 재렌더링합니다.
useState
setter를 호출할 필요도 없고 셀렉터 혹은 구성이 필요한 HoC를 사용해서 어떻게 애플리케이션 state의 적절한 부분을 구독할지 고민할 필요도 없습니다.
dumb같고 선언적인 방식으로 정의되었지만 기본적으로 모든 컴포넌트들이 smart 컴포넌트로 전환되었습니다.
실행 버튼을 눌러서 하단의 코드가 어떻게 작동하는지 확인하세요. 코드는 편집 가능하므로 자유롭게 바꿔보실 수 있습니다.
예를 들면 모든 observer
호출 또는 TodoView
를 꾸미고 있는 observer
한 가지를 지워보세요.
프리뷰에서 오른쪽에 하이라이트 되는 숫자는 컴포넌트가 렌더링 된 횟수를 가리킵니다.
하단은 어떠한 추가 bookkeeping 없이 데이터만 바꾸면 된다는 점을 설명해 줍니다. MobX는 store에 있는 state를 기반으로 유저 인터페이스의 관련 부분들을 자동으로 파생시키고 업데이트합니다.
참조(reference) 작업
지금까지는 observable 객체(프로토타입 객체와 순수 객체 모두), 배열과 원시 값(promitive)을 만들었습니다.
아마 MobX에서 참조가 어떻게 다뤄지는지 궁금할 것입니다.
내 state가 그래프를 형성할 수 있을까요?
이전 코드들에서 todo 속성에 있는 assignee
를 발견했을 겁니다.
거기에 사람을 포함하고 있는 또 다른 'store'(쉽게 말하자면 배열)를 적용하여 값을 부여하고, 테스크를 할당해봅시다.
이제 '사람'과 'todo'로 이루어진 두 개의 개별 store가 있습니다.
사람 store의 특정 인물에게 assignee
를 할당하기 위해 참조를 배치했습니다.
이 변화들은 TodoView
에 의해 자동으로 감지됩니다.
MobX를 이용하면 일단 데이터를 정규화할 필요가 없고, 컴포넌트가 업데이트 되도록 하기 위해 셀렉터를 작성할 필요도 없습니다.
사실 데이터가 어디에 저장되어 있든 문제없습니다.
객체들이 observable이기만 하면 MobX가 감지할 수 있습니다.
실제 JavaScript 참조도 당연히 작동합니다.
파생 값과 관련이 있다면 MobX는 자동으로 트래킹 할 것입니다.
이를 확인해보기 위해 하단 input 박스에서 당신의 이름을 바꿔보세요.
(먼저 상단의 실행 버튼을 누르고 진행하세요!)
이름:
위 input 박스의 HTML은 단순합니다.
<input onkeyup="peopleStore[1].name = event.target.value" />
비동기식 action
todo 애플리케이션의 모든 것은 state로부터 파생되었기 때문에 state가 언제 변경되든 상관없습니다. 따라서 비동기식 action을 만들기도 쉽습니다. 하단의 버튼을 여러 번 눌러서 새 todo 항목을 비동기식으로 로드해보세요.
코드는 굉장히 단순합니다.
UI가 현재 로딩 상태를 반영하도록 하기 위해 store의 pendingRequests
속성을 업데이트하는 것으로 시작합니다.
로딩이 끝나면 store의 todo를 업데이트하고 다시 pendingRequests
카운터를 감소시킵니다.
이 코드와 이전의 TodoList
정의를 비교해서 pendingRequests가 어떻게 사용되었는지를 확인해보세요.
action
으로 timeout 함수를 감쌌다는 점을 유의하세요.
절대적으로 필요한 일은 아니지만, 두 개의 변화가 한 번의 처리 과정에서 진행되도록 하기 때문에 두 업데이트가 모두 완료된 후에만 observer에게 이를 알립니다.
observableTodoStore.pendingRequests++; setTimeout(action(() => { observableTodoStore.addTodo('Random Todo ' + Math.random()); observableTodoStore.pendingRequests--; }), 2000);
결론
이게 전부입니다! 보일러 플레이트로부터 자유롭습니다.
완전한 UI를 구성하는 몇 가지 단순하고 선언적인 컴포넌트일 뿐입니다. 그리고 state에서 완벽하게, 반응적으로 파생된 것입니다.
이제 직접 애플리케이션에서 mobx
와 mobx-react-lite
패키지를 사용해볼 준비가 되었습니다.
지금까지의 내용을 간단하게 요약해봅시다.
-
observable
데코레이터 또는observable(객체 또는 배열)
함수를 사용하여 MobX가 객체를 트래킹 할 수 있도록 만드세요. -
computed
데코레이터는 state로부터 자동으로 값을 가져와 캐시 할 수 있는 함수를 만드는 데 사용할 수 있습니다. -
observable한 state 기반의 함수들을 자동으로 실행하기 위해서는
autorun
을 사용하세요. 로깅, 네트워크 요청 등에 유용합니다. -
mobx-react-lite
패키지의observer
를 사용하여 React 컴포넌트가 진정으로 reactive 하도록 만드세요. 거대한 양의 데이터를 사용하는 복잡한 애플리케이션에 사용되더라도 자동적으로, 효율적으로 업데이트할 것입니다.
상단의 편집 가능한 코드 블록들을 수정해보면서 MobX가 모든 변화에 어떻게 반응하는지에 대해 기본적인 감각을 익혀보실 수 있습니다.
예를 들면 report
에 로그 명령어를 추가해서 언제 호출되는지를 살펴보세요.
또는 report
를 한 번도 표시하지 않았을 때 TodoList
렌더링에 어떤 영향이 미치는지, 또는 특정한 상황에서만 표시한다면 등등...
MobX는 아키텍처를 제한하지 않습니다
상단의 예제는 작위적인 예시이며 로직을 메서드로 압축하거나 저장소, 컨트롤러, 뷰 모델 등에서 구성하는 것과 같이 적절한 엔지니어링 관행을 사용하는 것이 좋습니다. 다양한 아키텍처 패턴이 적용될 수 있고 몇 가지는 공식 문서에서 더 구체적으로 논의되었습니다. 상단의 예제와 공식 문서의 예제는 MobX가 어떻게 사용되어야 하는지가 아닌 어떻게 사용될 수 있는지를 보여줍니다. HackerNews의 누군가는 이렇게 말합니다.
“MobX가 다른 데서도 많이 언급되었지만 역시 칭찬을 안 할 수 없네요. MobX로 작성한다는 것은 컨트롤러∙디스패쳐(dispatcher)∙작업(action)∙감독관(supervisor) 또는 다른 형태의 데이터 흐름 관리를 사용하는 것이 기본적으로 Todo 앱 이상에 필요한 것이 아니라 애플리케이션의 요구에 따라 패턴화할 수 있는 아키텍처 문제로 되돌아간다는 것을 의미합니다.”