상태관리 라이브러리
상태 관리 라이브러리의 가장 중요한 기능은 어플리케이션에 흩어져있는 데이터를 한 곳에서 관리하여, 데이터에 대한 변경이 발생하면, 변경된 데이터를 신속, 정확하게 전파 하여 일관된 데이터를 가지도록 하는 것이다. 이 개념은 React의 Flux에서 시작되었으며, React 진영에서는 Flux, Redux, Mobx가 대표적인 상태 관리 라이브러리이고, Angular 진영에서는 ngrx가 대표적인 상태 관리 라이브러리 이다.
글로벌 상태 관리란, 컴포넌트 간의 데이터 교류, 특히 부모-자식 관계가 아닌 컴포넌트끼리 데이터 교류를 하는것을 의미한다.
상태 관리 라이브러리란 무조건 필요한 라이브러리는 아니다. 하지만 규모가 큰 앱에서는 있는게 확실히 편하다.
실제로 Redux, Mobx없이 좋은 앱을 만들 수 있다.(React 16.3에서 Context API가 좋아지며 글로벌 상태 관리를 별도의 라이브러리 없이 할 수 있게 되었다.)
하지만 Flux, Redux, MobX 등의 상태 관리 라이브라리를 사용하는것에 대한 장점이 있다.
-
상태 업데이트 로직의 분리
복잡한 상태 업데이트 로직들을 컴포넌트에서 뜯어낼 수 있고, 이를 모듈화 하여 여러 파일들로 저장해서 보기 좋게 정돈 할 수 있다. 이를 통하여 더욱 높은 유지보수성을 일궈낼 수 있다. -
더 쉬운 상태 관리
상태 관리 라이브러리를 사용하지 않는다면 컴포넌트가 지닌 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