Please enable JavaScript to view the comments powered by Disqus.

mobx-react와 React Hooks API 함께 사용하기

mobx-react v4에서 v6로 마이그레이션

React hooks와 mobx-react v4

React 16.8에서 도입된 React Hooks는 함수형 컴포넌트에서 상태를 관리할 수 있는 새로운 방법을 제공한다. Hooks API를 사용하면 함수형 컴포넌트에서도 자체 상태값을 가질 수 있으며, 클래스 컴포넌트의 라이프사이클 메소드도 대체할 수 있다. 물론 클래스 컴포넌트가 더 좋은지 함수형이 더 좋은지는 논쟁의 여지가 있다. 그리고 컴포넌트가 커지면 복잡도가 높아지는 것은 클래스나 함수형이나 마찬가지다. 하지만 개인적으로는 적절한 크기의 컴포넌트라면 확실히 Hooks를 사용했을 때 코드가 더 간결하고 직관적이라는 인상을 받았다.

그런데 mobx-react가 제공하는 observer로 래핑된 함수형 컴포넌트에서 useState같은 훅 API를 사용하려고 하면 React는 “훅은 함수형 컴포넌트에서만 사용할 수 있다”는 메시지와 함께 오류를 발생시킨다. mobx-react v4의 observer API는 클래스 컴포넌트를 리턴하는 hoc(higher order component)이기 때문이다. 훅을 사용하면서 mobx-react의 store에서 데이터를 가져오려면 mobx-react v6 또는 mobx-react-lite를 사용해야 한다.

mobx-react v6

mobx-react-lite는 React 16.8과 훅을 지원하기 위해 함수형 컴포넌트에서만 사용할 수 있는 API만 제공하고 있다. 특히 mobx-react-lite는 React legacy Context API를 사용하는 Provider, inject API를 제공하지 않는다. 대신 React.createContext API를 사용해서 store를 가져오는 방법을 제안한다.

mobx-react v4, v5를 사용해서 앱을 개발하고 있었다면 mobx-react-lite의 React Hooks를 위한 API를 포함하고 있는 mobx-react v6로 마이그레이션 하면 된다. 마이그레이션이라 해도 이미 구현되어 있는 클래스 컴포넌트는 수정할 필요가 없어서 크게 부담이 없다. 물론 공식 문서에서는 store를 새로운 API(=useLocalStore)로 구현하는 방법을 권하고 있다. 하지만 Hook API가 제공된다고 해서 클래스 컴포넌트를 버려야 할 이유는 없으므로 어떻게 사용할지는 이 도구를 사용하는 사람의 선택에 달렸다고 할 수 있다.

함수형 컴포넌트에서 inject 를 대체할 useStore 함수

앞서 언급했듯이 마이그레이션을 위해서 기존의 코드를 수정해야 할 필요는 없다. 다만 훅을 사용하는 함수형 컴포넌트를 위한 inject hoc는 별도로 제공하지 않으므로 store를 가져오기 위한 헬퍼 함수를 만들어야 한다. 간단하게 구현 가능하다.

import React from 'react';
import { MobXProviderContext } from 'mobx-react';

/**
 * React hooks를 사용하는 컴포넌트에서 store를 가져올 때 사용한다.
 * 참조) https://mobx-react.js.org/recipes-migration#hooks-for-the-rescue
 */
function useStores() {
  return React.useContext(MobXProviderContext);
}

export default useStores;

React.useContext API는 파라미터로 전달된 Context의 현재 값을 반환한다. 거기에 MobXProviderContext 를 사용하면 mobx-react의 Provider 가 제공하는 store 객체를 가져올 수 있다.

import { observer } from 'mobx-react'
import { useStores } from '../useStores'

const UserInfo = observer(() => {
  const { user } = useStores()
  return (
    <div>
      name: {user.name}
    </div>
  )
})

컴포넌트를 observer hoc로 래핑하는 과정은 동일하다. 대신 클래스 컴포넌트의 inject hoc를 사용하는 대신 useStores 헬퍼 함수를 사용해서 store 객체를 가져올 수 있다.

개발을 진행하면서 특정 데이터를 사용하는 패턴이 보인다면 커스텀 훅을 만드는 것처럼 커스텀 useStores 함수를 만들 수도 있을 것이다.

import { useObserver } from 'mobx-react'

function useUserData() {
  const { user, login } = useStores()
  return useObserver(() => ({ // useObserver를 사용해서 리턴하는 값의 업데이트를 계속 반영한다
    userName: user.name,
    isLoggedIn: login.isLoggedIn,
  }))
}

const UserInfo = () => { 
  const { username, isLoggedIn } = useUserData()
  return (
    <div>
      {username} is {isLoggedIn ? 'on' : 'off'}
    </div>
  )
}

useUserData가 리턴하는 객체가 useObserver로 래핑되어 있기에 컴포넌트를 observer로 래핑하지 않아도 동작한다. 하지만 컴포넌트에 다른 observable을 사용한다면 래핑이 필요하다.

함수형 컴포넌트를 위한 injector hoc

함수형 컴포넌트에서는 useStores를 사용하면 된다. 하지만 클래스 컴포넌트에서 사용하던 스타일로 inject hoc를 사용하고 싶다면 직접 구현할 수 있다. 공식 문서에서도 간단한 구현을 제공하고 있다. 하지만 개인적으로는 useStores를 사용하는 편이 더 간단하고 훅 API에도 어울려 보인다.

import { observer } from 'mobx-react'
import { useStores } from '../useStores'

function inject(selector, baseComponent) {
  const component = ownProps => {
    const store = useStores()
    return useObserver(() => baseComponent(selector(store, ownProps)))
  }
  component.displayName = baseComponent.name
  return component
}