Mobx 사용법

Dongjun Ahn·2022년 5월 25일
0

이번 프로젝트에서 mobx를 사용 하게 됐다.
간단하게 사용법을 공부하면 좋을 것같다.

수많은 블로그에 많은 글들이 있지만,
역시나 나는 짬뽕해서 사용했다.

설치

npm install --save mobx mobx-react

yarn add mobx mobx-react

mobx 6버전 이상

mobx 버전6 이전에서는 데코레이터를 사용하여 observable, computed및 로 표시하도록 권장하였으나, 현재는 호환성을 위해서 mobx6 이상에서는 데코레이터를 지양하고, 대신 makeObservable/makeAutoObservable 사용을 권장한다.

mobx 6버전 이하

데코레이터 사용을 위한 babel설치

npm install @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators

yarn add @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators

Next js 프로젝트에서 바벨 사용을 위해, 프로젝트 최상단에 .babelrc 생성

// .babalrc

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

적용

MobX + React + Typescript

하나의 RootStore에 저장하여 사용할 수 있는 싱클톤패턴으로 작성.

1. store -> SampleStore -> index.tsx

makeObservable(target, annotations?, options?) 사용
String, Number, Object, Arrray, Async Sample

import { action, makeObservable, observable, computed, flow, runInAction } from 'mobx';
import { RootStore } from '@/store';
import { sampleAPI, sampleUpdateAPI } from '@/api';
export interface SampleObject {
  number?: number;
  string?: string;
}
export type SampleArray = number[];

class SampleStore {
  readonly rootStore: RootStore;
  number = 0;
  object: SampleObject = { number: 1, string: 'test' };
  array: SampleArray = [1, 2, 3, 4, 5];
  constructor(rootStore: RootStore) {
    this.rootStore = rootStore;

    // makeObservable(target, annotations?, options?)
    // or
    makeObservable(this, {
      number: observable,
      object: observable,
      array: observable,
      addNumber: action,
      double: computed,
      changeObject: action,
      addArray: action,
      removeArray: action,
      flowApi: flow,
      runInActionAPI: action,
    });
  }
  get double(): number {
    return this.number * 2;
  }
  addNumber = (): void => {
    this.number = this.number + 1;
  };
  changeObject = (key: string, value: number | string): void => {
    this.object[key] = value;
  };
  addArray = (idx, data): void => {
    this.array.splice(idx, 0, data);
  };
  removeArray = (idx): void => {
    this.array.splice(idx, 1);
  };
  *flowApi() {
    const res = yield sampleUpdateAPI();
    try {
      console.log(res);
      this.array = res;
    } catch (e) {
      console.log(e.message);
    }
  }
  runInActionAPI = async (): Promise<void> => {
    const res = await sampleAPI('runInActionAPI');
    runInAction(() => {
      console.log(res);
    });
  };
}

export default SampleStore;

2. store -> index.ts

import SampleStore from './SampleStore';

export class RootStore {
  SampleStore: SampleStore;
  constructor() {
    this.SampleStore = new SampleStore(this);
  }
}

export const store = new RootStore();

3. _app.tsx

import { AppProps } from 'next/app';
import { Provider } from 'mobx-react';
import { store } from '@/store';
import '@/styles/globals.css';

const MyApp = ({ Component, pageProps }: AppProps) => {
  return (
    <Provider {...store}>
      <Component {...pageProps} />
    </Provider>
  );
};

export default MyApp;

Nextjs + MobX + React + Typescript

1. store -> SampleStore -> index.tsx

import { action, makeObservable, observable, computed, flow, runInAction } from 'mobx';
import { RootStore } from '@/store';
import { sampleAPI, sampleUpdateAPI } from '@/api';
export interface SampleObject {
  number?: number;
  string?: string;
}
export type SampleArray = number[];

export const initialSample = {
  number: 0,
  object: { number: 1, string: 'test' },
  array: [1, 2, 3, 4, 5],
};

export class SampleStore {
  readonly rootStore: RootStore;
  number: number;
  object: SampleObject;
  array: SampleArray;
  constructor(initialData = initialSample, rootStore: RootStore) {
    this.rootStore = rootStore;

    // initialData
    this.number = initialData.number;
    this.object = initialData.object;
    this.array = initialData.array;

    // observable 은 state를 저장하는 추적 가능한 필드를 정의합니다.
    // action은 state를 수정하는 메서드를 표시합니다.
    // computed는 state로부터 새로운 사실을 도출하고 그 결괏값을 캐시 하는 getter를 나타냅니다.
    // makeObservable(target, annotations?, options?)
    makeObservable(this, {
      number: observable,
      object: observable,
      array: observable,
      addNumber: action,
      double: computed,
      changeObject: action,
      addArray: action,
      removeArray: action,
      flowApi: flow,
      runInActionAPI: action,
    });
  }
  get double(): number {
    return this.number * 2;
  }
  addNumber = (): void => {
    this.number = this.number + 1;
  };
  changeObject = (key: string, value: number | string): void => {
    this.object[key] = value;
    // this.object = {
    //   ...this.object,
    //   [key] : value
    // }
  };
  addArray = (idx, data): void => {
    this.array.splice(idx, 0, data);
  };
  removeArray = (idx): void => {
    this.array.splice(idx, 1);
  };

