Mobx 공식 docs를 보면 리액트와 결합하는 방법은 총 세가지입니다.
이 중에 3번째 방법이 공식 문서에서 추천하는 best practice입니다. 그런데 왜 context api를 사용해야할까요?
공식문서에는 다음과 같이 나와있습니다.
Using observables directly works very well, but since this typically introduces module state, this pattern might complicate unit testing. Instead, we recommend using React Context instead.
즉, 두번째 방법의 경우 전역데이터가 필요하지 않는 컴포넌트에서도 접근할 수 있기때문에 unit test를 복잡하게 만든다고 하는것 같습니다. 객체지향 프로그래밍에서의 private 플래그로 멤버를 보호하는 개념인것 같습니다.
같은 고민은 했던 외국 개발자분이 계시더라고요. 링크를 참고하세요.
간단하게 counter 앱을 만들면서 익숙해져 봅시다.
먼저 Mobx를 이용해 CounterStore를 만들어봅시다.
store/CounterStore.js
import {observable, action, makeObservable} from 'mobx';
class CounterStore {
@observable number = 0;
constructor() {
makeObservable(this);
}
@action
increaseNumber = () => {
this.number++;
};
@action
decreaseNumber = () => {
this.number--;
};
}
export default new CounterStore();
위에서 만든 CounterStore를 활용해서 react context를 만들고, Mobx를 활용한 store들이 여러개 늘어날 것을 고려해 Custom Provider를 만듭니다.
store/index.js
import React, {createContext, useContext} from 'react';
import CounterStore from '../store/CounterStore';
export const CounterContext = createContext();
const GlobalProvider = ({children}) => {
return (
// 만약 UserStore를 추가한다고하면 아래와 같이 Provider를 중첩시킬 수 있습니다.
// <UserContext.Provider value={UserStore}>
<CounterContext.Provider value={CounterStore}>
{children}
</CounterContext.Provider>
// </UserContext.Provider>
);
};
export default GlobalProvider;
실제 사용하는 컴포넌트에서는 consumer를 활용하기 보다는 useContext를 사용하는 것이 훨씬 더 깔끔하겠죠? 하지만 우린 커스텀 hooks을 만들어서 분리시켜 관리할 것입니다. 그럼 유지 보수, 확장성이 좋아집니다.
hooks/useCounter.js
import React, {useContext} from 'react';
import {CounterContext} from '../store';
const useCounter = () => {
const counterContext = useContext(CounterContext);
return counterContext;
};
export default useCounter;
그럼 실제로 사용해보겠습니다.
App.js
const Counter = observer(() => {
const counterStore = useCounter(); // 이렇게 커스텀 훅을 활용했습니다.
return (
<>
<Text>{counterStore.number}</Text>
<Button title="+1" onPress={() => counterStore.increaseNumber()} />
<Button title="-1" onPress={() => counterStore.decreaseNumber()} />
</>
);
});
const App = () => {
return (
<GlobalProvider> // 커스텀 Provider
<SafeAreaView style={{flex: 1, justifyContent: 'center'}}>
<Counter />
</SafeAreaView>
</GlobalProvider>
);
};
export default App;
여기서 Counter 컴포넌트에 observer를 HOC로 감싸줘야하는 이유가 뭘까요? context 객체로 CounterStore를 넘겼는데도 말이죠. 정확하진 않지만 Provider에 value로 넘긴 값은 객체의 참조값입니다. 바뀐건 객체의 멤버죠? 그래서 정석적으로 Mobx 업데이트, 리렌더링을 위해 observer api를 활용해야 한다고 생각합니다. 혹시 틀린게 있으면 알려주세요!
오늘은 React와 Mobx를 통합하는 방법에 대해 알아봤습니다. React뿐만 아니라 React Native에서도 같은 방법을 활용할 수 있기 때문에 잘 알아두면 매우 깔끔한 코드와 아키텍처로 앱을 개발할 수 있을 것 같네요.