MobX : Hooks와 함께 사용하기

Seoyul Kim·2020년 8월 31일
9

React

목록 보기
5/5

✔️ 여기서는 MobX를 클래스형이 아닌 함수형 컴포넌트에서 Hooks, 그리고 Context API와 함께 사용하는 방법을 정리한다.

  • 먼저 MobX와 함께 사용될 Context API에 대하여 알아보도록 하자.

Context

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}
  • 주로 어플리케이션으로 전역적으로 데이터가 사용되어야 할 때 사용된다. 즉, 데이터를 일일이 전달할 필요 없이 한번에 특정 컴포넌트에 전달하는 방법으로 글로벌한 데이터를 관리할 때 사용한다.

  • Context 를 통해서 원하는 값이나 함수를 바로 사용할 수 있다.

  • Context 는 createContext 라는 함수를 사용해서 만들며, 이 함수를 호출하면 Provider 와 Consumer 라는 컴포넌트들이 반환된다.

  • Provider 는 Context 에서 사용할 값을 설정할 때 사용되고, Consumer 는 나중에 우리가 설정한 값을 불러와야 할 때 사용된다.

API

React.createContext

const MyContext = React.createContext(defaultValue);
  • Context 객체를 만든다. Context 객체를 구독하고 있는 컴포넌트를 렌더링할 때 React는 트리 상위에서 가장 가까이 있는 짝이 맞는 Provider로부터 현재값을 읽는다.

  • defaultValue 매개변수는 트리 안에서 적절한 Provider를 찾지 못했을 때만 쓰이는 값이다.

Context.Provider

<MyContext.Provider value={/* 어떤 값 */}>
  • Context 오브젝트에 포함된 React 컴포넌트인 Provider는 context를 구독하는 컴포넌트들에게 context의 변화를 알리는 역할을 한다.

  • Provider 는 value prop를 받아서 이 값을 하위에 있는 컴포넌트에게 전달한다. 값을 전달받을 수 있는 컴포넌트의 수에 제한은 없으며, Provider 하위에 또 다른 Provider를 배치하는 것도 가능하고, 이 경우 하위 Provider의 값이 우선시된다.

  • Provider 하위에서 context를 구독하는 모든 컴포넌트는 Provider의 value prop가 바뀔 때마다 다시 렌더링 된다. Provider로부터 하위 consumer(.contextType와 useContext을 포함한)로의 전파는 shouldComponentUpdate 메서드가 적용되지 않으므로, 상위 컴포넌트가 업데이트를 건너 뛰더라도 consumer가 업데이트된다.

Class.contextType

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* MyContext의 값을 이용한 코드 */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* ... */
  }
}
MyClass.contextType = MyContext;
  • React.createContext()로 생성한 Context 객체를 원하는 클래스의 contextType 프로퍼티로 지정할 수 있다. 그러면 그 클래스 안에서 this.context를 이용해 해당 Context의 가장 가까운 Provider를 찾아 그 값을 읽을 수 있게되며, 이 값은 render를 포함한 모든 컴포넌트 생명주기 매서드에서 사용할 수 있다.

  • 참고로 이 API를 사용하면 하나의 context만 구독할 수 있다.

Context.Consumer

<MyContext.Consumer>
  {value => /* context 값을 이용한 렌더링 */}
</MyContext.Consumer>
  • context 변화를 구독하는 React 컴포넌트로 함수 컴포넌트안에서 context를 읽기 위해서 쓸 수 있다.

  • Context.Consumer의 자식은 함수여야한다. 이 함수는 context의 현재값을 받고 React 노드를 반환한다. 이 함수가 받는 value 매개변수 값은 해당 context의 Provider 중 상위 트리에서 가장 가까운 Provider의 value prop과 동일하며 상위에 Provider가 없다면 value 매개변수 값은 createContext()에 보냈던 defaultValue와 동일하다.

Context.displayName

const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';

<MyContext.Provider> // "MyDisplayName.Provider" in DevTools
<MyContext.Consumer> // "MyDisplayName.Consumer" in DevTools
  • Context 객체는 displayName 문자열 속성을 설정할 수 있다. React 개발자 도구는 이 문자열을 사용해서 context를 어떻게 보여줄 지 결정한다.

context API 적용해보기

  • 컴포넌트 트리의 형태는 다음과 같다
<App>
  <Layout>
    <NavBar>
  • Context를 사용하기 위해서 React.createContext(defaultValue)로 Context 객체를 만든다.

  • Context를 만들면 Provider와 Consumer를 사용할 수 있다. 이 두 가지의 역할은 변수를 전달하는 공급자와 전달받은 변수를 사용하는 소비자이다.


