image

React Hooks + TypeScript + MobX 사용해보기

React에는 여러 상태관리 라이브러리가 있다. 대표적으로 Redux, MobX, 그리고 GraphQL에서 사용하는 Apollo Client가 있다.

나는 여기서 RestAPI일 경우 Redux, GraphQL일 경우 Apollo Client를 사용하는 편이다. 뭔가 MobX같은 경우는 나중에 해봐야지~ 하면서 미뤘던 것 같다.

그런데 찾아보니 mobx-react v6부터 mobx-react-lite를 사용하지 않고도 React Hooks문법을 지원한다고 알려져서 슬슬 배워봐야지 하고 생각하고 있었다.

(가끔 class 문법도 사용하긴 하지만 나는 hooks문법을 사랑한다 ❤️)

MobX란?

Redux와는 다른 느낌의 상태관리 라이브러리 이다. 크게 Store와 Action으로 이루어져 있어 리덕스(Store, Action, Reducer 등...)와 다르게 구조가 엄청 간단하다!

image

그런데 최근 npm trends를 살표보면 생각보다 인기가 많은 것 같지는 않다. 2020년 6월 30일 기준으로 1년간 다운로드 수를 살펴봤는데 Redux가 1등, Apollo Client가 2등, MobX가 3등이다...

그리고 직접 사용해보니 편하긴 한데 익숙하지 않아서 그런지 나한테는 맞지 않는 듯 했다. (자유도가 너무 높아 딱! 정해진 방법이 없고 사용자마다 스타일이 다른것 같다.)

이번 포스트에서는 MobX를 학습하기 위해 간단한 Todo List를 만들어 볼 생각이다.

설치

$npm i mobx mobx-react

mobxmobx-react를 설치한다. mobx-react는 v6이상부터 hooks문법을 지원한다.

Store

스토어를 만들기 위해 src폴더 안에 stores라는 폴더를 새로 생성한 뒤 todo.ts라는 파일을 작성한다.

/src/stores/todo.ts

import { observable } from 'mobx';

export interface TodoData {
  id: number;
  content: string;
  checked: boolean;
}

interface Todo {
  todoData: TodoData[];
  currentId: number;
  addTodo: (content: string) => void;
  removeTodo: (id: number) => void;
}

export const todo = observable<Todo>({
  todoData: [],
  currentId: 0,

  addTodo(content) {
    this.todoData.push({ id: this.currentId, content, checked: false });
    this.currentId++;
  },
  removeTodo(id) {
    const index = this.todoData.findIndex((v) => v.id === id);
    if (id !== -1) {
      this.todoData.splice(index, 1);
    }
  },
});

MobX에서 스토어를 만드는 방법은 정말 간단하다. 다음과 같이 todo라는 객체를 선언 한 뒤 observable로 감싸주면 끝난다.

그리고 불변성을 지켜줄 필요가 없다. 저 observable이 상태가 변화는지 관찰해 주기 때문이다. 액션들은 스토어 안쪽에 같이 작성할 수 있다.

/src/useStore.ts

import { todo } from './stores/todo';

const useStore = () => ({ todo });

export default useStore;

useStore는 컴포넌트 마다 스토어를 사용하기 위해 작성한다. 만약 스토어가 여러개일 경우 불러와서 합쳐주면 된다. (합치지 않고 개별적으로 불러와 사용할 수 도 있다.)

Todo List 만들기

Todo 데이터를 표시해주는 Todo Item과 Todo Item들을 나열해주는 Todo List를 작성했다.

/src/component/TodoItem.tsx

import React from 'react';

import useStore from '../useStore';
import { TodoData } from '../stores/todo';

interface Props {
  data: TodoData;
}

const TodoItem = ({ data }: Props) => {
  const { todo } = useStore();

  const removeItem = () => {
    todo.removeTodo(data.id);
  };

  return (
    <div>
      <input type="checkbox" />
      <span>{data.content}</span>
      <span onClick={removeItem}></span>
    </div>
  );
};

export default TodoItem;

TodoLIst에 표시할 Item이다. todo 스토어의 액션을 사용하기 위해 useStore를 사용해 가져왔다. todo.removeTodo는 스토어를 생성할 당시 만들어준 액션이다.

/src/component/TodoList.tsx

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

import useStore from '../useStore';
import TodoItem from './TodoItem';

const TodoList = () => {
  const {
    todo: { todoData },
  } = useStore();

  return useObserver(() => (
    <section>
      {todoData.map((v) => (
        <TodoItem data={v} key={`todoData_${v.id}`} />
      ))}
    </section>
  ));
};

export default TodoList;

TodoList에선 스토어에서 데이터만 가져와 TodoItem으로 뿌려주게 된다. 데이터를 사용할 때는 해당 컴포넌트를 useObserver로 감싸워야 한다.

/src/component/TodoForm.tsx

import React, { useState } from 'react';
import useStore from '../useStore';

const TodoForm = () => {
  const { todo } = useStore();
  const [content, setContent] = useState('');

  const onSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    todo.addTodo(content);
  };

  const onChangeContent = (e: React.ChangeEvent<HTMLInputElement>) => {
    setContent(e.target.value);
  };

  return (
    <form onSubmit={onSubmit}>
      <input type="text" onChange={onChangeContent} value={content} placeholder="내용" />
      <button type="submit">입력</button>
    </form>
  );
};

export default TodoForm;

이제 todo스토어에 데이터를 추가할 폼을 작성했다. 여기서도 동일하게 useStore로 가져온 todo.addTodo를 이용해 데이터를 추가해 줬다.

간단하게 만들려고는 했는데 너무 간단해서 벌써 끝나버렸다. 😅

image

빌드한 후 실행해 보니 잘 작동하는 것 같다.

마무리

MobX를 간단하게 체험하듯이 둘러봤는데 다음 프로젝트 진행할 때 한번 사용해봐야 느낌이 올 것 같다. 전체적으로 라이브러리의 자유도가 너무 높아서 협업으로 작업할 때는 초기에 스타일을 통일해야 코드가 안망가질것 같다....

개인적으로는 편하긴 한데 아직까진 Redux에 조금 더 손이 간다. 그래도 다음 프로젝트때 한번 깊은 단계(?) 까지는 사용해보고 싶다.