본문 바로가기

[리액트] mobx-react-lite로 todoList 작성하기

오랜만에 리액트!

리덕스만 써보다가 mobx-react-lite 상태관리 라이브러리의 예제 코드를 작성해보았다.

 

 

mobX란 리액티브 프로그래밍 기반으로 리액트에서 상태관리를 가능하게 해주는 라이브러리로 앵귤러를 미리 해서 그런가 크게 어렵게 느껴지진 않았다.

 

참고했던 링크들

mobx-react-lite 짧막 정리

이번주부터 살펴보기 시작해서 아직 내부 로직까지 완벽하게 이해하진 못했지만 mobx-react-lite가 뭔지에 대해 지금까지 이해한 바는 다음과 같다.

 

mobx-react v5는 클래스에 종속적이라 hook과 같이 사용할 수 없었다. 그래서 mobx-react-lite로 라이브러리를 새로 작성하였고 mobx-react v6는 mobx-react-lite를 디펜던시로 받아서 이전 버전의 호환성을 유지하고 있다.

 

또한 mobx에 있었던 legacy context를 더이상 사용하지 않아 provider와 inject또한 이제 리액트 컨텍스트로 대채할 수 있게 되었다.

The library does not include any Provider/inject utilities as they can be fully replaced with React Context.

mobx-react-lite로 스토어를 등록한 후 사용하는 방법은 링크를 참조하여 구현할 수 있는데 주로 사용되는 api 설명은 다음과 같다.

공식문서의 API Reference를 참조하였다.

  • useLocalStore() : 컴포넌트의 생명주기와 함께하는 옵저버블 데이터를 등록할 수 있는 훅
  • useObserver() : 옵저버를 사용함과 동시에 기타 컴포넌트 최적화 등의 기능도 함께 사용할 수 있는 훅

useObserver() 내부 구현 로직을 뜯어보면 단지 observable 값의 존재 여부에 따라 업데이트 로직이 내부에서 실행되고 있어 provider를 사용하지 않고 store를 글로벌 영역에서 싱글톤으로 만들어 가져다 사용해도 된다.

 

[출처] mobx-react-lite/src/useObserver.ts

주의해야할 점

mobx를 사용하면서 먼저 주의해야 할 점은 useContext로 옵저버블 데이터를 받을 때 구조 분해 할당을 해서는 안된다는 것이다.

구조분해할당을 하면 옵저버블에서 일반 객체로 변환되어 데이터 변화에 따른 렌더링이 일어나지 않게 되기 때문이다.

 

그리고, 객체를 요소로 갖는 Array를 사용할 때 주의해야 할 점이 있다.

 

위에서 todoProvider에서 완료 여부를 체크하는 checkbox의 onChange 이벤트에 의해 todos 배열내 해당 요소에 complete를 변경하는 것을 아래처럼 변경한다면 아무 변화도 일어나지 않는다.

setComplete(todoId) {
  const idx = store.todos.findIndex(todo => todo.id === todoId);
  if (idx > -1) {
    store.todos[idx].complete = !store.todos[idx].complete; // 렌더링되지 않음!
  }
},

아마 Array가 변했는지를 얕은 비교를 통해 렌더링이 발생하여 결국 Array의 해당 요소의 참조는 변하지 않아서 Array가 변하지 않았다고 판단하게 되기 때문인 듯 하다.

 

따라서 해당 요소 전체를 새로운 객체로 할당하거나 immer 와 같은 라이브러리를 활용한다.

setComplete(todoId) {
  const idx = store.todos.findIndex(todo => todo.id === todoId);
  if (idx > -1) {
    store.todos[idx] = {
      ...store.todos[idx],
      complete: !store.todos[idx].complete
    };
  }
},

마지막으로 주의해야할 사항은 부모-자식 컴포넌트가 동일한 옵저버블을 바라보고 있을 때 다음의 워닝이 뜨는 것을 해결하는 것이다.

[MobX] You haven't configured observer batching which might result in unexpected behavior in some cases. See more at https://github.com/mobxjs/mobx-react-lite/#observer-batching

관련 이슈에 따르면 리액트 이벤트 핸들러에 의한 업데이트가 아닌 이상 부모 > 자식 순서로 컴포넌트가 업데이트되는 것을 보장할 수 없다. 따라서 mobx-react-lite/batchingForReactDom를 임포트하여 observer가 원래 순서대로 업데이트되는 것을 보장할 수 있어야 한다.

[unsolved] 재사용가능한 코드 만들기

위에서 useObserver을 옵저버블이 필요한 코드마다 계속 호출하는 것을 없애기 위해 다음의 코드를 보고 따라 구현해보았다.

import React from 'react';
import { useObserver } from 'mobx-react-lite';

export const useStoreData = <Selection, ContextData, Store>(
  context: React.Context<ContextData>,
  storeSelector: (contextData: ContextData) => Store,
  dataSelector: (store: Store) => Selection
) => {
  const value = React.useContext(context);
  if (!value) {
    throw new Error();
  }
  const store = storeSelector(value);
  return useObserver(() => {
    return dataSelector(store);
  });
};

[출처] MobX를 React Hooks (TypeScript) 와 함께 사용하는방법

 

스토어에서 값은 바뀌는데 렌더링이 되지 않는 것으로 보아 옵저버블이 일반 객체로 변한 것인지 의심되었다.

 

이 글에서 mobx-react-lite 동작 원리에 대해 기술하고 있어 좀 더 읽어보고 어떻게 적용할 수 있을지 고민해야 겠다.

2020-07-16 업데이트

위의 ueeStoreData를 사용하기 위해 여러가지 실험을 해보았다. 이 글을 많이 참고하였다.

 

먼저 todoItem을 별도의 클래스로 분리하고 옵저버블과 액션을 적용하였다. 완료/미완료 변경과 내용 업데이트관련 로직을 공통 메소드로 분리하고 싶었기 때문이다.

 

그리고나서 해당 TodoItem을 todoStore내 todos의 요소로 주었다.

 

이제 컴포넌트에서 useContext와 useObserver가 아닌 useRootData 훅으로 원하는 데이터만을 관찰하도록 변경하였다.

TodoItem 인스턴스에 setComplete와 updateTodo 메소드가 존재하므로 todoStore에서 더이상 정의하지 않아도 된다.

그런데 나머지 기능들은 원하는대로 동작했는데 어째서인지 boolean 값인 complete만 setComplete() 직후 렌더링이 일어나지 않았다. 분명 옵저버블의 값은 변경되고 있는데도 말이다... 혹시나 싶어서 complete를 number로 변경하니 잘 되었다.🧐

 

당최 이유가 뭔지 알수가 없었어서 저 재사용가능한 코드를 실무에서도 사용할 수 있으려면 해당 문제를 해결해야 겠다.