//괄호 안에는 기본값을 넣어준다.
const CurrentUserContext = React.createContext(null);

class App extends React.Component {
  state = { currentUser: null };

  render() {
    return (
      <CurrentUserContext.Provider currentUser={this.state.currentUser}>
        <Layout>
      </CurrentUserContext.Provider>
    );
  }
}
  • Layout을 포함한 모든 하위 컴포넌트들은 이제부터 공급자로부터 전달받은 currentUser 변수에 접근할 수 있다.
class NavBar extends Component {
  render() {
    return (
      <CurrentUserContext.Consumer>
        {currentUser => (
          <a href="/">Home</a>
          <a href="/about">About</a>
          <a href=`/profile/${currentUser.id}`>
        )}
      </CurrentUserContext.Consumer>
    );
  }
}
  • 이 함수는 현재 Context의 value(currentUser)를 인자로 받고 React 노드를 반환하는 함수이다. 이 value는 Context의 가장 가까운 공급자의 값과 같다. 하나의 공급자가 있지만 여러 개의 공급자가 존재할 수도 있다. Consumer는 컴포넌트 트리의 최상위에서부터 가장 가까운 공급자의 value를 가져온다.

  • context의 Provider는 단순히 값을 전달하는 연결고리일 뿐이다. 데이터를 유지하는 기능은 없다. 대신에 wrapper를 만들어서 데이터를 관리하게 만들 수 있다.

  • Provider에서 전달하는 값엔 어떤 데이터든지 전달이 가능하다. 즉, 함수도 전달이 가능하며 value 뿐만 아니라 ‘Action’도 전달이 가능하다.

MobX

  • State 관리 라이브러리로 기본적으로 객체 지향 느낌이 강하며, 컴포넌트와 state를 연결하는 리덕스와 달리 번잡한 보일러플레이트 코드들을 데코레이터 제공으로 깔끔하게 해결한다.

    👉🏻 Redux

    • Flux 개념을 바탕으로 한 React에서 현재 가장 많이 사용되는 state 관리 라이브러리이다.
  • mobx-react는 observable한 상태들, computed로 계산된 결과를 메모이즈 해둘 수 있는 기능들 등 리덕스와는 다른 매력을 가지고 있는 라이브러리이다.

  • mobx는 리덕스와 다르게 관심사에 따라서 여러개의 스토어를 만들고 필요에 따라서 컴포넌트 위에 프로바이더를 만들어서 사용할 수 있다.

  • observer 함수 자체가 클래스 컴포넌트를 리턴하는 hoc(higher order component)으로 클래스를 위해서 만들어진 함수이기 때문에 mobx-react v5에서는 hooks와는 호환되지 않았는데 이런 구조들을 개선하기 위해서 mobx-react-lite가 나왔다.(mobx-react v6도 가능하다.)

  • mobx-react-lite는 훅을 지원하기 위해 함수형 컴포넌트에서만 사용할 수 있는 API만 제공하고 있으며 React.createContext API를 사용해서 store를 가져올 수 있다.

React 함수형 컴포넌트에서 MobX 사용하기(Typescript)

//implicit return 사용(다르게는 return 으로 적어서 사용할 수 있다.)
const userSelected = useLocalStore(() => ({
    answers: [],
    addAnswer: (answer: string) => {
      userSelected.answers.push(answer);
    },
  }));
  • 먼저 상태를 저장할 수 있는 저장소를 만들어준다. store는 우리가 관찰하고 있는 데이터와 그 데이터를 추가 및 수정할 수 있는 함수를 가질 수 있다.

    ✔️ implicit return
    When using implicit returns, object literals must be wrapped in parenthesis so that the curly braces are not mistaken for the opening of the function's body.

    const foo = () => { bar: 1 } // foo() returns undefined
    const foo = () => ({ bar: 1 }) // foo() returns {bar: 1}
export const AnswerContext = createContext<AnswerState>({} as AnswerState);
  • 다른 컴포넌트에서도 state를 사용하기 위해서 context를 만들어준다.
//감싸줄 childern 컴포넌트를 받는다.
const AnswerProvider: React.FC<React.PropsWithChildren<{}>> = ({
  children,
}) => {
   
   //provider 안에 정의한 store를 넣어준다. 
  const userSelected = useLocalStore(() => ({
    answers: [],
    addAnswer: (answer: string) => {
      userSelected.answers.push(answer);
    },
  }));
  
  //provider가 children 컴포넌트들을 감싸게하고
  return (
    <AnswerContext.Provider value={userSelected}>
      {children}
    </AnswerContext.Provider>
  );
};
  • 다른 컴포넌트들을 모두 감싸줄 provider를 만든다.

  • context를 가져오면 provider가 있는데 그 provider가 children 컴포넌트들을 감싸게 하고 value를 넘겨준다. 이 value는 context value로 컴포넌트의 모든 레벨에서 사용할 수 있다.

  • provider에게 제공해야하는 props 중 하나는 value이다. 그 value는 정의한 store가 되도록한다.

