controlled-machine 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,13 +1,132 @@
|
|
|
1
1
|
# Controlled Machine
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **⚠️ ARCHIVED**: 이 프로젝트는 아카이브되었습니다. 아래의 여정과 통찰을 바탕으로 새로운 접근 방식의 프로젝트가 진행될 예정입니다.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 문제 정의
|
|
8
|
+
|
|
9
|
+
### XState와 useReducer가 잘 안 쓰이는 이유
|
|
10
|
+
|
|
11
|
+
XState와 useReducer는 상태 모델링에 강력한 도구다. 선언적으로 상태를 표현하고, 구현과 인터페이스를 분리할 수 있다. **그런데도 실제로는 잘 안 쓰인다.**
|
|
12
|
+
|
|
13
|
+
이유는 **외부 상태에 닫혀 있기 때문**이다.
|
|
14
|
+
|
|
15
|
+
- 외부 상황을 무시하고 자신의 내부 상태만 철저히 관리한다
|
|
16
|
+
- 오직 이벤트를 통해서만 소통하려 한다
|
|
17
|
+
|
|
18
|
+
React 컴포넌트 생태계에서 가장 강력한 힘을 가진 것은 **props로 전달되는 외부 상태**다. 하지만 reducer나 XState는 이 외부 상태를 다루기 까다롭다.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
┌─────────────────┐ ┌─────────────────┐
|
|
22
|
+
│ 외부 상태 │ ──?──▶ │ 내부 머신 │
|
|
23
|
+
│ (props) │ │ (uncontrolled) │
|
|
24
|
+
│ │ ◀──?─── │ │
|
|
25
|
+
└─────────────────┘ └─────────────────┘
|
|
26
|
+
|
|
27
|
+
1. 외부 → 내부: 어떤 이벤트를 보낼지?
|
|
28
|
+
2. 내부 → 외부: 어떻게 동기화할지?
|
|
29
|
+
3. 충돌 시: 누가 이길지?
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**도구의 한계가 개념의 가치까지 묻어버리는 셈이다.**
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 여정
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
목표: XState/useReducer를 "controlled"처럼 쓰고 싶다
|
|
40
|
+
│
|
|
41
|
+
▼
|
|
42
|
+
시도 1: 새로운 상태 머신 라이브러리 만들기 (controlled-machine)
|
|
43
|
+
│
|
|
44
|
+
├─ input/internal/computed 분리
|
|
45
|
+
├─ FSM 상태 추가
|
|
46
|
+
├─ Discriminated Union 지원
|
|
47
|
+
│
|
|
48
|
+
▼
|
|
49
|
+
문제: 결국 XState와 비슷해지면서 복잡해짐
|
|
50
|
+
│
|
|
51
|
+
▼
|
|
52
|
+
시도 2: 합성 가능한 구조로 분리
|
|
53
|
+
│
|
|
54
|
+
├─ Core는 단순하게
|
|
55
|
+
├─ 확장은 Wrapper로
|
|
56
|
+
│
|
|
57
|
+
▼
|
|
58
|
+
깨달음: 기존 도구(XState, useReducer)를 그대로 쓰고
|
|
59
|
+
"controlled wrapper"만 만들면 되잖아?
|
|
60
|
+
│
|
|
61
|
+
▼
|
|
62
|
+
최종 목표: controlled(xstate), controlled(useReducer)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 통찰
|
|
68
|
+
|
|
69
|
+
### 1. 문제를 복잡하게 풀지 마라
|
|
70
|
+
|
|
71
|
+
- input/internal/computed 분리 → 복잡한 조합 로직
|
|
72
|
+
- Managed mode/Computed mode → 두 가지 경로 관리
|
|
73
|
+
- state/discriminatedState → 타입 시스템 복잡화
|
|
74
|
+
|
|
75
|
+
**해결책**: 문제 자체를 없애거나, 단일 책임으로 분리
|
|
76
|
+
|
|
77
|
+
### 2. 기존 도구를 활용하라
|
|
78
|
+
|
|
79
|
+
XState와 useReducer는 이미 검증됨. 새로 만들 필요 없이 **동기화 문제만 해결**하면 됨.
|
|
80
|
+
|
|
81
|
+
### 3. 진짜 해결해야 할 문제
|
|
82
|
+
|
|
83
|
+
> 외부 상태(props)와 내부 상태(machine/reducer)를 어떻게 안전하게 동기화할 것인가?
|
|
84
|
+
|
|
85
|
+
이 하나의 문제만 잘 풀면 된다.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 새로운 방향
|
|
90
|
+
|
|
91
|
+
기존 상태 관리 도구를 그대로 사용하고, **동기화만 해결하는 wrapper**를 만든다:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { createMachine } from 'xstate'
|
|
95
|
+
import { controlled } from 'controlled-wrapper' // 새 프로젝트
|
|
96
|
+
|
|
97
|
+
const xstateMachine = createMachine({ ... })
|
|
98
|
+
const controlledMachine = controlled(xstateMachine, {
|
|
99
|
+
sync: { ... }, // 외부 → 내부 (이벤트 변환)
|
|
100
|
+
notify: { ... }, // 내부 → 외부 (콜백 호출)
|
|
101
|
+
conflict: { ... } // 충돌 해결
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
자세한 내용은 [`docs/`](./docs/) 폴더 참조:
|
|
106
|
+
- [PROBLEM.md](./docs/PROBLEM.md) - 문제 정의
|
|
107
|
+
- [JOURNEY.md](./docs/JOURNEY.md) - 여정
|
|
108
|
+
- [INTERFACE.md](./docs/INTERFACE.md) - 새 인터페이스 설계
|
|
109
|
+
- [REJECTED.md](./docs/REJECTED.md) - 거부된 접근 방식들
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
# 아래는 기존 문서 (참고용)
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Original: Controlled Machine (v0.4.x)
|
|
120
|
+
|
|
121
|
+
A controlled state machine with **internal state management**.
|
|
122
|
+
Machine owns **its own state**. Your component passes **external data**.
|
|
8
123
|
|
|
9
124
|
```bash
|
|
10
125
|
npm install controlled-machine
|
|
126
|
+
# or
|
|
127
|
+
yarn add controlled-machine
|
|
128
|
+
# or
|
|
129
|
+
pnpm add controlled-machine
|
|
11
130
|
```
|
|
12
131
|
|
|
13
132
|
---
|
|
@@ -435,6 +554,30 @@ createMachine<{
|
|
|
435
554
|
{ when: ['isEnabled', 'canIncrement', (ctx) => !ctx.isLoading], do: ... }
|
|
436
555
|
```
|
|
437
556
|
|
|
557
|
+
### Guard Utilities
|
|
558
|
+
|
|
559
|
+
Compose guards with `not`, `and`, `or`:
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
import { createMachine, not, and, or } from 'controlled-machine'
|
|
563
|
+
|
|
564
|
+
// not() - negate a guard
|
|
565
|
+
{ when: not('isDisabled'), do: 'handleClick' }
|
|
566
|
+
{ when: not((ctx) => ctx.loading), do: 'submit' }
|
|
567
|
+
|
|
568
|
+
// and() - all guards must pass
|
|
569
|
+
{ when: and(['hasValue', 'isValid']), do: 'submit' }
|
|
570
|
+
|
|
571
|
+
// or() - at least one guard must pass
|
|
572
|
+
{ when: or(['isAdmin', 'hasPermission']), do: 'delete' }
|
|
573
|
+
|
|
574
|
+
// Nested composition
|
|
575
|
+
{ when: not(or(['isLoading', 'isDisabled'])), do: 'handleClick' }
|
|
576
|
+
|
|
577
|
+
// Mixed named and inline guards
|
|
578
|
+
{ when: and(['hasValue', (ctx) => ctx.count > 0]), do: 'action' }
|
|
579
|
+
```
|
|
580
|
+
|
|
438
581
|
---
|
|
439
582
|
|
|
440
583
|
## Computed Values
|
|
@@ -695,7 +838,7 @@ createMachine<{
|
|
|
695
838
|
### Exports
|
|
696
839
|
|
|
697
840
|
```ts
|
|
698
|
-
import { createMachine, effect } from 'controlled-machine'
|
|
841
|
+
import { createMachine, effect, not, and, or } from 'controlled-machine'
|
|
699
842
|
import { useMachine } from 'controlled-machine/react'
|
|
700
843
|
import type {
|
|
701
844
|
MachineTypes,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "controlled-machine",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A controlled state machine
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "[ARCHIVED] A controlled state machine experiment - see README for insights and new direction",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"exports": {
|