본문 바로가기

React/상태관리하기

MobX 톺아보기 0 - 컨셉, 원칙

상태관리 라이브러리 MobX를 React에 적용하기 위해 공식 문서에서 추천하는 순서에 따라 톺아보며 익히려 한다.

 

 


[ Concept ]

 

우선 MobX의 컨셉을 살펴보자. 

 

MobX는 애플리케이션에서 다음 세 가지 개념을 구분한다.

  1. State (상태)
  2. Action (액션)
  3. Derivations (파생)

 

1. 상태를 정의하고 관찰 가능하게 만들기

상태는 애플리케이션을 작동하는 데이터이다. 일반적으로 할 일 목록과 같은 도메인별 상태와 현재 선택된 요소와 같은 보기 상태가 있다. 상태는 값을 지정하는 스프레드 시트 셀과도 같다.

 

상태는 일반 객체, 배열, 클래스, 순환 데이터 구조 또는 참조 등 원하는 데이터 구조에 저장이 가능하다. MobX의 작동 방식은 중요하지 않다. 시간이 지남에 따라 변경하려는 모든 프로퍼티를 관창 가능으로 표시하여 MobX에서 추적할 수 있도록 하기만 하면 된다.

 

import { makeObservable, observable, action } from "mobx"

class Todo {
    id = Math.random()
    title = ""
    finished = false

    constructor(title) {
        makeObservable(this, {
            title: observable,
            finished: observable,
            toggle: action
        })
        this.title = title
    }

    toggle() {
        this.finished = !this.finished
    }
}
해당 예시 코드는 makeAutoObservable을 사용하여 단축할 수 있지만, 명시적으로 표현하면 다양한 개념을 더 자세히 보여줄 수 있다.

 

observable을 사용하는 것은 객체의 프로퍼티를 스프레드시트 셀로 바꾸는 것과 같다. 하지만 스프레드시트와 달리 이러한 값은 원시 값뿐만 아니라 참조, 객체 및 배열도 포함할 수 있다.

 

하지만 우리가 액션으로 표시한 toggle은 어떨까?

 

 

✏️ 생성자에서 makeObservable를 이용하여 observable을 설정하여 정의한 상태를 관찰 가능하게 할 수 있는 것 같다.

 

 

2. 액션을 사용하여 상태 업데이트 하기

액션은 상태를 변경하는 모든 코드이다. 사용자 이벤트, 백엔드 데이터 푸시, 예약된 이벤트 등이 이에 해당한다. 액션은 사용자가 스프레드시트 셀에 새 값을 입력하는 것과 같다.

 

위의 Todo 모델에서는 finished의 값을 변경하는 toggle 메서드가 있음을 알 수 있다. finished는 observable으로 표시된다. observable을 변경하는 모든 코드를 액션으로 표시하는 것이 좋다. 이렇게 하면 MobX가 자동으로 트랜잭션을 적용하여 손쉽게 최적의 성능을 낼 수 있다.

 

액션을 사용하면 코드를 구조화하는 데 도움이 되며 의도하지 않은 상태에서 실수로 상태를 변경하는 것을 방지할 수 있다. 상태를 수정하는 메서드를 MobX 용어로는 액션이라고 한다. 현재 상태를 기반으로 새로운 정보를 계산하는 뷰와는 대조적이다. 모든 메서드는 이 두 가지 목적 중 하나만 수행해야 한다.

 

 

✏️ observable 된 프로퍼티를 변경하는 모든 메서드를 모두 액션으로 표시해 주게 되면 최적의 성능이 가능하다. 또한 의도하지 않게 상태를 변경하는 것을 방지할 수 있다.

 

 

 

3. 상태 변경에 자동으로 반응하는 파생 요소 만들기

추가 상호 작용 없이 상태에서 파생할 수 있는 모든 것이 파생이다. 파생은 다양한 형태로 존재한다.

  • 사용자 인터페이스
  • 남은 todo의 수와 같은 파생 데이터
  • 백엔드 통합(예 : 서버로 변경 사항 전송)

 

MobX는 두 가지 종류의 파생을 구분한다.

  • 순수 함수를 사용하여 현재 관찰 가능한 상태에서 항상 파생될 수 있는 computed 값.
  • 상태가 변경될 때 자동으로 발생해야 하는 부작용, 리액션.(명령형 프로그래밍과 리액티브 프로그래밍 사이의 다리)

 

MobX를 처음 시작할 때 사람들은 리액션을 과도하게 사용하는 경향이 있다. golden rule은 현재 상태를 기반으로 값을 만들려면 항상 computed를 사용해야 한다는 것이다.

 

 

3-1. computed를 사용하여 파생된 값 모델링

 

import { makeObservable, observable, computed } from "mobx"

class TodoList {
    todos = []
    get unfinishedTodoCount() {
        return this.todos.filter(todo => !todo.finished).length
    }
    constructor(todos) {
        makeObservable(this, {
            todos: observable,
            unfinishedTodoCount: computed
        })
        this.todos = todos
    }
}

 

MobX는 todo가 추가되거나 todo의 finished 속성이 수정될 때 unfinishedTodoCount가 자동으로 업데이트되도록 한다. 

 

이러한 computed는 MS Excel과 같은 스프레드시트 프로그램의 수식과 유사하다. 필요할 때만 자동으로 업데이트된다. 즉, 무언가가 그 결과에 관심을 가질 때만 업데이트된다.

 

