상태관리 라이브러리


상태 관리 라이브러리의 가장 중요한 기능은 어플리케이션에 흩어져있는 데이터를 한 곳에서 관리하여, 데이터에 대한 변경이 발생하면, 변경된 데이터를 신속, 정확하게 전파 하여 일관된 데이터를 가지도록 하는 것이다. 이 개념은 React의 Flux에서 시작되었으며, React 진영에서는 Flux, Redux, Mobx가 대표적인 상태 관리 라이브러리이고, Angular 진영에서는 ngrx가 대표적인 상태 관리 라이브러리 이다.

글로벌 상태 관리란, 컴포넌트 간의 데이터 교류, 특히 부모-자식 관계가 아닌 컴포넌트끼리 데이터 교류를 하는것을 의미한다.

상태 관리 라이브러리란 무조건 필요한 라이브러리는 아니다. 하지만 규모가 큰 앱에서는 있는게 확실히 편하다.

실제로 Redux, Mobx없이 좋은 앱을 만들 수 있다.(React 16.3에서 Context API가 좋아지며 글로벌 상태 관리를 별도의 라이브러리 없이 할 수 있게 되었다.)

하지만 Flux, Redux, MobX 등의 상태 관리 라이브라리를 사용하는것에 대한 장점이 있다.


  1. 상태 업데이트 로직의 분리
    복잡한 상태 업데이트 로직들을 컴포넌트에서 뜯어낼 수 있고, 이를 모듈화 하여 여러 파일들로 저장해서 보기 좋게 정돈 할 수 있다. 이를 통하여 더욱 높은 유지보수성을 일궈낼 수 있다.

  2. 더 쉬운 상태 관리
    상태 관리 라이브러리를 사용하지 않는다면 컴포넌트가 지닌 setState 를 사용해서 열심히 상태를 조합하고, 이를 여러 컴포넌트를 거쳐서 props로 전달해야하는데 상태 관리 라이브러리는 이러한 작업을 훨씬 쉽게 할 수 있다.


Redux와 MobX의 간단한 비교

사용률은 Redux가 훨씬 높다. 하지만 그렇다고 Redux를 사용해야 하는 것은 아니다. 둘다 상태관리 라이브러리로서의 기능을 충분히 만족한다.

Redux

React와 같이 동적인 상태를 관리 할 땐 state 에 담고 이를 수정할땐 꼭 setState 를 사용해하며, 또 컴포넌트의 업데이트 최적화를 위하여 불변성을 지켜야 한다.

MobX

불변성은 신경쓰지 않아도 된다. 컴포넌트 업데이트 최적화는 컴포넌트 단위를 최대한 작게 만들고, 리스트를 렌더링 할 땐 리스트 내용 외의 값이 props 로 들어가는것을 방지하기만 하는 몇가지 규칙만 따라주면 알아서 최적화가 잘 된다.


MobX

MobX의 주요 개념

1. Observable State (관찰 받고 있는 상태)

MobX 를 사용하고 있는 앱의 상태는 Observable하다. 앱에서 사용하고있는 상태는, 변할 수 있으며, 만약에 특정 부분이 바뀌면, MobX 에서는 정확히 어떤 부분이 바뀌었는지 알 수 있다.

2. Computed Value (연산된 값)

연산된 값은, 기존의 상태값과 다른 연산된 값에 기반하여 만들어질 수 있는 값이다. 이는 주로 성능 최적화를 위하여 많이 사용된다. 어떤 값을 연산해야 할 때, 연산에 기반되는 값이 바뀔때만 새로 연산하게 하고, 바뀌지 않았다면 그냥 기존의 값을 사용 할 수 있게 해준다.

3. Reactions (반응)

Reactions은 Computed Value 와 비슷하다. Computed Value 의 경우는 우리가 특정 값을 연산해야 될 때에만 처리가 되는 반면에, Reactions 은 값이 바뀜에 따라 해야 할 일을 정하는 것을 의미한다.

4. Actions (액션)

액션은 상태에 변화를 일으키는것을 말한다. 만약에 [Observable State 에 변화를 일으키는 코드를 호출한다] 이것은 하나의 액션입니다.


