상세 컨텐츠

본문 제목

[MobX] 공식문서 살펴보기 : MobX 요지, Store 역할

😎 지식/FE-리액트🌐

by :Eundms 2024. 5. 20. 10:09

본문

 

MobX는 상태(State), 동작(Action), 파생(Derivation) 개념을 구분한다

 

1. State : 애플리케이션을 구동하는 데이터 

todo 아이템과 같은 도메인 별 state

현재 선택된 요소와 같은 view state

 

원하는 데이터 구조에 state을 저장한다

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
    }
}

 

2. action을 이용한 state 업데이트

사용자 이벤트, 백엔드 데이터 푸시, 예약된 이벤트 등과 같이 state를 변경하는 코드 조각 

 

observable을 변경하는 코드는 action으로 표시 

현재의 state를 기반으로 새로운 정보를 계산하는 view와는 다르다

 

3. state 변화에 자동으로 응답하는 derivation 만들기

사용자 인터페이스, 남은 todos의 수와 같은 파생 데이터, 백엔드 통합

 

- computed 값 : 현재의 observable state에서 순수 함수를 사용하여 파생될 수 있는 값

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
    }
}

todo가 추가되거나 finished 속성 중 하나가 수정될 때 unfinishedTodoCount를 자동으로 업데이트한다

 

- reaction : state가 변경될 때 자동으로 발생해야 하는 부수효과 (명령형 프로그램밍과 반응형 프로그래밍 사이를 연결해주는 다리 효과)

GUI의 일부를 다시그리는 행동

 

콘솔 출력, 네트워크 요청, DOM 패치 적용을 위해 React 컴포넌트 트리를 점진적으로 업데이트하는 부수효과를 생성

action과 reaction 모두 부수효과를 일으킬 수 있다

 

form 을 제출할 때 네트워크 요청을 하는 것 처럼, 트리거 될 수 있는 명확하고 명시적인 출처가 있는 부수효과는 관련 이벤트 핸들러에서 명시적으로 트리거 되어야 한다

 

어떻게? >> observer 함수를 이용하여 컴포넌트를 감싼다

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"))

onClick 핸들러는 toggle action을 사용할 때 적절한 TodoView 컴포넌트를 강제로 다시 렌더링하지만, 

TodoListView 컴포넌트는 완료되지 않은 작업의 수(unfinishedTodoCount)가 변경된 경우에만 다시 렌더링 된다. 

 

action이 state를 변경하는 단방향 데이터 흐름을 사용하여 영향을 받는 모든 view를 업데이트한다

1. state가 변경되면 derivation이 자동, 원자 단위로 업데이트된다

2. derivation은 동기식으로 업데이트한다

3. computed는 느리게 업데이트된다

4. computed값은 순수해야 하며, state를 바꾸면 안된다


데이터 스토어 정의

스토어의 주요 책임은 컴포넌트의 로직 state를 프론트엔드 및 백엔드에서 사용할 수 있고 독립으로 테스트할 수 있는 단위로 만드는 것

도메인 state 저장소 UI state 저장소를 분리함으로써 도메인 state를 재사용하고 테스트할 수 있는 장점이 있고

다른 애플리케이션에서 재사용할 수 있다

 

도메인 스토어 

하나의 도메인 스토어는 애플리케이션에서 하나의 개념을 담당해야 한다

여러 도메인 객체가 내부에 있는 트리 구조로 구성될 수 있다

 

스토어는 도메인 객체를 인스턴스화하며 도메인 객체가 자신이 속한 저장소를 알고 있는지 확인한다

각 도메인 객체의 인스턴스가 하나만 있는지 확인한다

백엔드에서 업데이트 내용을 받은 경우 기존 인스턴스를 업데이트한다

스토어를 테스트할 수 있고, 서버 측에서 실행할 수 있는지 확인하기 위해 HTTP 요청을 별도의 객체로 이동하여 통신 계층을 추상화할 수 있어야 한다

스토어 인스턴스는 하나만 있어야 한다

 

도메인 객체

자체 클래스(생성자 함수)를 사용하여 표현해야 한다. 

메서드를 가질 수 있다

import { makeAutoObservable, autorun, runInAction } from "mobx"
import uuid from "node-uuid"

export class TodoStore {
    authorStore
    transportLayer
    todos = []
    isLoading = true

    constructor(transportLayer, authorStore) {
        makeAutoObservable(this)
        this.authorStore = authorStore // 작성자를 확인할 수 있는 스토어
        this.transportLayer = transportLayer // 서버 요청을 할 수 있는 것
        this.transportLayer.onReceiveTodoUpdate(updatedTodo =>
            this.updateTodoFromServer(updatedTodo)
        )
        this.loadTodos()
    }

