All Articles

리액트에서 mobX 이용해 상태관리 해보자

image.png

벨로퍼트 님의 글을 보고 따라해 본 mobX
mobX는 먼저 4가지 개념을 알고 있어야 한다.

Observable state

말 그대로 관찰받고 있는 상태를 말한다.
아마 리액트의 state와 비슷하다고 보면 되려나?

Computed Value

기존 상태값을 토대로 만들어진 연산된 값.

Reaction

Computed Value는 특정 값을 연산해야 할때만 처리가 되는데, 그 중간에! 어떠한 값이 바뀌는 작업에 어떠한 일이 벌어지는걸 reaction이라고 한다.

Action

상태에 변화를 일으키는 것. 쉽게 생각해서 함수 같은 것을 떠올리면 되려나! 근데 여기선 리덕스에서처럼 액션을 객체형태로 만들어 쓰지 않는게 하나의 특징쓰~

연습해보자!

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

// observable state
const calculator = observable ({
  a: 1,
  b: 2,
})

초기 상태 설정을 위해 observable state를 만들어준다.

reaction(
  ()=> calculator.a,
  (value, reaction) => {
    console.log(`a값이 ${value}로 바뀌었네요!`);
  }
);

reaction(
  ()=> calculator.b,
  (value, reaction) => {
    console.log(`a값이 ${value}로 바뀌었네요!`);
  }
);

calculator.a = 10;
calculator.b = 20;

이후 reaction을 통해 값이 변할때 리액션을 이루어질 수 있도록 설정

콘솔창을 확인해보면

image.png

변화된 값에 대한 콘솔이 찍힌다.
이번엔 computed를 써보자.

const sum = computed(() => {
  console.log('계산중입니다.'); // observe가 사용될때 이 콘솔이 나온다?
  return calculator.a + calculator.b;
})

sum.observe(()=> calculator.a); // a값을 주시하고 있음
sum.observe(()=> calculator.b) ; // b값을 주시하고 있음

calculator.a = 10;
calculator.b = 20;

console.log(sum.value);
console.log(sum.value);

calculator.a = 20;

콘솔창을 다시 확인해보자.

image.png

짠짠~ 계산중이라는 콘솔과 바뀐 값이 잘 찍히고 있다.

이번엔 auturun을 써볼차례쓰..!
autorun을 사용하면 reaction을 대신한다고…하는데.. 일단 써보자!

autorun(() => console.log(`a값이 ${calculator.a}로 바뀌었네요!`));
autorun(()=> console.log(`b값이  ${calculator.b}로 바뀌었네요`));
// autorun(()=> sum.get());

calculator.a = 10;
calculator.b = 20;

console.log(sum.value);
console.log(sum.value);

calculator.a = 20;
console.log(sum.value);

위에서 get함수를 쓰면 하나 하나 observe 하지 않아도 된다고 한다.

image.png

get도, observe도 하지 않았을 때의 콘솔창이다. 여기서 observe를 쓴다면

image.png

짠! 이렇게 나온다.
위에서 쓴 get 함수에서 주석을 푸르고, observe를 지워준다면
콘솔에 동일한 결과가 나온다.

이번엔 클래스를 활용해 식을 깔끔하게 써보자
cafe에서 메뉴가 추가되는 코드를 짜보았다.

import { decorate, observable, computed, autorun } from "mobx";

class Cafe {
  basket = [];

  get total() {
    console.log('계산중입니다...!');
    return this.basket.reduce((pre,cur) => pre + cur.price, 0);
  }

  select (name, price) {
    this.basket.push({ name, price });
  }
}

// decorate를 통해 각 값에 MobX 함수를 적용
decorate(Cafe, {
  basket: observable,
  total: computed,
});

const cafe = new Cafe();
autorun(() => cafe.total);
cafe.select('Iced Latte', 5000);
console.log(cafe.total);
cafe.select('Honey Bread', 6000);
console.log(cafe.total);
cafe.select('Lemon Ade', 4000);
console.log(cafe.total);

image.png

계산은 잘 되는군 이열~

이번엔 예제에 조건을 추가하고 마지막줄에 콘솔을 한번 찍었다

const cafe = new Cafe();
autorun(() => cafe.total);
autorun(() => {
  if (cafe.basket.length > 0) {
     console.log(cafe.basket[cafe.basket.length -1]);
  }
});

cafe.select('Iced Latte', 5000);
cafe.select('Honey Bread', 6000);
cafe.select('Lemon Ade', 4000);
console.log(cafe.total);

그 결과 콘솔창은

image.png

이걸 이제 transaction으로 감싸주자.
젤 윗줄에서 transaction을 import 해준 후에..

transaction(() => {
 cafe.select('Iced Latte', 5000);
 cafe.select('Honey Bread', 6000);
 cafe.select('Lemon Ade', 4000);
 }
)
console.log(cafe.total);

