Skip to content

MobX Best Practices는 어디에

yejineee edited this page Nov 30, 2020 · 3 revisions

개요

상태관리 라이브러리로 MobX를 사용하기로 정했다. Redux 대비 코드량이 적고 러닝커브가 낮았고 Recoil대비 자료가 많았기 때문에 프로젝트에 쉽게 적용할 수 있을 거라 판단했다.

상태관리 하고자 하는 것

우리 가계부에선 한 달치 Transaction을 가공해서 보여주는 페이지가 많았다. 그래서 한 달치 Transaction을 저장해 두는 TransactionStore을 두기로 했다.

Ver1. class

class Transaction {
  selectedDate = {
    year: new Date().getFullYear(),
    month: new Date().getMonth() + 1,
  };

  accountObjId = 'test';

  accountDateList: any = testAccountDateList;

  constructor() {
    makeObservable(this, {
      accountObjId: observable,
      accountDateList: observable,
      totalPrices: computed,
    });

    this.loadTransactions();
  }

  async loadTransactions() {
    const queryString = `?year=${this.selectedDate.year}&month=${this.selectedDate.month}`;
    const result = await axios.get(
      `${urls.transaction(this.accountObjId)}${queryString}`,
    );
    this.accountDateList = result;
  }

  get year() {
    return this.selectedDate.year;
  }

  get month() {
    return this.selectedDate.month;
  }

  set date({ year, month }: { year: number; month: number }) {
    this.selectedDate.year = year;
    this.selectedDate.month = month;
  }

  get totalPrices() {
    return Object.entries(toJS(this.accountDateList)).reduce(
      (totalPrices: PricesType, [, oneAccountDate]) => {
        const res = (oneAccountDate as []).reduce(
          (subPrices: PricesType, transaction: any) => {
            if (transaction.category.type === 'INCOME') {
              return {
                ...subPrices,
                income: subPrices.income + transaction.price,
              };
            }
            if (transaction.category.type === 'EXPENSE') {
              return {
                ...subPrices,
                expense: subPrices.expense + transaction.price,
              };
            }
            return subPrices;
          },
          { income: 0, expense: 0 },
        );

        return {
          income: totalPrices.income + res.income,
          expense: totalPrices.expense + res.expense,
        };
      },
      { income: 0, expense: 0 },
    );
  }
}

메인페이지에서는 한달치 데이터를 가지고 다음와 같은 값을 만들어야 했다.

  1. 상단 바를 위한 한달치 거래내역의 총 수입과 총 지출
  2. 각 거래내역을 보여주기 위한 컴포넌트

class형태로 설명한 공식문서

MobX는 자유롭게 쓸 수 있다는 것이 단점이었기 때문에 Best Practice를 찾으려고 했다. 마침 공식문서에 첫 페이지에 class형으로 store를 구현한 코드가 있어 class로 구현했다.

mobx를 적극적으로 써보려고 했지만..

MobX를 적극적으로 사용하고 싶었기 때문에 MobX의 computed를 이용해서 미리 많은 값을 만들어 두려고 했다. 1.의 값을 구하는 함수가 totalPrices이다. 이후 메인페이지의 거래내역들을 위한 컴포넌트도 이 TransactionStore에서 만들어 줄 지도 고민했다.

하지만 이렇게 만들게 되면 props를 전달받아 각 컴포넌트에게 위임하였던 컴포넌트 생성과정을 Mobx가 다 담당하게 된다. 이러면 store가 담당하는 일이 많아져 코드 복잡도가 상승한다. 이런 단점이 있어서 가독성 향상을 위해 메인페이지에서 총 수입지출을 구하도록 수정했다.

Ver2. ContextAPI를 활용한 함수형

export const TransactionStoreProvider = ({ children }: any) => {
  const initSelectedDate = {
    year: 2020,
    month: 11,
  };
  const [selectedDate, setSelectedDate] = useState<SelectedDateType>(
    initSelectedDate,
  );
  const [accountObjId, setAccountObjId] = useState<string>('test');

  const store = useLocalStore(() => ({
    accountDateList: {},
    loadTransactions: async () => {
      const queryString = `?year=${selectedDate.year}&month=${selectedDate.month}`;
      const result = await axios.get(
        `${urls.transaction(accountObjId)}${queryString}`,
      );
      store.accountDateList = result;
    },
    changeSelectedDate: (selectedDateInput: SelectedDateType) => {
      setSelectedDate(selectedDateInput);
      store.loadTransactions();
    },
    changeAccountObj: (accountObjInput: string) => {
      setAccountObjId(accountObjInput);
      store.loadTransactions();
    },
  }));

  return (
    <StoreContext.Provider value={store}>{children}</StoreContext.Provider>
  );
};

피어세션에서 왜 class형 쓰냐는 질문

class형태로 구현해서 쓰고 있었다. 2주차 피어세션에서 class형으로 구현한 특별한 이유가 있냐는 질문을 받았다. 공식문서에서 그렇게 나와있어 사용했다고 답했는데 질문하신 캠퍼분의 팀은 React의 함수형+Hooks와 같이 mobx도 함수형으로 하려고 한다고 들었다.

그래서 구글에서 mobx + react를 함수형으로 사용한 최신 practice를 검색해 봤지만 잘 나오지 않았다. 혹시 몰라 youtube에서 검색해 봤다. 만든지 5개월밖에 안된 강의 영상에서 contextAPI를 활용한 함수형으로 mobx를 사용하는 예제를 보게 되어 구현했다.

하지만 contextAPI를 이용해서 구현하는건 찝찝했고 항상 Provider로 사용하고자 하는 컴포넌트를 감싸줘야하는 것도 불편해보였다.

Ver3. useObserver활용

사용하고 싶은 store를 불러온 다음 mobx-react의 useObserver API를 통해 obserable하게 만드는 방법이 있었다. ContextAPI를 사용하지 않아도 되서 매우 깔끔한 방식이었다. 하지만 useObserver API가 deprecated되어서 사용하기가 찝찝했다.

Ver4. 공식문서대로

export const TransactionStore = makeAutoObservable({
  accountDateList: testAccountDateList,
  selectedDate: initDate,
  accountObjId: 'empty',
  state: 'pending',
  async loadTransactions() {
    this.state = 'pending';
    try {
      const result = await transactionAPI.getTransaction(this.accountObjId, {
        ...this.selectedDate,
      });
      runInAction(() => {
        this.accountDateList = { ...result } as any;
        this.state = 'done';
      });
    } catch (err) {
      runInAction(() => {
        this.state = 'error';
      });
    }
  },
  setSelectedDate(selectedDateInput: SelectedDateType) {
    this.selectedDate = selectedDateInput;
  },

  setAccountObjId(accountObjIdInput: string) {
    this.accountObjId = accountObjIdInput;
  },
});

공식문서의 Observable state를 factoryFunction + makeAutoObserable를 통해 구현하는 예제와

React에서 using global variables를 사용하는 예제를 합쳐서 위와 같이 state를 구현했다.

또 비동기 action 처리를 위한 건 async + runInAction 예제를 참고했다.

느낀점

MobX가 정말 빠르게 develop되고 있어서 2년전 best practice, 6개월전 best practice, 지금 best practice가 다른 것 같다. 이곳저곳 구글과 깃헙에서 누군가 예시를 잘 짜둔 걸 찾기 위해 많은 시간을 썼는데 결국 답은 공식문서에 있었다.

Clone this wiki locally