Mobx 사용법 (Mobx와 React를 활용한 Todo앱)

jonyChoiGenius·2023년 10월 5일
0

React.js 치트 시트

목록 보기
22/22
  • Mobx는 객체지향 느낌의 라이브러리
  • 보일러 플레이트코드로 Component와 State를 연결하는 방식과 달리 데코레이터를 통해 해결한다.

@observable

@observable 데코레이터로 지정한 state는 관찰 대상으로 지정되고, 그 State는 값이 변경도리 때마다 렌더링 된다.

observable한 객체를 만들기 위해서는 makeObservable 함수를 사용하며, 각 프로퍼티마다 적절한 annotations를 지정한다.

  • observable 은 state를 저장하는 추적 가능한 필드를 정의
  • action은 state를 수정하는 메서드를 표시
  • computed는 state로부터 새로운 사실을 도출하고 그 결괏값을 캐시 하는 getter를 나타냄

Mobx 개념

Observable : '추적 가능한' 상태.
Action : State를 수정하는 메서드
Computed : state의 변화를 통해 계산되는 값.
Derivation : state를 통해서 자동으로 계산될 수 있는 모든 값 (Computed Value)를 포함
Reaction : state를 통해서 자동으로 수행되는 Task. 가령 특정 컴포넌트를 렌더링 하는 등을 의미한다. Derivation이 값을 만든다면, Reaction은 활동ㅇ르 수행한다.

즉 Action으로 Observable State를 변화시키면, 이를 통해 Derivation이 변화하거나 Reaction이 일어난다.

Mobx의 기본 원칙

  1. state가 변화하면 모든 derivation이 자동으로/동기식으로 업데이트됨 -> action이 일어난 직후에도 변경된 computed 값을 확인할 수 있음.
  2. computed 값은 '느리게Lazy' 업데이트 됨. 즉, 사용되고 있는 컴퓨티드 값만 업데이트가 일어나며, 사용하지 않는 경우 가비지 컬렉팅.
  3. computed는 순수하며, state를 변경하지 않음.

Mobx 사용해보기

리액트 환경을 셋팅한 상태에서
yarn add mobx 를 입력하여 Mobx를 설치할 수 있다.

다음은 클래스를 이용해서 상태를 관리하는 예시이다. todos라는 배열을 관리하며, 하나의 getter와 두 개의 메서드가 있다.

class TodoStore {
  // todos 상태
  todos = [];

  // todos에서 완료된 갯수를 가져오는 getter
  get completedTodosCount() {
    return this.todos.filter((todo) => todo.completed === true).length;
  }

  // todos의 길이와 완료된 갯수를 토대로 메시지를 반환하는 메서드
  report() {
    if (this.todos.length === 0) return "<할 일 없음>";
    const nextTodo = this.todos.find((todo) => todo.completed === false);
    return (
      `다음 할 일: "${nextTodo ? nextTodo.task : "<할 일 없음>"}".` +
      `진척도: ${this.completedTodosCount}/${this.todos.length}`
    );
  }

  addTodo(task) {
    this.todos.push({
      task: task,
      completed: false,
      assignee: null,
    });
  }
}

const todoStore = new TodoStore();

console.log(todoStore.report()); //<할 일 없음>

todoStore.addTodo("몹엑스 공부하기");
console.log(todoStore.report()); //다음 할 일: "몹엑스 공부하기".진척도: 0/1
todoStore.addTodo("몹엑스 때려치기");
console.log(todoStore.report()); //다음 할 일: "몹엑스 공부하기".진척도: 0/2
todoStore.todos[0].completed = true;
console.log(todoStore.report()); //다음 할 일: "몹엑스 때려치기".진척도: 1/2

변화가 일어날 때마다 console.log를 찍지 않고, mobx를 이용하여 자동으로 실행하도록 만들어보자.

import { makeObservable, observable, action, computed, autorun } from "mobx";

class ObservableTodoStore {
  // todos 배열 상태
  todos = [];
  // 남은 할일 숫자 상태
  pendingRequests = 0;

  // constructor에서 makeObservable 함수를 실행한다.
  constructor() {
    makeObservable(this, {
      // 해당 함수의 두번째 인자로 주석을 넣어준다.
      todos: observable,
      pendingRequests: observable,
      completedTodosCount: computed,
      report: computed,
      addTodo: action,
    });
    autorun(() => console.log(this.report));
  }

  get completedTodosCount() {
    return this.todos.filter((todo) => todo.completed === true).length;
  }

  get report() {
    if (this.todos.length === 0) return "<할 일 없음>";
    const nextTodo = this.todos.find((todo) => todo.completed === false);
    return (
      `다음 할 일: "${nextTodo ? nextTodo.task : "<할 일 없음>"}".` +
      `진척도: ${this.completedTodosCount}/${this.todos.length}`
    );
  }

