Redux보다 러닝커브가 낮고 가독성이 좋으며, 대규모 프로젝트를 운영하고 있는 기존 기업들이 최근 mobx를 많이 사용하여 mobX를 학습해보았습니다!
yarn add mobx mobx-react
mobx 버전6 이전에서는 데코레이터를 사용하여 observable
, computed
및 로 표시하도록 권장하였으나, 현재는 ES 표준이 아니며 표준화 과정에 오랜 시간이 걸립니다. 또한 표준이 이전 데코레이터가 구현된 방식과는 다를 것으로 보입니다. 호환성을 위해서 mobx6 이상에서는 데코레이터를 지양하고, 대신 [makeObservable
/makeAutoObservable
](https://mobx.js.org/observable-state.html) 사용을 권장합니다.
❗️실제로 mobx 6 이상에서는 데코레이터가 사용된 observer가 정상적으로 작동하지 않는것으로 확인되었습니다. 대신 makeObservable를 사용하여 정상 작동을 확인했습니다.
"experimentalDecorators": true,
"useDefineForClassFields": true,
yarn add @babel/plugin-proposal-class- properties @babel/plugin-proposal-decorators
{
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": false }]
]
}
https://github.com/hold1593/MobX_Practice
상태관리코드 작성에는 여러가지 방법이 있지만 해당 프로젝트에서는 여러가지 스토어를 만들어 하나의 RootStore에 저장하여 사용할 수 있는 싱클톤패턴으로 작성하였습니다.
makeAutoObservable()
속성, 객체, 배열, Map, Set을 자동으로 observable로 만듭니다.
⇒ 버전 6이상에서 데코레이터 대신 사용하기 용이한 api
import {action, makeObservable, observable} from 'mobx';
// import {makeAutoObservable} from "mobx"
// 클래스 형태로 작성
export class TodoStore {
rootStore;
// 변화를 감지할 배열 변수
todos = [];
constructor(root) {
//구독할 변수와 action함수들을 지정
makeObservable(this, {
todos: observable, // 내가 관찰할 변수
addTodo: action, // 상태 변화를 일으킬 액션 함수
deleteTodo: action, // 상태 변화를 일으킬 액션 함수
changeRate: action, // 상태 변화를 일으킬 액션 함수
})
// a. makeAutoObservable()
// makeAutoObservable(this)
// makeAutoObservable를 사용하면 위처럼 하나씩 지정해줄 필요도,
// 데코레이터를 써줄 필요도 없다.
// 자동으로 observable,action 타입을 지정해줌
this.rootStore = root;
// initial state 설정하는곳
this.todos = [...]
}
addTodo(content: string, rate: number){...}
deleteTodo(id: number) {...}
changeRate(id: number, rate: number){...}
import { TodoStore } from "./TodoStore";
// 여러개의 store들을 import하여 아래와 같이 사용할 수 있다.
// 이곳에 작성된 store들은 뷰컴포넌트에서 rootStore에 접근하여 모두 사용 가능하다.
export class RootStore{
todoStore;
constructor(){
this.todoStore = new TodoStore(this);
}
}
import * as React from 'react';
import { RootStore } from './RootStore';
// useContext를 이용하여 스토어를 이용할 수 있는 훅을 생성한다
export const StoreContext = React.createContext(new RootStore());
export const StoreProvider = StoreContext.Provider;
export const useStore = () => React.useContext(StoreContext);
4 . index.tsx
import { StoreProvider } from './stores/Context';
import { RootStore } from './stores/RootStore';
//rootStore 하나로 모든 스토어를 사용할 수 있다.
const rootStore = new RootStore();
ReactDOM.render(
<React.StrictMode>
{/* rootStore를 사용할 컴포넌트를 감싸주어 사용한다 */}
<StoreProvider value={rootStore}>
<App />
</StoreProvider>
</React.StrictMode>,
document.getElementById('root')
);
reportWebVitals();
import {useStore} from "../stores/Context";
import {observer} from "mobx-react";
//observable를 구독하는자 => observer로 컴포넌트를 감싸주면 된다.
const App = observer(() => {
const [newTodo, setNewTodo] = useState('');
const [newRate, setNewRate] = useState<number | null>(0);
// useStore 훅을 사용하여 todoStore를 불러옴
const {todoStore} = useStore();
...
const handleComplete = () => {
todoStore.addTodo(newTodo, newRate); // 스토어에 로컬state 저장
}
const onDelete = (id: number) => {
todoStore.deleteTodo(id);
}
const onChangeRate = (id: number, value: any ) => {
todoStore.changeRate(id, value)
}
return(...)
}
yarn add mobx mobx-react mobx-react-lite
{
"presets": ["next/babel"],
"plugins": [
["@babel/plugin-proposal-decorators",{"legacy": true}],
["@babel/plugin-proposal-class-properties",{"loose": true}]
]
}
최상위 페이지 _app.js에서 <Provider></Provider>
태그로 컴포넌트들을 감싸주면 하위 컴포넌트들 모두mobX store inject가 가능합니다.
//_app.js
import { StoreProvider } from '../store/StoreProvider'
export default function App({ Component, pageProps }) {
return (
// <StoreProvider {...pageProps} initialState={pageProps.initialState}>
<StoreProvider {...pageProps}>
<Component {...pageProps} />
</StoreProvider>
)
}
모든 전역상태는 Store.js에서 관리합니다. observable한 변수 및 action 객체 등 한곳에서 모아 처리합니다.
mobx-persist-store는 SSR 에서도 사용 가능하며, 사용 시 if(typeof window !== 'undefined') 필터링 후 사용해야 합니다.
//store/Store.js
import { action, observable, computed, runInAction, makeObservable } from 'mobx'
import { enableStaticRendering } from 'mobx-react-lite'
import { makePersistable } from 'mobx-persist-store';
// NextJS 특정, 서버 측 렌더링하지 않음
enableStaticRendering(typeof window === 'undefined')
export class Store {
lastUpdate; //observable
light; //observable
... // 각종 observable 변수
someProperty = []; // mobx-persist 사용변수
constructor() {
makeObservable(this, {
...
})
// localStorage에 상태를 저장
// ssr에서 사용 시 아래 형태와 같이 사용
if(typeof window !== 'undefined'){
makePersistable(this, { name: 'SampleStore', properties: ['someProperty'], storage: window.localStorage });
}
}
// action
start = () => { ... }
// computed
get timeString() { ... }
// action
stop = () => { ... }
// action
hydrate = () => { ... }
}
// store/StoreProvider.js
import { createContext, useContext } from 'react'
import { Store } from './Store'
let store;
export const StoreContext = createContext();
export function useStore() {
const context = useContext(StoreContext)
if (context === undefined) {
throw new Error('useStore must be used within StoreProvider')
}
return context
};
export function StoreProvider({ children, initialState: initialData }) {
const store = initializeStore(initialData)
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>
};
function initializeStore(initialData = null) {
const _store = store ?? new Store()
// 데이터를 가져오는 메서드가 있는 경우 여기에서 hydrate 됨.
if (initialData) {
_store.hydrate(initialData)
}
// ssr과 ssg는 항상 새로운 store를 생성
if (typeof window === 'undefined') return _store
// 클라이언트에서 한번 store를 생성
if (!store) store = _store
return _store