@observable 데코레이터로 지정한 state는 관찰 대상으로 지정되고, 그 State는 값이 변경도리 때마다 렌더링 된다.
observable한 객체를 만들기 위해서는 makeObservable 함수를 사용하며, 각 프로퍼티마다 적절한 annotations를 지정한다.
Observable : '추적 가능한' 상태.
Action : State를 수정하는 메서드
Computed : state의 변화를 통해 계산되는 값.
Derivation : state를 통해서 자동으로 계산될 수 있는 모든 값 (Computed Value)를 포함
Reaction : state를 통해서 자동으로 수행되는 Task. 가령 특정 컴포넌트를 렌더링 하는 등을 의미한다. Derivation이 값을 만든다면, Reaction은 활동ㅇ르 수행한다.
즉 Action으로 Observable State를 변화시키면, 이를 통해 Derivation이 변화하거나 Reaction이 일어난다.
리액트 환경을 셋팅한 상태에서
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(target, annotations?, options?)
mobx로부터 makeObservable을 import한다.
해당 함수를 클래스의 constructor 내부에서 실행한다.
makeObservable의 두번째 인자로 annotations를 입력하게 된다.
해당 annotations를 멤버들(즉, getter, getter, 상태, 메서드)이 mobx에서 하는 역할을 지정하게 된다.
makeObservable 대신 makeAutoObservable을 입력하면 이 작업을 대신 해준다.
프로퍼티 -> observable
getter -> computed
setter -> action
함수 -> autoAction
제너레이터 -> flow
단순히 읽는 용도로 사용하기 위해서는 false로 주석을 단다.
autorun(effect: (reaction) => void, options?)
함수는 내용이 변경될 때마다 실행되어야 하는 하나의 함수를 인자로 받는다. observable
혹은 computed
가 변화할 때마다 실행된다.
리액트에서 mobx를 사용하기 위해서는 mobx-react-lite를 설치하면 된다. mobx-react의 경량 버전으로, 리액트의 함수 컴포넌트에 최적화되어 있다.
yarn add mobx mobx-react-lite
mobx-react-lite의 작동 원리는 함수 컴포넌트 전체를 autorun으로 감싸는 것이다. mobx-react-lite의 'observer' 함수 내부적으로 autorun이 작동한다. 그 밖의 내부 동작은 다음 벨로그 글을 참조하자
해당 블로그 글을 참조했다.
먼저 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()
makeAutoObservable(target, annotations?, options?)
을 통해 객체를 observable로 만든다. 이때 세번째 인자로 옵션을 넘길 수 있는데, autoBind 옵션을 주었다. '메서드'는 자바스크립트에 this 바인딩 되지 않는다. 이를 자동으로 바인딩해주는 옵션이 autoBind이다. 혹은 화살표 함수를 사용할 수 있다.this.todoData = []
와 같이 앞서 선언한 변수에 초기값을 넣어준다.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>
);
});