✏️ 어떠한 속성을 observable 설정해 놓으면 해당 속성이 변경될 때마다 파생된 값을 이용하여 computed 한다는 것 같다.

 

 

3-2. 리액션을 사용하여 부작용 모델링하기

사용자가 화면에서 상태 또는 computed 된 값의 변화를 확인할 수 있으려면 GUI의 일부를 다시 프린팅 하는 리액션이 필요하다.

 

리액션은 computed 된 값과 비슷하지만 정보를 생성하는 대신 콘솔에 출력, 네트워크 요청, 리액트 컴포넌트 트리를 점진적으로 업데이트하여 DOM을 패치하는 등의 부작용을 일으킨다.

 

요컨대, 리액션은 리액티브 프로그래밍과 명령형 프로그래밍을 연결한다.

 

가장 많이 사용되는 형태의 리액션은 UI 컴포넌트이다. 액션과 리액션 모두 부작용을 유발한다는 점에 유의해야 한다. 양식을 제출할 때 네트워크 요청을 하는 것과 같이 트리거할 수 있는 명확하고 명시적인 출처가 있는 부작용은 관련 이벤트 핸들러에서 명시적으로 트리거해야 한다.

 

✏️ 리액션은 DOM을 패치하는 부작용이 발생하기 때문에 UI 컴포넌트에서 많이 사용되고, 네트워크 요청과 같은 명확한 부작용의 경우 이벤트 핸들러에서 명시적으로 트리거해야 된다는 것 같다.

 

 

3-3. 반응형 리액트 컴포넌트

리액트를 사용하는 경우, 설치 시 선택한 바인딩 패키지의 observer 함수로 컴포넌트를 래핑 하여 반응형 컴포넌트로 만들 수 있다. 아래의 예제에서는 더 가벼운 mobx-react-lite 패키지를 사용한다.

 

import * as React from "react"
import { render } from "react-dom"
import { observer } from "mobx-react-lite"

const TodoListView = observer(({ todoList }) => (
    <div>
        <ul>
            {todoList.todos.map(todo => (
                <TodoView todo={todo} key={todo.id} />
            ))}
        </ul>
        Tasks left: {todoList.unfinishedTodoCount}
    </div>
))

const TodoView = observer(({ todo }) => (
    <li>
        <input type="checkbox" checked={todo.finished} onClick={() => todo.toggle()} />
        {todo.title}
    </li>
))

const store = new TodoList([new Todo("Get Coffee"), new Todo("Write simpler code")])
render(<TodoListView todoList={store} />, document.getElementById("root"))

 

observer는 리액트 컴포넌트를 렌더링 하는  데이터의 파생물로 변환한다. MobX를 사용할 때는 smart 컴포넌트나 dump 컴포넌트가 없다. 모든 컴포넌트는 smart 하게 렌더링 되지만, dumb 방식으로 정의된다. MobX는 필요할 때마다 컴포넌트가 항상 다시 렌더링 되도록 할 뿐 그 이상은 하지 않는다.


  • smart 컴포넌트나 dumb 컴포넌트

smart 컴포넌트나 dumb 컴포넌트에 대해 간략하게 정리하면 다음과 같다. 모든 컴포넌트가 State가 필요한 것은 아니다. State가 없고 필요하지 않은 컴포넌트를 dumb 컴포넌트라고 한다. 반대로 State가 있는 컴포넌트를 smart 컴포넌트라고 한다.

 


 

따라서 위 예제의 onClick 핸들러는 toggle 액션을 사용할 때 적절한 TodoView 컴포넌트를 강제로 다시 렌더링 하지만, 완료되지 않은 todo의 수가 변경된 경우에만 TodoListView 컴포넌트를 다시 렌더링 하게 된다. 또한 Task left 줄을 제거하거나 별도의 컴포넌트에 넣으면 작업을 체크 표시할 때 ToDoListView 컴포넌트가 더 이상 렌더링되지 않는다.

 

 

 

3-4. 커스텀 리액션

거의 필요하지 않지만 autorun, reaction 또는 when 함수를 사용하여 특정 상황에 맞게 만들 수 있다. 예를 들어, 다음 autorun은 unfinishedTodoCount가 변경될 때마다 로그 메시지를 출력한다.

 

// 싱태를 자동으로 관찰하는 함수
autorun(() => {
    console.log("Tasks left: " + todos.unfinishedTodoCount)
})

 

unfinishedTodoCount를 변경할 때마다 새로운 메시지가 출력되는 무엇일까? 답은 바로 rule of thumb이다.

 

MobX는 추적된 함수를 실행하는 동안 읽은 기존의 관찰 가능한 프로퍼티에 반응한다.

 

 

 

 


[ Principles ]

 

다음으로 MobX의 원칙을 살펴보자.

 

MobX는 액션이 상태를 변경하면 영향을 받는 모든 VIew가 업데이트되는 단방향 데이터 흐름을 사용한다.

 

  1. 모든 파생물은 상태가 변경되면 자동으로 원자 단위로 업데이트된다. 따라서 중간 값을 관찰할 수 없다.
  2. 모든 파생은 기본적으로 동기식으로 업데이트된다. 예를 들어 액션이 상태를 변경한 직후 computed 값을 안전하게 검사할 수 있다.
  3. computed 값은 느리게 업데이트된다. 활발하게 사용되지 않는 computed 값은 부작용에 필요할 때까지 업데이트되지 않는다. View가 더 이상 사용되지 않으면 자동으로 가비지에 수집된다.
  4. 모든 computed 값은 순수해야 한다. 상태를 변경해서는 안된다.