React에서 Decorator문법을 사용하여 Mobx사용하기

프로젝트 준비

기존의 create-react-app이 설치되어야 하며 yarn이 없다면 설치해주어야 한다.

MAC의 경우

brew install yarn --without-node  
yarn -v

명령어를 통하여 설치하고 버전을 확인할 수 있다.

MobX 시작은 아래의 명령어로 시작할 수 있다.

npx create-react-app mobx-with-react
cd mobx-with-react
yarn add mobx mobx-react
yarn start

Decorator 를 사용하면 훨씬 더 편하게 문법을 작성 할 수 있다. Decorator문법과 함께 MobX를 사용하기 위하여 babel 설정을 해야 한다. babel 설정을 커스터마이징 하려면 yarn eject 를 한 후 진행해야 한다.

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

이후 package.json 을 열어 babel 쪽을 찾아서 다음과 같이 수정해야 한다.

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


카운터 앱을 통한 예

create-react-app을 통해 생성한 기본 프로젝트에서 시작하는 것을 전제로 한다.

store 만들기

stores/counter.js
import { observable, action } from 'mobx';

export default class CounterStore {
  @observable number = 0;

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

  @action decrease = () => {
    this.number--;
  }
}


Provider 로 프로젝트에 스토어 적용

MobX에서 프로젝트에 스토어를 적용 할 때는 Redux 처럼 Provider 라는 컴포넌트를 사용한다. 프로젝트의 엔트리 파일인 index.js 파일을 다음과 같이 수정하면 된다.

src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'mobx-react'; // MobX 에서 사용하는 Provider
import './index.css';
import App from './App';
import CounterStore from './stores/counter'; // 방금 만든 스토어 불러온다.

const counter = new CounterStore(); // 스토어 인스턴스를 만들고

ReactDOM.render(
  <Provider counter={counter}>
    {/* Provider 에 props 로 넣어준다. */}
    <App />
  </Provider>,
  document.getElementById('root')
);


inject 로 컴포넌트에 스토어 주입

inject 함수는 mobx-react 에 있는 함수로서 컴포넌트에서 스토어에 접근할 수 있게 해준다. 정확히는 스토어에 있는 값을 컴포넌트의 props 로 주입을 해준다.

src/Counter.js
import React, { Component } from 'react';
import { observer, inject } from 'mobx-react';

@inject('counter')
@observer
class Counter extends Component {
  // **** 함수형태로 파라미터를 전달해주면 특정 값만 받아올 수 있다.
  // @inject(stores => ({
  // number: stores.counter.number,
  // increase: stores.counter.increase,
  // decrease: stores.counter.decrease,
  //}))
  render() {
    const { counter } = this.props;
    return (
      <div>
        <h1>{counter.number}</h1>
        <button onClick={counter.increase}>+1</button>
        <button onClick={counter.decrease}>-1</button>
      </div>
    );
  }
}

export default Counter;


mobx-react-devtools 개발도구 설치

필수적인 작업은 아니지만 설치한다면 어떤 값을 바꿨을 때 어떠한 컴포넌트들이 영향을 받고 업데이트는 얼마나 걸리고 어떠한 변화가 일어났는지에 대한 세부적인 정보를 볼 수 있게 해준다.

yarn add mobx-react-devtools

설치 후 App.js 에서 적용해주면 된다.

import React, { Component } from 'react';
import Counter from './Counter';
import DevTools from 'mobx-react-devtools';

class App extends Component {
  render() {
    return (
      <div>
        <Counter />
        {process.env.NODE_ENV === 'development' && <DevTools />}
      </div>
    );
  }
}

export default App;


MobX 의 리액트 컴포넌트 최적화

mobx-react 를 사용할 때 성능을 최적화 시키려면 몇가지 규칙을 따라주면 된다.

1. 리스트를 렌더링 할 땐, 컴포넌트에 리스트 관련 데이터만 props 로 넣어라.

리스트가 렌더링 될 때는 성능에 대해서 신경을 써주셔야 한다. 리스트 컴포넌트에 리스트 관련 props 만 넣는것을 한다.
아래와 같은 코드는 비효율적이다.