아까의 cafe.select들을 모조리 transaction안에 넣어주면
콘솔창은 가장 마지막에 딱 한번 찍히게 된다. 이렇게!

image.png

약간 더 간편하게 하려면 decorator 문법을 써주면 된다고 한다. 도전도전..!

import { observable, computed, autorun, action, transaction } from "mobx";

class Cafe {
  @observable basket = [];

  @computed
  get total() {
    console.log('계산중입니다...!');
    return this.basket.reduce((pre,cur) => pre + cur.price, 0);
  }

  @action
  select (name, price) {
    this.basket.push({ name, price });
  }
}

const cafe = new Cafe();
autorun(() => cafe.total);
autorun(() => {
  if (cafe.basket.length > 0) {
     console.log(cafe.basket[cafe.basket.length -1]);
  }
});

transaction(() => {
  cafe.select('Iced Latte', 5000);
  cafe.select('Honey Bread', 6000);
  cafe.select('Lemon Ade', 4000);
})
console.log(cafe.total);

decorator 문법은 자바스크립트 뿐만 아니라 파이썬 등에서도 다양하게 쓰이는데
@ <= 요 골뱅이를 불러서 쓰는게 공통이라고 한다.

image.png

콘솔을 찍어보면 아까와 동일한 값이 나온다.
decorate 함수를 쓰는 것 대신 decorator를 통해 좀 더 코드가 간결해진 느낌?

리액트에서 mobX 적용해보기

npx create-react-app 프로젝트이름
cd 프로젝트이름
yarn add mobx mobx-react

mobX는 걍 깔기만 하면 cra에서 쓸 수 있지만,
위에서 사용한 decorate는 ES7 문법이기 때문에 babel 설정을 따로 해주어야 한다.

벨로퍼트님은 package.json을 수정해주라고 하는데 안 먹혀서 다른 방법을 찾아보았다.
어떤 사람은 eject 명령어를 쓰던데, 한번 eject 하면 영원히 못 돌린다고 들어서
넘 무서우니깐 좀 더 귀찮지만 안 무서운 방법을 사용해봤다.

  • cra 프로젝트에 mobX 설치
yarn add customize-cra --dev
yarn add react-app-rewired --dev

// cra 버전2를 사용하고 있다면 둘 다 설치
// cra 버전1을 사용하고 있다면 rewired만 설치

위의 명령어로 라이브러리를 설치하면 eject 없이도 이미 만들어놓은 cra 프로젝트에 customizing해서 설정값을 바꿀수 있는듯?

image.png 근데 댄 아브라모씨가 추천하는 방법은 아니다.. 한번만 쓸게유ㅠㅠ

  • 바벨 설치 yarn add @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators
  • package.json 수정
 "scripts": {
   "start": "react-app-rewired start",
   "build": "react-app-rewired build",
   "test": "react-app-rewired test --env=jsdom"
}
  • config-overrides.js 파일 생성
const {
    addDecoratorsLegacy,
    disableEsLint,
    override
} = require("customize-cra");

  module.exports = {
    webpack: override(
        disableEsLint(),
        addDecoratorsLegacy()
    )
  };

위와 같이 작성해주고, root directory에 놓는다.

자 인제 Counter 라는 컴포넌트를 만들거다.

 import React, { Component } from "react";
 import {decorate, observable, action} from "mobx";
 import {observer} from "mobx-react";

 class Counter extends Component {
     number = 0;

     increase = () => {
         this.number++;
     }

     decrease = () => {
         this.number--;
     }
     render() {
         return(
           <div>
             <h1>{this.number}</h1>
             <button onClick={this.increase}>+1</button>
             <button onClick={this.decrease}>-1</button>
           </div>
         );
     }
 }

 decorate(Counter , {
     number : observable,
     increase : action,
     decrease : action
 })

 export default observer(Counter);

신기하게도 이게 끝이다! mobX를 쓰면 constructor 안에 state 안 써도 되고, 함수 안에도 setState함수를 안 써도 된다!! 신기신기!!!

위에서 babel도 설정해줬으니, 이번엔 decorator를 활용해보자.

import React, { Component } from "react";
import {observable, action} from "mobx";
import {observer} from "mobx-react";

class Counter extends Component {
    @observable number = 0;

    @action increase = () => {
        this.number++;
    }

    @action decrease = () => {
        this.number--;
    }
    render() {
        return(
          <div>
            <h1>{this.number}</h1>
            <button onClick={this.increase}>+1</button>
            <button onClick={this.decrease}>-1</button>
          </div>
        );
    }
}

export default observer(Counter);

image.png

날것 그대로의 카운터의 모습이다. 플/마 버튼을 눌러보면 잘 작동한다.
넘나 신기할 따름이다 ㅋㅋㅋ

지금 만들고 있는 테스트용 페이지가 있는데 거기다가도 적용해 봐야겠다.

Reference