[튜토리얼] MobX 간단하게 사용하기

LEE JIYUN·2021년 7월 17일
1

Tutorials

목록 보기
1/1

MobX란

MobX는 React에서 사용 가능한 전역상태관리 라이브러리입니다. state나 useState를 이용해서 상태관리가 가능하지만 MobX를 사용하면 전역에서 관리하면서 반응적 업데이트가 가능합니다.

MobX Overview

(@)observable

Observable로 지정한 관측 가능한 값입니다. JS 기본형 데이터와 객체, 배열 및 Map 등이 될 수 있으며, new Observable 값을 return합니다.

*Decorator를 사용한 @observable key = value; 의 형태로 적용 가능하지만 이 경우 babel과 같은 transpiler가 있어야 합니다. transpiler를 사용하지 못하는 환경이라면 extendObservable(this, { key: value }) 를 이용합니다.

(@)computed

Computed value는 기존 state 또는 다른 Computed value에서 파생될 수 있는 값입니다. 개념적으로 스프레드시트의 formula와 유사합니다. 다음과 같은 특징을 가집니다.

  • State가 변경되면 자동으로 실행됩니다.
  • 결과 값이 이전 값과 동일한 경우 다시 실행되지 않습니다.
  • Computed value가 더 이상 표시되지 않는 경우 (예: 값을 사용하는 Component가 제거된 경우) MobX에서 자동으로 garbage를 수거할 수 있습니다. 물론 Observe or keepAlive를 사용해서 Computed value가 계속 유효하도록 설정할 수도 있습니다.

Getter & Setter
Getter 메서드는 프로퍼티를 읽고 사용할 때, Setter 메서드는 프로퍼티에 값을 할당할 때 실행됩니다.

const store = observable({
    variable: 2,
    get func() {
        return this.variable * 2;
    },
    set func(value) {
        this.variable = value;
    }
});

*Computed와 Autorun은 비슷해보이지만 다릅니다. 모두 반응형으로 호출되지만 다른 observer에서 사용할 수 있는 값을 반응적으로 생성하려면 @computed를 사용하고, 새 값을 생성하지 않고 실행하려면 autorun을 사용합니다. 또 @computed는 자동으로 garbage collecting 을 하지만 autorun은 사용하지 않는 value를 직접 수거해야 합니다.

autorun(effect: (reaction) => void)

Computed는 이전 값과 비교하여 변동이 있는 경우에만 실행되지만 Autorun은 dependencies 중 하나가 변경될 때마다 즉시 다시 실행되며, 전달된 함수가 실행되는 동안에만 observe 합니다.

(@)observer

Observer는 ReactJS Component를 반응형으로 업데이트합니다. Component를 감싸고 내부에 있는 observable 변수를 업데이트 합니다.

--------------- Class형 ---------------
@observer class Component extends React.Component {
    render() {
        return (<span> { store.data } </span> )
    }
});

observer(class Component ... { ... })

--------------- Function형 ---------------
const Component = observer(() => {
  return (<span> { store.data } </span> )
});

action(fn)

Action은 State를 변경하는 모든 작업을 일컫습니다. 변경 사항을 일괄 처리하고 작업이 완료된 후에만 Computed value와 Reaction에 알리므로 중간에 생성된 값 또는 불완전한 값이 표시되지 않습니다.

Action의 비동기 처리
@action 데코레이터와 action() 함수는 현재 실행 중인 함수(CallStack에 쌓인 작업)에만 적용되고, 비동기 처리된 함수(WebAPIs를 통해 EventLoop Queue에 쌓은 작업)에는 적용되지 않습니다. 따라서 비동기 처리되는 부분도 따로 action으로 감싸주거나 runInAction 또는 flow를 사용해야 의도한대로 실행됩니다.
*Note : Strict mode에서 state를 변경하려면 action을 필수적으로 사용해야 합니다.

Action은 flow를 이용해 간단히 비동기 처리 할 수 있지만, runInAction과 async / await를 사용하는 방법도 알아보겠습니다.

--------------- runInAction ---------------
 const firstStore = observable({
    string = "",
     
    @action
        actionFunction() {
            callBack().then(
                (res) => {
                    runInAction(() => {
                        this.string = "success";
                    })
                },
                (err) => {
                    runInAction(() => {
                        this.string = "fail";
                    })
                }
            )
        }
});

