본문 바로가기

React/이론

[React] MobX - 심화

슈퍼마켓 구현하기 

MobX를 깊게 다루기 위해서 슈퍼마켓을 추상적으로 구현하려고 한다. 

 

market 스토어 작성

import { observable, action, computed, makeObservable } from "mobx";

export default class MarketStore {
  selectedItems = [];

  constructor() {
    makeObservable({
      selectedItems: observable,
      put: action,
      take: action,
      total: computed,
    });
  }

  put = (name, price) => {
    const exists = this.selectedItems.find((item) => item.name === name);
    if (!exists) {
      this.selectedItems.push({
        name,
        price,
        count: 1,
      });
      return;
    }
    exists.count++;
  };

  take = (name) => {
    const itemToTake = this.selectedItems.find((item) => item.name === name);

    itemToTake.count--;

    if (itemToTake.count === 0) {
      this.selectedItems.remove(itemToTake);
    }
  };

  get total() {
    console.log("총합 계산");

    return this.selectedItems.reduce((previous, current) => {
      return previous + current.price * current.count;
    }, 0);
  }
}

put 함수를 통해 상품을 고를 경우 이미 있다면 count만 올리고 없을 경우 추가하는 action을 만들고,

take를 통해 갯수 제거, total을 통해 selectedItems에 변화가 생기면 자동으로 총합을 계산해주는 computed를

만들었다. 

 

market 스토어 적용하기

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { Provider } from "mobx-react";
import CounterStore from "./stores/counter";
import MarketStore from "./stores/market";

const counter = new CounterStore();
const market = new MarketStore();

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider counter={counter} market={market}>
    <App />
  </Provider>
);

counter 아래에 MarketStore로 새롭게 객체 인스턴스를 만들었다. 

 

기능 구현 - 아이템 추가

const ShopItemList = () => {
  const { market } = useStores();                              -- 1

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

useStores를 통해 market을 가져와 onPut 함수에 put 함수를 추가하였다.

 

import React from "react";
import "./ShopItem.css";

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

export default ShopItem;

ShopItem에서 onPut 함수를 사용해 클릭시 이벤트가 발생하게 만들었다. 

 

기능 구현 - 장바구니 데이터 반영

import { observer } from "mobx-react";
import React from "react";
import useStores from "../../hooks/useStores";
import BasketItem from "./BasketItem";

const BasketItemList = () => {
  const { market } = useStores();

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

  return (
    <div>
      {itemList}
      <hr />
      <p>
        <b>총합: </b> {market.total}원
      </p>
    </div>
  );
};

export default observer(BasketItemList);

market의 selectedItems를 기준으로 BasketItem을 출력하고 onTake 함수에 take 함수를 넘겨주었다. 

 

import { observer } from "mobx-react";
import React from "react";
import "./BasketItem.css";

const BasketItem = ({ name, price, count, onTake }) => {
  return (
    <div className="BasketItem">
      <div className="name">{name}</div>
      <div className="price">{price}원</div>
      <div className="count">{count}</div>
      <div className="return" onClick={() => onTake(name)}>   -- 1
        갖다놓기
      </div>
    </div>
  );
};

export default observer(BasketItem);                          -- 2

1. onTake 함수를 click 이벤트에 걸어주었다. 

2. MobX의 리스트를 렌더링할 때 내부에 있는 컴포넌트에도 observer를 구현해줘야 성능적으로 최적화가 일어난다. 

 

스토어끼리 관계 형성

현재는 counter와 market이 관계가 없기 때문에 서로간의 접근이 불필요한 상황이다. 

하지만 만약 해야할 경우 어떻게 해야할까? 

 

스토어끼리 접근하려면, RootStore를 만들어줘야한다. 

import CounterStore from "./counter";
import MarketStore from "./market";

class RootStore {
  constructor() {
    this.counter = new CounterStore(this);  -- 1
    this.market = new MarketStore(this);
  }
}

export default RootStore;

핵심은 다른 스토어들을 불러온 다음에 constructor에 각 스토어를 만들어 준 다음에, 

this.스토어명 = new 새로운 스토어(this) 

입력한다. 

 

여기서 this가 충요한데, this를 넣어줌으로 각 스토어들이 현재 루트 스토어가 무엇인지 알 수있게 된다.

export default class MarketStore {
  selectedItems = [];

  constructor(root) {
    this.root = root;              --- 1
    makeObservable(this, {
      selectedItems: observable,
      put: action,
      take: action,
      total: computed,
    });
  }
  
  ... 
}

각 스토어에서 this.root = root을 통해 서로 연결을 할 수 있다.  

 

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { Provider } from "mobx-react";
// import CounterStore from "./stores/counter";
// import MarketStore from "./stores/market";
import RootStore from "./stores";

// const counter = new CounterStore();
// const market = new MarketStore();

const rootStore = new RootStore();

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider
    // counter={counter}
    // market={market}
    {...rootStore}
  >
    <App />
  </Provider>
);

Provider 부분에 spread 문법으로 props에 rootStore를 넘겨주면 자동으로 Counter 스토어와 Market 스토어가

전달된다. 

 

이제 market에서 counter로 접근하고 싶다면, this.root.counter.number 이런식으로 접근이 가능하다. 

 

MobX 리액트 컴포넌트 최적화 

mobx-react를 사용할 때, 성능 최적화를 위해서 몇가지 규칙이 필요하다. 

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

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>)
    }
}

위와 같은 코드는 별로 좋지 않다. 왜냐하면 user.name이 바뀔때도 컴포넌트가 리렌더링되기 때문이다. 

 

@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. 세부 참조는 최대한 늦게하자

세부 참조란, 특정 객체의 내부 값을 조회하는 것을 말한다. 

  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 컴포넌트 내부에서 하게 한다면, 

더 높은 성능으로 컴포넌트를 업데이트할 수 있다. 여기서 item.name 값을 바뀌지 않기 때문에 key 설정 부분은

문제없다. 

 

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

컴포넌트에 함수를 전달할 때 미리 바인딩하는 것이 좋고, 파라미터가 유동적일 땐 

컴포넌트 안에서 하는 것이 좋다. 

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

onPut에서 미리 item.name, item.price를 넣어주고 있는데, 

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>
  );
};

onPut = {onPut}으로 전달하고 파라미터는 컴포넌트 내부에서 넣어주는 것이 좋다. 

 

즉, 컴포넌트 밖 ( ShopItemList)에서 미리 onPut을 바인딩하고, 컴포넌트 내부에서 파라미터를 전달하고 있다. 

 

깃허브 

https://github.com/SeoJaeWan/mobx-deep

 

GitHub - SeoJaeWan/mobx-deep

Contribute to SeoJaeWan/mobx-deep development by creating an account on GitHub.

github.com

 

반응형

'React > 이론' 카테고리의 다른 글

[React] Redux - toolkit편  (1) 2022.07.22
[React] Redux - 기본편  (1) 2022.07.21
[React] MobX - React에서 사용하기  (3) 2022.05.03
[React] MobX - 시작하기  (1) 2022.04.29
[React] import React from 'react'는 어디에 쓰일까?  (1) 2022.04.17