mobX 사용법 + NextJS

MihyunCho·2021년 12월 21일
1
post-thumbnail

Redux보다 러닝커브가 낮고 가독성이 좋으며, 대규모 프로젝트를 운영하고 있는 기존 기업들이 최근 mobx를 많이 사용하여 mobX를 학습해보았습니다!

mobX + React + typescript

mobx 설치

yarn add mobx mobx-react

mobx 버전6 이전에서는 데코레이터를 사용하여 observablecomputed및 로 표시하도록 권장하였으나, 현재는 ES 표준이 아니며 표준화 과정에 오랜 시간이 걸립니다. 또한 표준이 이전 데코레이터가 구현된 방식과는 다를 것으로 보입니다. 호환성을 위해서 mobx6 이상에서는 데코레이터를 지양하고, 대신 [makeObservable/makeAutoObservable](https://mobx.js.org/observable-state.html) 사용을 권장합니다.

❗️실제로 mobx 6 이상에서는 데코레이터가 사용된 observer가 정상적으로 작동하지 않는것으로 확인되었습니다. 대신 makeObservable를 사용하여 정상 작동을 확인했습니다.

  1. tsconfig.json에 컴파일러 옵션 추가
  "experimentalDecorators": true,
  "useDefineForClassFields": true,
  1. 데코레이터 지원 설치
yarn add @babel/plugin-proposal-class- properties @babel/plugin-proposal-decorators
  1. .babelrc 활성화
{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose": false }]
  ]
}

mobx를 이용한 미니TodoList

https://github.com/hold1593/MobX_Practice

상태관리코드 작성에는 여러가지 방법이 있지만 해당 프로젝트에서는 여러가지 스토어를 만들어 하나의 RootStore에 저장하여 사용할 수 있는 싱클톤패턴으로 작성하였습니다.

  1. TodoStore.tsx (원하는 스토어를 아래 형태로 여러개 만들 수 있다)

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){...}
  1. RootStore.tsx
import { TodoStore } from "./TodoStore";

// 여러개의 store들을 import하여 아래와 같이 사용할 수 있다.
// 이곳에 작성된 store들은 뷰컴포넌트에서 rootStore에 접근하여 모두 사용 가능하다.
export class RootStore{
  todoStore;
  
  constructor(){
    this.todoStore = new TodoStore(this);
  }
}
  1. Context.tsx
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();
  1. [screens] > App.tsx
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(...)
}

NextJS에서 mobX 세팅하기

mobX 설치

yarn add mobx mobx-react mobx-react-lite

.babelrc 생성 및 작성

{
    "presets": ["next/babel"],
    "plugins": [
        ["@babel/plugin-proposal-decorators",{"legacy": true}],
        ["@babel/plugin-proposal-class-properties",{"loose": true}]
    ]
}

_app.js 에서 Provider 적용하기

최상위 페이지 _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 폴더에 Store.js와 StoreProvider.js 생성 후 아래 로직을 바탕으로 코드 작성

모든 전역상태는 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
profile
Sic Parvis Magna 🧩

0개의 댓글