ReactDOM.render(
  <>
    <AnswerProvider>
      <App />
    </AnswerProvider>
  </>,
  document.getElementById("root")
);
  • provider를 가져와 다른 컴포넌트를 감싸게 한다. store provider안에 있다면 모든 children 컴포넌트들은 value(정의한 store)를 사용할 수 있다.
const AnsweredList = () => {
  const AnswerStore = useContext(AnswerContext);


  return useObserver(() => (
    <div />
  ));
};
  • StoreContext 접근하여 데이터를 가져오기 위해서는 컨텍스트에 접근하게 할 수 있는 훅인 useContext를 사용한다.

  • 여기에서 AnswerStore는 provider에서 넘겨준 value이다.

  • 데이터가 변할 때 마다 변경사항을 알아차리기 위해서 return 시 useObserver로 감싸준다.(데이터를 단순히 사용할 경우에는 감싸줄 필요가 없고 변경 사항이 있을 경우에만 감싸준다.)

전체 코드

import React, { createContext } from "react";
import { useLocalStore } from "mobx-react";

//context value를 위한 타입을 지정해준다.(클래스를 타입스크립트의 타입으로도 지정할 수 있다.)
interface AnswerStateValue {
  answers: string[];
  addAnswer: (answer: string) => void;
}

//context를 사용하면 모든 컴포넌트를 일일이 통하지 않고도 원하는 값을 컴포넌트 트리 깊숙한 곳까지 보낼 수 있다.
//AnswerStateValue의 default value로 빈 객체를 지정한다.(store가 initiallized되지 않을 일은 없고 항상 정의되어있기를 원하기 때문)그리고 이 빈 객체는 컴포넌트가 context provider로 감싸져 있지 않을 때만 사용된다. 
export const AnswerContext = createContext<AnswerStateValue>({} as AnswerState);

// Provider 에서 state 를 사용하기 위해서 컴포넌트를 새로 만들어줍니다.
//children을 받기 위해서는 children을 포함하고 있는 props의 타입을 정해준다.
const AnswerProvider: React.FC<React.PropsWithChildren<{}>> = ({
  children,
}) => {
// class로 개발된 mobx 스토어든, mst로 작성된 스토어든, 여기서 초기화를 시켜준다.
  const userSelected = useLocalStore(() => ({
    answers: [],
    addAnswer: (answer: string) => {
      userSelected.answers.push(answer);
    },
  }));
  
  //Provider 내에서 사용할 값은, "value" 라고 부른다.
  //Provider를 이용해 하위 트리로 값을 보낼 수 있다.
  return (
    <AnswerContext.Provider value={userSelected}>
      {children}
    </AnswerContext.Provider>
  );
};

export default AnswerProvider;

// 값이 필요하다면 그냥 useContext를 사용하면 된다.
const AnsweredList = () => {
  const AnswerStore = useContext(AnswerContext);


  return useObserver(() => (
    <div />
  ));
};
  • 위와 같이 함으로서 mobx는 inject를 사용할 필요가 없고 간결하게 작성할 수 있으며, context를 이용하기 때문에 타입스크립트의 타입추론도 더 잘 받을 수 있다.

✔️ propsWithChildren
We can use a handy type called PropsWithChildren which will automatically include the children prop for us:

type Props = {
  name: string;
}
const MyComponent: React.FC<PropsWithChildren<Props>> = ({ name, children }) => <h1>{children}, {name}!</h1>

✔️ Using React.createContext with an empty object as default value.

interface ContextState {
// set the type of state you want to handle with context e.g.
name: string | null;
}
//set an empty object as default state
const Context = React.createContext({} as ContextState);
// set up context provider as you normally would in JavaScript [React Context API](https://reactjs.org/docs/context.html#api)
import React from "react";
import ReactDOM from "react-dom";
import "mobx-react/batchingForReactDom";
import App from "./App";
import AnswerProvider from "./stores/AnsweredList";

ReactDOM.render(
  <>
    <AnswerProvider>
      <App />
    </AnswerProvider>
  </>,
  document.getElementById("root")
);
  • Context 를 프로젝트에 적용하려면 앱을 Provider 로 감싸주어야한다.

