๐ป ์ค๋์ ์ฝ๋ ํ โ ๐ ๏ธ ์ฒ์๋ถํฐ ๋ง๋๋ State Machine โ ๋ณต์กํ ์ํ ์ ํ์ ์์ธก ๊ฐ๋ฅํ๊ฒ ๊ด๋ฆฌํ๊ธฐ
๋ฌธ์
๋ฒํผ ํ๋์ `isLoading`, `isError`, `isSuccess`, `isIdle` ํ๋๊ทธ 4๊ฐ๊ฐ ๋์์ ์กด์ฌํ๊ณ , ๋ถ๊ฐ๋ฅํ ์กฐํฉ(`isLoading && isError`)์ด ๋ฐํ์์ ๋ฐ์ํฉ๋๋ค. ๋ถ๊ฐ๋ฅํ ์ํ๋ฅผ ์์ ๋ง๋ค ์ ์๊ฒ ํ๋ ค๋ฉด?
---
ํด๊ฒฐ: Finite State Machine์ ์ง์ ๋ง๋ค๊ธฐ
```typescript
// state-machine.ts โ ๋ณต์ฌํด์ ๋ฐ๋ก ์คํ ๊ฐ๋ฅ
type Listener = (state: S) => void;
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: { /* ... */ },
});
```
---
๋ ์์๋ณด๊ธฐ
ํ๋ก๋์
๊ธ ๊ตฌํ์ด ํ์ํ๋ค๋ฉด โ [XState v5](https://stately.ai/docs) (guards, actions, nested states ์ง์)
State Machine ํจํด ํด์ค โ [Statecharts.dev](https://statecharts.dev/)
Kent C. Dodds์ [Stop using isLoading booleans](https://kentcdodds.com/blog/stop-using-isloading-booleans) ๊ธ๋ ์ถ์ฒํฉ๋๋ค
> ๐ก boolean ํ๋๊ทธ 3๊ฐ ์ด์์ด ๋ณด์ด๋ฉด, State Machine์ ๊ณ ๋ คํ ํ์ด๋ฐ์
๋๋ค.