💻 오늘의 코드 팁 — 🛠️ 처음부터 만드는 State Machine — 복잡한 상태 전환을 예측 가능하게 관리하기
문제
버튼 하나에 `isLoading`, `isError`, `isSuccess`, `isIdle` 플래그 4개가 동시에 존재하고, 불가능한 조합(`isLoading && isError`)이 런타임에 발생합니다. 불가능한 상태를 아예 만들 수 없게 하려면?
---
해결: Finite State Machine을 직접 만들기
```typescript
// state-machine.ts — 복사해서 바로 실행 가능
type Listener
interface MachineConfig
initial: S;
transitions: Record
}
function createMachine
config: MachineConfig
) {
let current: S = config.initial;
const listeners = new Set
return {
get state() {
return current;
},
send(event: E): S {
const next = config.transitions[current]?.[event];
if (!next) {
console.warn(`No transition: ${current} + ${event}`);
return current;
}
current = next;
listeners.forEach((fn) => fn(current));
return current;
},
canSend(event: E): boolean {
return !!config.transitions[current]?.[event];
},
subscribe(fn: Listener
listeners.add(fn);
return () => listeners.delete(fn);
},
};
}
// ── 사용 예: API 요청 흐름 ──
const fetchMachine = createMachine<
'idle' | 'loading' | 'success' | 'error',
'FETCH' | 'RESOLVE' | 'REJECT' | 'RETRY' | 'RESET'
>({
initial: 'idle',
transitions: {
idle: { FETCH: 'loading' },
loading: { RESOLVE: 'success', REJECT: 'error' },
success: { RESET: 'idle' },
error: { RETRY: 'loading', RESET: 'idle' },
},
});
// 상태 변화를 구독
const unsub = fetchMachine.subscribe((s) => console.log('→', s));
console.log(fetchMachine.state); // 'idle'
fetchMachine.send('FETCH'); // → loading
fetchMachine.send('FETCH'); // ⚠️ No transition (loading에서 FETCH 불가)
fetchMachine.send('REJECT'); // → error
fetchMachine.send('RETRY'); // → loading
fetchMachine.send('RESOLVE'); // → success
console.log(fetchMachine.canSend('FETCH')); // false (success에서 FETCH 불가)
fetchMachine.send('RESET'); // → idle
unsub();
```
실행:
```bash
npx tsx state-machine.ts
```
---
핵심 구조 (3단)
| 구성 요소 | 역할 |
|-----------|------|
| State (상태) | `idle`, `loading`, `success`, `error` — 현재 위치 |
| Event (이벤트) | `FETCH`, `RESOLVE` 등 — 전환 트리거 |
| Transition (전환 규칙) | `{ loading: { RESOLVE: 'success' } }` — 허용된 경로만 정의 |
왜 좋은가?
1. 불가능한 상태가 아예 존재하지 않음 — `loading`에서 `FETCH`를 보내도 무시됨
2. 상태 다이어그램 = 코드 — `transitions` 객체가 곧 명세서
3. TypeScript가 이벤트 오타를 잡아줌 — `send('FECTH')` → 컴파일 에러
4. 테스트가 쉬움 — 입력(이벤트)과 출력(상태)이 명확
React에서 쓰기
```tsx
function useMachine
config: MachineConfig
) {
const [machine] = useState(() => createMachine(config));
const [state, setState] = useState(machine.state);
useEffect(() => machine.subscribe(setState), [machine]);
return [state, machine.send, machine.canSend] as const;
}
// 컴포넌트에서
const [state, send, canSend] = useMachine({
initial: 'idle',
transitions: { /* ... */ },
});
```
---
더 알아보기
> 💡 boolean 플래그 3개 이상이 보이면, State Machine을 고려할 타이밍입니다.
Comments (0)
💬
No comments yet.
Be the first to comment!