useLocalStore

  • 로컬 observable state는 useLocalStore hook을 사용해 소개될 수 있다. useLocalStore는 initializer 함수를 한번 실행시키는데, observable store를 생성하고, 컴포넌트의 lifetime 동안 유지한다.

  • return된 객체의 모든 property는 자동적으로 observable될 수 있다. getter는 computed property로 변할 것이고, method는 store에 bind될 것이며 mobx transaction을 자동적으로 적용할 것이다. 만약 새로운 인스턴스가 initializer에서 반환되면, 그 인스턴스는 그 상태로 유지될 것이다.

useContext

const value = useContext(MyContext);
  • context 객체(React.createContext에서 반환된 값)을 받아 그 context의 현재 값을 반환한다. context의 현재 값은 트리 안에서 이 Hook을 호출하는 컴포넌트에 가장 가까이에 있는 <MyContext.Provider>의 value prop에 의해 결정된다.

  • 컴포넌트에서 가장 가까운 <MyContext.Provider>가 갱신되면 이 Hook은 그 MyContext provider에게 전달된 가장 최신의 context value를 사용하여 렌더러를 트리거 한다.

  • useContext로 전달한 인자는 Provider나 consumer가 아닌 context 객체 그 자체이어야 한다.

  • useContext를 호출한 컴포넌트는 context 값이 변경되면 항상 리렌더링 되며, 만약 컴포넌트를 리렌더링 하는 것에 비용이 많이 든다면 메모이제이션을 사용하여 최적화 하는 것이 좋다.

스토어 합치기

// 원본 코드
// https://github.com/mobxjs/mobx-react/blob/master/src/Provider.js
import React from "react";

// 컨텍스트를 만든다.
export const MobXProviderContext = React.createContext({});

export function Provider({ children, ...stores }) {
  // 상위 컨텍스트의 스토어들 가져온다.
  const parentValue = React.useContext(MobXProviderContext);
  // 부모의 컨텍스트 값과 새로운 스토어들을 가져와 합쳐준다.
  const value = React.useRef({
    ...parentValue,
    ...stores
  }).current;

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

Provider.displayName = "MobXProvider";
const UserProfile = () => {
  // Inject에 대응하는 부분이다.
  const { userStore } = useContext(MobxProviderContext);

  // 이렇게 하면 타입스크립트의 느낌표(!) 지옥에서도 빠져나올 수 있다.
  if (!userStore) {
    throw "userStore가 없습니다";
  }

  // observe에 대응하는 부분이다.
  return useObserver(() => (
    <div>
      {userStore.name} - {userStore.age}
    </div>
  ));

  // 혹은, 랜더 시에 필요한 특정 값만 필요하다면 이렇게도 사용할 수 있다.
  const businessLogicProfile = useObserver(() => {
    if (!userStore.public) {
      return "조회할수 없는 프로필입니다";
    }
    return `${userStore.name} - ${userStore.age}`;
  });

  return <div>{businessLogicProfile}</div>;
};

기본 용어

Observable

  • MobX에서 렌더링 대상이 되는 state를 관찰 대상(observable value)라고 하며 @observable 데코레이너로 지정한 state는 관찰 대상으로 지정되고 그 state는 값이 변경될 때 마다 렌더링 된다.

Store

  • 글로벌 영역에서 어플리케이션의 state와 비즈니스 로직을 가지고 있는 주체를 store라고 한다.

  • state를 글로벌한 영역에서 관라힌다는 말은 state 관리 라이브러리 사용의 목적 중 한가지로 리덕스에서는 state와 state를 핸들링하는 비즈니스 로직을 가지고있는 reducer, action 등을 포함하는 의미이기도 하지만, MobX에서 store는 명확히 state와 비즈니스 로직을 포함하는 class를 store라고 부른다.


우아한 형제들 기술 블로그
mobx-react v6 마이그레이션하기
Context
MobX with React
리액트 16.3 에 소개된 새로워진 Context API 파헤치기
MobX를 React Hooks(TypeScript)와 함께 사용하는 방법
mobx-react와 React Hooks API 함께 사용하기
React 16 Context 사용하기
MobX 6.X, MobX vs ContextAPI
Using MobX with React Hooks and TypeScript
How To Use React With MobX And Hooks - Note Taking App Tutorial
Introduction to MobX & React in 2020
Fetch Data With Mobx Note Taking App Using React Mobx and Typescript
React TypeScript Cheatsheet

1개의 댓글

comment-user-thumbnail
2020년 12월 9일

잘봤습니다! 데코레이트를 사용할 수 없는게 살짝 아쉽네요...

답글 달기