--------------- async/await + runInAction ---------------
  
 const firstStore = observable({
    string = "",
     
    @action
      async actionFunction() {
          try {
              const first = await firstAwait()
              const second = secondAwait();
              // 두번째 await에는 적용되지 않음

              runInAction(() => {
                  this.string = first;
              })

          } catch (err) {
              runInAction(() => {
                  this.string = "fail";
              })
          }
 });

--------------- flow (!) ---------------
      
const firstStore = observable({
    string = "",
    
    // flow로 function을 묶은 뒤 function*(){ ... }  
    actionFunction = flow(function*() {
        try {
            // await 대신 yield 사용
            const first = yield firstFunction();
            const second = secondeFunction();
          
            // runInAction으로 감싸지 않아도 OK
            this.string = "success"          
        } catch (err) {
            this.string = "fail"
        }
    })
});

reaction(data fn, effect fn, option?)

reaction(() => value, (value, previousValue, reaction) => { sideEffect }, options?)
Reaction은 Autorun과 유사하지만 observable을 보다 세밀하게 제어할 수 있고, 초기화 과정에서는 실행되지 않고 새로운 value가 업데이트 될 때만 실행됩니다. Reaction의 첫번째 Data function은 return값을 두번째 Effect function으로 전달하며, sideEffect는 Data function에서 접근한 Data에만 반응합니다. Reaction은 cause와 effect가 직접적으로 연결되지 않은 경우만 사용하는 것이 좋습니다.

MobX action, computed, reaction ?

  1. action으로 observable 요소의 값을 변경
  2. computedobserve 하던 값의 변화에 따라 결과를 저장해 cache에 저장
    reaction의 parameter로 전달된 함수 실행
    (autorun을 사용 시에는 observe를 따로 지정하지 않아도 자동 실행)

MobX 적용하기

1. 먼저 React에서 MobX를 사용하기 위해서 MobX와 MobX-React를 설치합니다.

npm install mobx mobx-react --save
npm install @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators

< package.json >
...
"babel": {
    "presets": [
      "react-app"
    ],
     // decorator를 사용하기 위해 package.json의 babel plugin을 아래와 같이 설정합니다.
    "plugins": [
      ["@babel/plugin-proposal-decorators", { "legacy": true }],
      ["@babel/plugin-proposal-class-properties", { "loose": true }]
    ]
  }

2. 필요한 Store를 작성합니다.

< firstStore.js >

import { observable, reaction, computed, autorun } from "mobx";
import axios from "axios";

// 각각의 State를 observable 데코레이터를 이용해 관찰 대상으로 지정해도 되지만 
// 전체를 observable 객체로 만들어도 됩니다.
const firstStore = observable({
  string: "",
  number : 0,

  setString: action((newStr) => { this.string = newStr; });
  setNumber: action((increaseNum) => { this.number += increaseNum; });

  setFirstStoreInfo: action(async (cocode) => {
    try {
      const response = await axios({
        method: "get",
        url: `${API}path/${something}`,
      });

      // action을 분리하거나
       firstStore.setString(response.data.string);
       firstStore.setNumber(response.data.number);

      // runInAction으로 처리
      runInAction(() => {
        firstStore.string = response.data.string;
        firstStore.number += response.data.string;
      });
    } catch (err) {
      ...
    }
  })
});

  export { firstStore };

  --------------- REACTION ---------------

  reaction(
    () => firstStore.string,
    value => {
      console.log(`string이 ${value} 로 갱신되었습니다.`);
    }
  );

   autorun(() => console.log(`string이 ${firstStore.string} 로 갱신되었습니다.`));

  /* reaction과 autorun 실행 결과는 다음과 같습니다.
     autorun : undefined ✔️
     reaction : {data.string}
     autorun : {data.string}
  */

  --------------- COMPUTED ---------------
  // Computed : 특정 값이 바뀔 때 연산값을 사용합니다. 
  // 값이 갱신되었다 하더라도 이전과 같은 값이라면 불필요하게 업데이트를 하지 않습니다.

  let multiplyNumber = computed(() => {
    return firstStore.number * 2;
  });

  multiplyNumber.observe_((info) => {
    console.log(info.oldValue, info.newValue)
  });
 // observe_ 에 전달된 info는 computedValue로, 
 // 이전 값과 새로운 값을 비교할 수 있습니다.

  autorun(() => multiplyNumber.get());

3. 여러 Store를 하나로 합쳐줄 useStore.js 파일을 만듭니다.
이 파일에서 store를 불러와 사용합니다.

< useStore.js >

import { firstStore } from "./firstStore";
import { signin, signup } from "./secondStore";

const useStore = () => {
  return {
    firstStore,
    signin,
    signup
  };
};

export default useStore;

4. Store의 변수와 함수를 사용할 Component에 import 하여 사용합니다.
Component는 mobx-react에서 제공하는 observer로 감싸야 하며,
마찬가지로 @observer 데코레이터 대신 함수형 컴포넌트를 감쌌습니다. Store를 구조분해할당 (비구조화 할당) 하여 사용하면 직관적이고 편리합니다.

< Component.js >

import React, { useEffect } from "react";
import { observer } from "mobx-react";
import useStore from "../stores/useStore";

const Component = observer(() => {
  const { firstStore, signin } = useStore();

  useEffect(() => {
    // Component가 Mount되었을 때 
    // 함수 firstStore.setFirstStoreInfo() 호출
    firstStore.setFirstStoreInfo();
  }, []);

  return ( 
    <ComponentFrame>
      // 변수 firstStore.string을 컴포넌트 내에서 사용
      <p>{ firstStore.string }</p>
    </ComponentFrame>
  );
});

export default Component;

0개의 댓글