새로 만드려고 하는 react/ts 프로젝트에 mobx로 상태관리를 해볼 예정이다.
아래는 mobx의 개념과 간단한 예시코드이다.
@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이 일어난다.
state가 변화하면 모든 derivation이 자동으로/동기식으로 업데이트됨 -> action이 일어난 직후에도 변경된 computed 값을 확인할 수 있음.
computed 값은 '느리게Lazy' 업데이트 됨. 즉, 사용되고 있는 컴퓨티드 값만 업데이트가 일어나며, 사용하지 않는 경우 가비지 컬렉팅.
computed는 state를 변경하지 않음.
react project 생성 : npm install vite --save-dev
mobx 설치 : npm install --save mobx
const Counter = observer(() => {
const store = useMemo(() => new CounterStore(), []);
return (
<div style={{ textAlign: "center" }}>
<h1>{store.number}</h1>
<h1>Computed : {store.computedNumber}</h1>
<button onClick={store.increase}>+1</button>
<button onClick={() => store.decrease()}>-1</button>
<button onClick={() => store.asyncIncrease()}>비동기 +1</button>
<p>{store.message}</p>
<button onClick={store.fetchData}>데이터 가져오기 *</button>
{store.loading && <p>로딩 중...</p>}
{store.data && <p>데이터: {store.data}</p>}
</div>
);
});
export default Counter;
class CounterStore {
public number: number = 0;
public message: string = "";
public data: string | null = null;
public loading: boolean = false;
constructor() {
/*
observable
observable로 설정한 변수는 상태(state)로서 추적됩니다. 해당 값이 변경될 때마다 이를 사용하는 컴포넌트나 UI가 자동으로 리렌더링 됩니다.
action
action은 observable 상태를 변경하는 함수입니다. 상태를 변경할 때, 해당 함수는 'action'으로 정의되어야 하며,
MobX는 이를 추적하여 상태 변경에 대한 최적화를 처리합니다.
computed
computed는 observable 값들을 기반으로 계산된 값을 반환하는 프로퍼티입니다.
계산된 값은 캐시되며, 종속된 observable 값이 변경되었을 때만 재계산됩니다. 계산된 값이 비즈니스 로직에서 중요한 경우 유용합니다.
flow
flow는 비동기 코드에서 상태를 동기적으로 처리할 수 있도록 도와주는 기능입니다.
비동기 작업을 `yield`를 사용해 순차적으로 처리하고, 상태를 관리할 수 있게 해줍니다.
`flow`는 특히 비동기 로직을 MobX의 상태 관리 흐름에 맞춰 통합하는 데 유용합니다.
makeAutoObservable(this);
makeAutoObservable: 자동으로 모든 속성과 메서드에 관찰 기능을 추가
- 기본적으로 모든 속성은 `observable`로, 메서드는 `action`으로 처리됩니다.
- 별도의 설정 없이 클래스의 속성과 메서드 모두에 대해 관찰 기능을 자동으로 활성화합니다.
makeObservable
makeObservable: 명시적으로 속성과 메서드를 설정하여 관찰 기능을 추가
- 속성과 메서드를 직접 설정해야 합니다.
- `observable`, `action`, `computed` 등 구체적으로 정의하여 사용합니다.
근데 왜 makeObservable ?
특정 속성만 observable로 만들고 싶을 때
메서드에 action을 지정하고 싶을 때
복잡한 설정이 필요할 때 ex ) computed, action, observable 등의 설정을 보다 세밀하게 적용해야 하는 경우
*/
makeObservable(this, {
number: observable,
message: observable,
loading: observable,
data: observable,
increase: action.bound,
decrease: action,
computedNumber: computed,
fetchData: flow.bound,
});
// reaction을 사용하면 number 값이 변경될 때마다 특정 작업을 수행하도록 설정할 수 있음.
reaction(() => this.number,
(number) => {
if (number % 2 === 0) {
this.message = "숫자가 짝수입니다!";
} else {
this.message = "숫자가 홀수입니다!";
}
}
);
// 특정 조건이 만족될 때까지 기다린 후, 그 조건이 만족되면 주어진 작업을 실행
when(() => this.number > 10, () => {
this.message = "숫자가 10이 넘었습니다.";
});
// reaction과 비슷하지만, 특정 값이나 상태를 반응하는 것이 아니라, 설정한 함수 내에서 모든 observable을 자동으로 추적합니다. 이 함수는 의존하는 observable 값이 변경될 때마다 자동으로 실행됩니다. 자동으로 실행되어야 하는 로직이 있을 때 사용
autorun(() => {
console.log("Current number:", this.number);
});
}
public increase () {
this.number++;
};
public decrease () {
this.number--;
};
public async asyncIncrease() {
setTimeout(() => {
// 상태 변경을 트랜잭션처럼 처리하여, 상태 업데이트가 여러 번 발생할 때 불필요한 리렌더링을 방지하고, 상태의 일관성을 유지하기 위해사용.
runInAction(() => {
this.number++;
});
}, 1000); // 1초 후 실행
}
get computedNumber() {
return this.number * 2;
}
public *fetchData() {
this.loading = true;
try {
// 비동기 작업 시뮬레이션 (예: API 호출)
yield new Promise<void>((resolve) => setTimeout(resolve, 1000));
this.data = "가져온 데이터";
} catch (error) {
console.error("데이터 가져오기 실패:", error);
} finally {
this.loading = false;
}
}
}
export default CounterStore;
장점
예측 가능하고 디버깅이 용이 (Redux DevTools 지원).
태 변경이 명시적이라 관리가 쉬움.
단점
<장점>
<단점>
Redux는 대규모 앱에 유리하고, 사용법이 정해져 있기 때문에 예측 가능성과 디버깅이 강점.
mobX는 작은 앱에 적합하고,사용법이 프리하기 때문에 간단하고 직관적인 코드를 원할 때 유리.