  // flowApi = flow(function* (){
  //   const res = yield sampleAPI('flow');
  //   try{
  //     console.log(res);
  //   }catch(e){
  //     console.log(e.message)
  //   }
  // });
  *flowApi() {
    const res = yield sampleUpdateAPI();
    try {
      console.log(res);
      this.array = res;
    } catch (e) {
      console.log(e.message);
    }
  }

  runInActionAPI = async (): Promise<void> => {
    const res = await sampleAPI('runInActionAPI');
    runInAction(() => {
      console.log(res);
    });
  };
}

2. store -> index.ts

import { useMemo } from 'react';
import { action, makeObservable } from 'mobx';
import { enableStaticRendering } from 'mobx-react';
import { SampleStore, initialSample } from './SampleStore';

// NextJS 특정, 서버 측 렌더링하지 않음
enableStaticRendering(typeof window === 'undefined');

const isServer = typeof window === 'undefined';
let rootStore;
export class RootStore {
  SampleStore: SampleStore;
  constructor() {
    this.SampleStore = new SampleStore(initialSample, this);

    makeObservable(this, {
      hydrate: action,
    });
  }

  hydrate = (data) => {
    if (data) {
      this.SampleStore = new SampleStore(data, this);
    }
  };
}

function initializeStore(initialData = null) {
  const _rootStore = rootStore ?? new RootStore();

  // initialData 데이터가 있는 경우, rootStore에 hydrate
  if (initialData) {
    _rootStore.hydrate(initialData);
  }

  // ssr과 ssg는 항상 새로운 store를 생성함
  if (isServer) return _rootStore;
  // client에서 한번 store를 생성
  if (!rootStore) rootStore = _rootStore;

  return _rootStore;
}

export function useStore(initialState) {
  const rootStore = useMemo(() => initializeStore(initialState), [initialState]);
  return rootStore;
}

3. _app.tsx

import { AppProps } from 'next/app';
import { useStore } from '@/store';
import { ThemeProvider } from 'styled-components';
import { theme } from '@/styles/theme';
import '@/styles/globals.css';

const MyApp = ({ Component, pageProps }: AppProps) => {
  const store = useStore(pageProps);
  return (
    <ThemeProvider {...store} theme={theme}>
      <Component {...pageProps} />
    </ThemeProvider>
  );
};

export default MyApp;

아직 뭐가 맞는지 정확하게는 모르겠다.
좀더 보면서 업데이트 해야겠다.

Error(렌더링이슈)

하다보니 문제가 생겼다.
첫페이지는 제대로 동작했는데 다른 페이지로 렌더링 하는 동시에 에러발생

MobX Provider: The set of provided stores has changed. Please avoid changing stores as the change might not propagate to all children

에러 메세지와 같이, provider에 적용한 store가 변경되었다고 나온다.

새로운 페이지로 넘어갈 때, 최상위 페이지인 _app.tsx부터 렌더링 된 후 해당되는 페이지로 넘어가게 된다.

//_app.tsx
import { AppProps } from 'next/app';
import { useStore } from '@/store';
import { ThemeProvider } from 'styled-components';
import { theme } from '@/styles/theme';
import '@/styles/globals.css';

const MyApp = ({ Component, pageProps }: AppProps) => {
  const store = useStore(pageProps);
  return (
    <ThemeProvider {...store} theme={theme}>
      <Component {...pageProps} />
    </ThemeProvider>
  );
};

export default MyApp;

기존 코드처럼 _app.tsx파일이 새로 렌더링 되면 Store를 다시 생성하여 provider에 적용하게 되어있다.

Store를 새로 적용하게 되면, 기존에 관리되던 모든 State 상태가 날아가게 되므로, Store를 새롭게 적용하지 못하도록 되어있다.
그러므로 _app.js에서 새로 렌더링이 돼도 Store가 변하지 않도록, 아래 코드와 같이 사용한다.
Store를 State로 관리하면, 처음 렌더링 되었을 때만 새로 생성하고, 이후에는 리 렌더링 시에도 기존 Store를 사용할 수 있다.

// app.tsx
import { useState } from 'react';
import { AppProps } from 'next/app';
import { Provider } from 'mobx-react';
import { useStore } from '@/store';
import { ThemeProvider } from 'styled-components';
import { theme } from '@/styles/theme';
import '@/styles/globals.css';

const MyApp = ({ Component, pageProps }: AppProps) => {
  const [pProps] = useState(pageProps);
  const store = useStore(pProps);
  return (
    <Provider {...store}>
      <ThemeProvider theme={theme}>
        <Component {...pageProps} />
      </ThemeProvider>
    </Provider>
  );
};

export default MyApp;

Reference

너무나도 많은 블로그를 보며 해서 다 나열 할 수 없을 것같다..
https://github.com/vercel/next.js/tree/canary/examples/with-mobx
https://velog.io/@mihyun0416/mobX-%EA%B8%B0%EB%B3%B8-%EC%82%AC%EC%9A%A9%EB%B2%95TodoList#mobx--react--typescript
https://medium.com/@qsx314/5-next-js-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0-mobx-52bb25b3d36e

profile
Front-end Developer

0개의 댓글