  addTodo(task) {
    this.todos.push({
      task: task,
      completed: false,
      assignee: null,
    });
  }
}
const observableTodoStore = new ObservableTodoStore(); // <할 일 없음>
observableTodoStore.addTodo("몹엑스 공부하기"); // 다음 할 일: "몹엑스 공부하기".진척도: 0/1
observableTodoStore.addTodo("몹엑스 때려치기"); // 다음 할 일: "몹엑스 공부하기".진척도: 0/2
observableTodoStore.todos[0].completed = true; // 다음 할 일: "몹엑스 때려치기".진척도: 1/2

makeObservable

makeObservable(target, annotations?, options?)
mobx로부터 makeObservable을 import한다.
해당 함수를 클래스의 constructor 내부에서 실행한다.

annotations

makeObservable의 두번째 인자로 annotations를 입력하게 된다.
해당 annotations를 멤버들(즉, getter, getter, 상태, 메서드)이 mobx에서 하는 역할을 지정하게 된다.

makeObservable 대신 makeAutoObservable을 입력하면 이 작업을 대신 해준다.
프로퍼티 -> observable
getter -> computed
setter -> action
함수 -> autoAction
제너레이터 -> flow
단순히 읽는 용도로 사용하기 위해서는 false로 주석을 단다.

autorun

autorun(effect: (reaction) => void, options?) 함수는 내용이 변경될 때마다 실행되어야 하는 하나의 함수를 인자로 받는다. observable 혹은 computed가 변화할 때마다 실행된다.

리액트에서 Mobx 사용해보기

리액트에서 mobx를 사용하기 위해서는 mobx-react-lite를 설치하면 된다. mobx-react의 경량 버전으로, 리액트의 함수 컴포넌트에 최적화되어 있다.

yarn add mobx mobx-react-lite

mobx-react-lite의 작동 원리는 함수 컴포넌트 전체를 autorun으로 감싸는 것이다. mobx-react-lite의 'observer' 함수 내부적으로 autorun이 작동한다. 그 밖의 내부 동작은 다음 벨로그 글을 참조하자

Todo앱 만들기

해당 블로그 글을 참조했다.
먼저 TodoStore.ts를 만든다.

스토어 만들기
// TodoStore.ts
import { makeAutoObservable } from "mobx";

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

class TodoStore {
  	// 1
    todoData: TodoData[];
    currentId: number;

    constructor() {
        // 2
        makeAutoObservable(this, {}, { autoBind: true })
      	// 3
        this.todoData = []
        this.currentId= 0

    }
    
    addTodo(content:string):void {
        // 4
        this.todoData.push({id: this.currentId, content, checked:false})
        this.currentId++;
    }

    toggleTodo(id:number):void {
        const index = this.todoData.findIndex((v)=>v.id===id);
        if(id !== -1) {
            this.todoData[index].checked = !this.todoData[index].checked
        }
    }
}

// 5
export const todoStore = new TodoStore()
  1. '클래스'에서 상태를 선언할 때에, '프로퍼티명 : 타입'과 같이 입력하면 별도의 인터페이스 없이도 객체에 타입을 넣을 수 있다.
  2. makeAutoObservable(target, annotations?, options?)을 통해 객체를 observable로 만든다. 이때 세번째 인자로 옵션을 넘길 수 있는데, autoBind 옵션을 주었다. '메서드'는 자바스크립트에 this 바인딩 되지 않는다. 이를 자동으로 바인딩해주는 옵션이 autoBind이다. 혹은 화살표 함수를 사용할 수 있다.
  3. this.todoData = []와 같이 앞서 선언한 변수에 초기값을 넣어준다.
  4. 상태를 조작할 때에 객체불변성을 지킬 필요가 없다. Mobx에서 상태의 변화를 자동으로 감시한다.
  5. 객체를 생성하고, 생성된 객체를 export한다.
화면만들기

TodoList.tsx를 만들고,
observer로 컴포넌트를 감싸준다.
(만일 App.tsx에서 작업한다면 useObserver로 반환할 JSX태그를 감싼다.)

import { observer } from 'mobx-react-lite';
export const TodoListPage = observer(() => {})

이후 todoStore를 import하여 필요한 프로퍼티와 메서드를 불러온다.

import { todoStore } from './CreditManageStore';

export const TodoListPage = observer(() => {
    const { todoData, addTodo, toggleTodo } = todoStore;
  	...
})

이제 로직에 맞게 컴포넌트를 작성하면 된다.

export const TodoListPage = observer(() => {
    const [inputValue, setInputValue] = useState('');
    const { todoData, addTodo, toggleTodo } = todoStore;

    return (
        <div>
            {todoData.map((todo) => {
                return (
                    <div key={todo.id}>
                        {todo.id} /{todo.content} /
                        <input
                            type="checkbox"
                            checked={todo.checked}
                            onChange={() => toggleTodo(todo.id)}
                        />
                    </div>
                );
            })}
            <hr></hr>
            <input
                type="text"
                value={inputValue}
                onChange={(e) => {
                    setInputValue(e.target.value);
                }}
            />
            <button
                type="button"
                onClick={() => {
                    if (inputValue) addTodo(inputValue);
                }}
            >
                할 일 추가하기
            </button>
        </div>
    );
});

profile
천재가 되어버린 박제를 아시오?

0개의 댓글