@observer class MyComponent extends Component {
    render() {
        const {todos, user} = this.props;
        return (<div>
            {user.name}
            <ul>
                {todos.map(todo => <TodoView todo={todo} key={todo.id} />)}
            </ul>
        </div>)
    }
}

위와같은 코드는 name을 수정한다면 모든 TodoView 컴포넌트를 불필요하게 리렌더링 해야한다. 따라서 아래와 같이 리스트를 잘 분리시켜서 다음과 같이 하는 것이 좋다.

@observer class MyComponent extends Component {
    render() {
        const {todos, user} = this.props;
        return (<div>
            {user.name}
            <TodosView todos={todos} />
        </div>)
    }
}

@observer class TodosView extends Component {
    render() {
        const {todos} = this.props;
        return <ul>
            {todos.map(todo => <TodoView todo={todo} key={todo.id} />)}
        </ul>)
    }
}


2. 세부참조 (dereference)는 최대한 늦게해라.

세부 참조 (혹은 역참조) 란, 우리가 특정 객체의 내부의 값을 조회하는것을 말한다.

  const itemList = items.map(item => (
    <BasketItem
      name={item.name}
      price={item.price}
      count={item.count}
      key={item.name}
      onTake={onTake}
    />
  ));

위와 같은 코드에서 item의 name, price, count… 등을 접근하는 것이 세부참조이다.

아래와 같은 구조로 개발한다면 업데이트 성능 최적화를 이뤄낼 수 있다.

  const itemList = items.map(item => (
    <BasketItem
      item={item}
      key={item.name}
      onTake={onTake}
    />
  ));

변동이 일어날 수 있는 count 값의 세부참조를 BasketItem 컴포넌트 내부에서 하게 된다면, 더 높은 성능으로 컴포넌트를 업데이트 할 수 있다. key값은 유일하며 변동이 없는 데이터로 선정하여 key 설정 부분에선 문제가 되지 않게 한다.

key값으로 선정할 데이터가 없다면 모든 항목에 대해 고유한 short ID 를 생성하여 키로 사용 하는것이 좋다.


3. 함수는 미리 바인딩하고, 파라미터는 내부에서 넣어준다.

컴포넌트에 함수를 전달해 줄 때에는 미리 바인딩 하는것이 좋고 파라미터가 유동적일땐 파라미터를 넣는 작업을 컴포넌트 밖이 아니라 안에서 하는것이 좋다.

함수를 미리 바인딩 하는 예

render() {
    return <MyWidget onClick={() => { alert('hi') }} />
}

위와 같은 코드는

render() {
    return <MyWidget onClick={this.handleClick} />
}

handleClick = () => {
    alert('hi')
}

위와 같이 미리 함수를 바인딩 해야한다.


파라미터를 내부에서 넣어주는 예

const ShopItemList = ({ onPut }) => {
  const itemList = items.map(item => (
    <ShopItem {...item} key={item.name} onPut={() => onPut(item.name, item.price)} />
  ));
  return <div>{itemList}</div>;
};

위와 같은 코드처럼 미리 파라미터를 바인딩 하지 않고

const ShopItemList = ({ onPut }) => {
  const itemList = items.map(item => (
    <ShopItem {...item} key={item.name} onPut={onPut} />
  ));
  return <div>{itemList}</div>;
};

const ShopItem = ({ name, price, onPut }) => {
  return (
    <div className="ShopItem" onClick={() => onPut(name, price)}>
      <h4>{name}</h4>
      <div>{price}원</div>
    </div>
  );
};

위와 같이 메서드를 미리 바인딩 한 후 파라미터는 컴포넌트 내부에서 넣어주는 것이 좋다.


Reference

  • https://velog.io/@velopert/redux-or-mobx
  • https://velog.io/@velopert/MobX-1-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-9sjltans3p
  • https://velog.io/@velopert/MobX-2-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-MobX-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-oejltas52z
  • https://medium.com/@robinpokorny/index-as-a-key-is-an-anti-pattern-e0349aece318
  • https://mobx.js.org/best/react-performance.html
Share :

Comments