    // 서버에서 모든 todo를 가져옵니다.
    loadTodos() {
        this.isLoading = true
        this.transportLayer.fetchTodos().then(fetchedTodos => {
            runInAction(() => {
                fetchedTodos.forEach(json => this.updateTodoFromServer(json))
                this.isLoading = false
            })
        })
    }

    // 서버의 정보로 Todo를 업데이트합니다. Todo가 한 번만 존재함을 보장합니다.
    // 새로운 Todo를 생성하거나 기존 Todo를 업데이트하거나 
    // 서버에서 삭제된 Todo를 제거할 수 있습니다.
    updateTodoFromServer(json) {
        let todo = this.todos.find(todo => todo.id === json.id)
        if (!todo) {
            todo = new Todo(this, json.id)
            this.todos.push(todo)
        }
        if (json.isDeleted) {
            this.removeTodo(todo)
        } else {
            todo.updateFromJson(json)
        }
    }

    // 클라이언트와 서버에 새로운 Todo를 생성합니다.
    createTodo() {
        const todo = new Todo(this)
        this.todos.push(todo)
        return todo
    }

    // Todo가 어떻게든 삭제되었을 때 클라이언트 메모리에서 삭제합니다.
    removeTodo(todo) {
        this.todos.splice(this.todos.indexOf(todo), 1)
        todo.dispose()
    }
}

// 도메인 객체 Todo.
export class Todo {
    id = null // Todo의 고유 id, 변경할 수 없습니다.
    completed = false
    task = ""
    author = null // authorStore에서 가져온 Author 객체에 대한 참조
    store = null
    autoSave = true // Todo의 변경사항을 서버에 제출하기 위한 표시
    saveHandler = null // todo를 자동저장하는 부수효과의 Disposer(dispose).

    constructor(store, id = uuid.v4()) {
        makeAutoObservable(this, {
            id: false,
            store: false,
            autoSave: false,
            saveHandler: false,
            dispose: false
        })
        this.store = store
        this.id = id

        this.saveHandler = reaction(
            () => this.asJson, // JSON에서 사용되는 모든 것을 관찰합니다.
            json => {
                // autoSave가 true이면 JSON을 서버로 보냅니다.
                if (this.autoSave) {
                    this.store.transportLayer.saveTodo(json)
                }
            }
        )
    }

    // 클라이언트와 서버에서 해당 Todo를 제거합니다.
    delete() {
        this.store.transportLayer.deleteTodo(this.id)
        this.store.removeTodo(this)
    }

    get asJson() {
        return {
            id: this.id,
            completed: this.completed,
            task: this.task,
            authorId: this.author ? this.author.id : null
        }
    }

    // 서버의 정보로 Todo를 업데이트합니다.
    updateFromJson(json) {
        this.autoSave = false // 변경 사항을 서버로 다시 보내는 것을 방지합니다.
        this.completed = json.completed
        this.task = json.task
        this.author = this.store.authorStore.resolveAuthor(json.authorId)
        this.autoSave = true
    }

    // observer를 청소합니다.
    dispose() {
        this.saveHandler()
    }
}

UI 스토어 

  • 세션 정보
  • 애플리케이션이 로드된 정도에 대한 정보
  • 백엔드에 저장되지 않을 정보
  • UI에 전체적으로 영향을 미치는 정보
    • Window dimensions
    • 접근성 정보(Accessibility information)
    • 현재 사용 중인 언어(Current language)
    • 현재 활성 중인 테마(Currently active theme)
  • 관련 없는 여러 컴포넌트에 영향을 미치는 유저 인터페이스 state
    • 현재 선택 항목(Current selection)
    • 툴바 가시성(Visibility of toolbars)
    • 위저드(wizard) state
    • 전역 오버레이(global overlay) state
import { makeAutoObservable, observable, computed, asStructure } from "mobx"

export class UiState {
    language = "en_US"
    pendingRequestCount = 0

    // .struct는 dimension 객체가 deepEqual 방식으로 
    // 변경되지 않는 한 observer가 신호를 받지 않도록 합니다.
    
    windowDimensions = {
        width: window.innerWidth,
        height: window.innerHeight
    }

    constructor() {
        makeAutoObservable(this, { windowDimensions: observable.struct })
        window.onresize = () => {
            this.windowDimensions = getWindowDimensions()
        }
    }

    get appIsInSync() {
        return this.pendingRequestCount === 0
    }
}

스토어 결합하기

모든 저장소를 인스턴스화하고 참조를 공유하는 RootStore 만들기

 

관련글 더보기

댓글 영역