bansa 0.0.25 → 0.0.26
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 +112 -85
- package/dist/atom.d.mts +10 -10
- package/dist/atom.d.mts.map +1 -1
- package/dist/atom.mjs +126 -85
- package/dist/atom.mjs.map +1 -1
- package/dist/browser/index.d.ts +10 -10
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/react.d.mts +25 -5
- package/dist/react.d.mts.map +1 -1
- package/dist/react.mjs +44 -18
- package/dist/react.mjs.map +1 -1
- package/dist/utils.mjs +5 -3
- package/dist/utils.mjs.map +1 -1
- package/package.json +7 -5
- package/tsconfig.json +2 -2
- package/.oxfmtrc.jsonc +0 -3
- package/README.ko.md +0 -490
- package/oxlint.config.ts +0 -17
- package/tsconfig.app.json +0 -39
- package/tsconfig.lib.json +0 -35
package/README.md
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
# Bansa
|
|
2
2
|
|
|
3
|
-
English
|
|
3
|
+
[English](https://github.com/cgiosy/bansa/blob/main/README.md) | 한국어
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 소개
|
|
6
6
|
|
|
7
|
-
Bansa
|
|
7
|
+
Bansa는 파생 상태, 비동기 값, 의존성과 구독, 생명 주기, 사이드 이펙트를 쉽게 관리할 수 있는 라이브러리입니다. [Jotai](https://jotai.org/)와 유사하게, atom을 사용한 상향식 접근 방식을 따릅니다.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
어떤 라이브러리나 프레임워크도 사용하지 않는 순수 JavaScript 환경은 물론이고, React, Vue, Svelte 등에서도 사용 가능한 프레임워크 독립적 라이브러리입니다.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## 개념
|
|
12
12
|
|
|
13
|
-
###
|
|
13
|
+
### 상태
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
`$` 함수로 상태를 만들 수 있습니다. 상태는 두 종류가 있습니다.
|
|
16
16
|
|
|
17
|
-
####
|
|
17
|
+
#### 원시 상태
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
가장 기본적인 상태 단위로, 값이 정적이며, `.set` 메서드를 통해 임의의 값으로 업데이트할 수 있습니다.
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
일반 값(숫자/문자열/객체 등)을 `$` 함수에 전달하여 생성합니다.
|
|
22
22
|
|
|
23
23
|
```javascript
|
|
24
24
|
import { $ } from "bansa";
|
|
@@ -28,11 +28,11 @@ const $count = $(42);
|
|
|
28
28
|
const $user = $({ name: "John Doe", age: 30 });
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
####
|
|
31
|
+
#### 파생 상태
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
값을 함수로 계산하며, 생명 주기를 가지는 상태입니다. 값을 직접 업데이트할 수 없으며, 의존 중인 상태의 값이 바뀔 때에만 재실행될 수 있습니다. 활성화된 상태가 아니라면, 즉 해당 상태를 구독 중인 곳이 존재하지 않는다면 함수는 실행되지 않으며, 의존성 또한 없는 것으로 취급됩니다.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
`$`에 함수를 전달하여 생성합니다. 전달하는 함수의 인자로는 다른 상태의 값을 읽을 수 있는 `get` 함수와 상태의 수명을 나타내는 `{ signal }`이 주어집니다. `signal`에 대해선 다른 파트에서 더 자세히 다룹니다.
|
|
36
36
|
|
|
37
37
|
```javascript
|
|
38
38
|
const $countDouble = $((get) => get($count) * 2);
|
|
@@ -48,35 +48,21 @@ const $signalExample = $((_, { signal }) => {
|
|
|
48
48
|
});
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
이 때 `$countDouble`은 `$count`에 의존하므로, `$count`의 값이 변경되면 `$countDouble`의 값이 자동으로 다시 계산될 수 있습니다.
|
|
52
52
|
|
|
53
|
-
`$userMessage
|
|
53
|
+
`$userMessage`는 `$count`에 의존하며, `$count`의 값이 `50` 미만이 아니라면 추가로 `$user`에도 의존합니다. 즉, `$count`가 `50` 미만이라면 `$user`의 값이 바뀌더라도 다시 계산되지 않습니다.
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
이 설명은 상태가 활성화된 상황일 때를 설명한 것이며, 후술할 `.subscribe()` 또는 `.watch()` 메서드로 구독되기 전까지는 어느 쪽이든 실행되지 않습니다.
|
|
56
56
|
|
|
57
|
-
#####
|
|
57
|
+
##### 활성 상태로 유지하기
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
`$`의 두 번째 파라미터로 옵션을 전달할 수 있습니다. 옵션 객체에서 `persist`가 `true`로 설정되어 있다면, 해당 객체는 한 번 활성화되면 다시 비활성화되지 않습니다. 활성 상태를 유지하려고 무의미한 구독을 추가하는 대신 사용할 수 있습니다.
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
값이 거의 바뀌지 않고, 언제든 다시 쓸 수 있게 준비해둬야 하는 경우 유용합니다. 대표적으론 정적인 에셋을 `fetch` 또는 `import`하는 상황이 있습니다.
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
type AtomState<Value> =
|
|
65
|
-
| { promise: undefined; error: undefined; value: Value } // Success
|
|
66
|
-
| { promise: undefined; error: any; value?: Value } // Error
|
|
67
|
-
| { promise: PromiseLike<Value>; error: any; value?: Value } // Loading
|
|
68
|
-
| { promise: typeof inactive; error: undefined; value?: Value }; // Inactive
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
##### Keeping a state active
|
|
72
|
-
|
|
73
|
-
You can pass an options object as the second parameter to `$`. If `persist` is set to `true` in the options object, the state will not be deactivated once it becomes active. This can be used instead of adding a meaningless subscription just to keep the state active.
|
|
74
|
-
|
|
75
|
-
This is useful for values that rarely change but need to be ready for use at any time. A typical example is fetching or importing static assets.
|
|
63
|
+
### 상태 직접 읽기
|
|
76
64
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
You can read the current value of a state using the `atom.get()` method or `atom.state` property.
|
|
65
|
+
`atom.get()` 메서드나 `atom.state`을 통해 상태의 현재 값을 읽을 수 있습니다.
|
|
80
66
|
|
|
81
67
|
```javascript
|
|
82
68
|
console.log($count.get()); // 42
|
|
@@ -85,13 +71,27 @@ console.log($countDouble.get()); // 84
|
|
|
85
71
|
console.log($countDouble.state); // { promise: undefined, error: undefined, value: 84 }
|
|
86
72
|
```
|
|
87
73
|
|
|
88
|
-
|
|
74
|
+
파생 상태는 `.get()` 했을 때 throw 될 수 있습니다. 비동기 로딩 중일 땐 해당 `Promise`를 throw하며, 오류 상태일 때는 해당 오류를 throw합니다. 값이 성공적으로 계산된 상황 위주로 처리하고, 예외 상황은 전부 `catch` 블록 등으로 밀어넣고 싶은 상황에서 유용합니다.
|
|
75
|
+
|
|
76
|
+
`.get()` 메서드는 상태가 비활성화된 경우, 매우 잠시 동안 해당 상태를 살아 있는 상태로 전환합니다. 당연히 해당 상태와 모든 의존성이 새로 실행됩니다. 매우 잠시 동안은 적어도 현재 마이크로태스크가 끝나기까지를 의미합니다. 즉, 동기적으로 연속해서 `.get()`을 호출하더라도 매번 모든 것이 다시 실행되지는 않습니다.
|
|
77
|
+
|
|
78
|
+
비동기 상태를 다룰 때에는 거의 사용할 일이 없습니다. 대신 `.subscribe`, 또는 `.watch`와 `.state`을 사용하세요.
|
|
89
79
|
|
|
90
|
-
|
|
80
|
+
##### `.state`
|
|
91
81
|
|
|
92
|
-
|
|
82
|
+
`state`은 상태의 현재 상태를 나타내는 읽기 전용 객체입니다. 비동기 상태거나 오류가 예상되는, 값이 준비되지 않은 상황을 처리(placeholder를 보여주는 등)해야 하는 상황에서 유용합니다. `value`는 마지막으로 성공했을 때의 값을 가집니다. `error`와 `promise`는 현재 로딩 중이거나 에러가 발생한 경우 해당 값을 가집니다. `active`는 활성 상태 여부를 나타냅니다. 정확한 타입은 다음과 같습니다:
|
|
93
83
|
|
|
94
|
-
|
|
84
|
+
```typescript
|
|
85
|
+
type AtomState<Value> =
|
|
86
|
+
| { active: false; error: undefined; promise: undefined; value?: Value } // 비활성
|
|
87
|
+
| { active: true; error: undefined; promise: undefined; value: Value } // 성공
|
|
88
|
+
| { active: true; error: any; promise: undefined; value?: Value } // 에러
|
|
89
|
+
| { active: true; error: any; promise: PromiseLike<Value>; value?: Value }; // 로딩
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 상태 업데이트
|
|
93
|
+
|
|
94
|
+
`.set(updater)` 메서드를 통해 원시 상태의 값을 업데이트할 수 있으며, `updater`가 일반 값이라면 해당 값으로 업데이트하고, 함수라면 상태의 '예비 값' `nextValue`에 대해 `updater(nextValue)`로 업데이트합니다.
|
|
95
95
|
|
|
96
96
|
```javascript
|
|
97
97
|
console.log($count.get()); // 42
|
|
@@ -105,9 +105,9 @@ $count.set(increment);
|
|
|
105
105
|
console.log($count.get()); // 101
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
모든 업데이트는 마이크로태스크를 단위로 배치 처리됩니다. 즉, 동기적으로 발생하는 여러 업데이트는 한 번에 처리되며, 특히 하나의 상태가 여러 번 업데이트됐을 경우 마지막 값 한 번만 업데이트한 것으로 취급됩니다.
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
`updater`가 함수라면, 마지막으로 들어온 '예비 값' `nextValue`에 접근할 수 있습니다. 따라서, 다음과 같이 동기적으로 여러 번의 `.set`을 호출했을 때 `$count`는 `3`만큼 증가하게 됩니다. 단, 업데이트는 여전히 한 번만 됩니다.
|
|
111
111
|
|
|
112
112
|
```javascript
|
|
113
113
|
$count.set(increment);
|
|
@@ -115,17 +115,17 @@ $count.set(increment);
|
|
|
115
115
|
$count.set(increment);
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
반드시 현재 값을 기준으로 업데이트해야 한다면, `$count.set($count.state.value + 1)` 와 같이 `.get()` 또는 `.state`을 사용할 수 있습니다.
|
|
119
119
|
|
|
120
|
-
###
|
|
120
|
+
### 상태 구독
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
`.subscribe(listener)` 또는 `.watch(listener)` 메서드로 업데이트를 감지할 수 있습니다. 각 메서드는 구독 중단 함수를 반환합니다.
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
구독 시 해당 상태가 비활성화된 상태였다면 업데이트가 예약되며, 업데이트 시 해당 상태와 의존성까지 모두 활성화됩니다. 구독 해제 시 해당 상태를 구독하는 곳이 더이상 없다면 비활성화가 예약되며, 의존성도 비활성화 대상인지 확인됩니다.
|
|
125
125
|
|
|
126
|
-
`.subscribe
|
|
126
|
+
`.subscribe`는 상태가 성공적으로 업데이트되었을 때 주어진 함수를 호출합니다. 이미 업데이트가 성공적으로 된 상태일 경우 구독 시 해당 값으로 한 번 호출합니다. 호출 시 첫 번째 인자는 해당 상태의 값, 두 번째 인자는 `{ signal }`이 주어지며, `signal`은 상태의 수명과 연동됩니다.
|
|
127
127
|
|
|
128
|
-
`.watch
|
|
128
|
+
`.watch`는 해당 상태가 변화할 때 주어진 함수를 호출합니다. 오류나 비동기 상태를 추가적으로 처리하려는 경우 쓸 수 있습니다.
|
|
129
129
|
|
|
130
130
|
```javascript
|
|
131
131
|
const $count = $(0);
|
|
@@ -144,12 +144,12 @@ unsubscribe();
|
|
|
144
144
|
// value end 1
|
|
145
145
|
|
|
146
146
|
$count.set(2);
|
|
147
|
-
// (
|
|
147
|
+
// (출력 없음)
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
-
`.subscribe()
|
|
150
|
+
`.subscribe()`는 구독을 해제할 수 있는 함수를 반환합니다. 컴포넌트가 언마운트될 때 이 함수를 호출하여 메모리 누수를 방지하는 것이 중요합니다.
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
만약 여러 상태를 동시에 구독하고 싶다면, 상태를 하나 더 선언해야 합니다.
|
|
153
153
|
|
|
154
154
|
```javascript
|
|
155
155
|
const $merged = $((get) => ({
|
|
@@ -160,9 +160,9 @@ const $merged = $((get) => ({
|
|
|
160
160
|
$merged.subscribe(({ count, countDouble }) => console.log(`${count} * 2 = ${countDouble}`));
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
-
###
|
|
163
|
+
### 비동기 상태
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
함수에서 `Promise`가 반환된 파생 상태의 경우, 해당 상태를 `get`하거나 `subscribe`했을 때 자동으로 unwrap된 값을 쓸 수 있습니다. 로딩이나 실패했을 때를 다루고 싶다면 `watch`나 `state`을 사용할 수 있습니다.
|
|
166
166
|
|
|
167
167
|
```javascript
|
|
168
168
|
const $user = $(async (get) => {
|
|
@@ -184,11 +184,11 @@ faultyAtom.watch(() => {
|
|
|
184
184
|
});
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
-
###
|
|
187
|
+
### 상태의 수명 (`signal`)
|
|
188
188
|
|
|
189
|
-
|
|
189
|
+
파생 함수의 인자로 전달되는 `options.signal`은 `AbortSignal` 및 `Promise` (엄밀히는 thenable)처럼 사용 가능합니다. 상태가 업데이트됐거나, 상태가 비활성화되는 등 상태의 수명이 변했을 때 `abort` 및 `resolve`됩니다.
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
`AbortSignal`처럼 `fetch`나 `addEventListener`같은 기존 웹 API에 전달하여 취소나 구독 중단 등에 사용할 수 있으며, `Promise`처럼 `signal.then`을 통해 자체 cleanup 함수를 쓸 수도 있습니다.
|
|
192
192
|
|
|
193
193
|
```javascript
|
|
194
194
|
const $user = $(async (get, { signal }) => {
|
|
@@ -201,9 +201,9 @@ const $user = $(async (get, { signal }) => {
|
|
|
201
201
|
});
|
|
202
202
|
```
|
|
203
203
|
|
|
204
|
-
###
|
|
204
|
+
### 커스텀 업데이트 조건(동등성 확인)
|
|
205
205
|
|
|
206
|
-
|
|
206
|
+
기본적으로 `Object.is`로 동등성을 체크하므로 객체나 배열의 경우 참조가 다르면 내용이 같더라도 업데이트가 발생할 수 있으며, 추가로 동등성을 확인하기 위해 상태 선언 시 `equals`를 옵션으로 줄 수 있습니다. 이 경우 `Object.is`로 같은지 확인하고, 다르다면 `equals` 함수로 다시 확인합니다. 둘 중 하나라도 참을 반환하는 경우 값 변경은 무시됩니다.
|
|
207
207
|
|
|
208
208
|
```javascript
|
|
209
209
|
const $user = $({ id: 1, name: "Alice" }, { equals: (next, prev) => next.id === prev.id });
|
|
@@ -215,23 +215,23 @@ userAtom.set({ id: 1, name: "Bob" });
|
|
|
215
215
|
userAtom.set({ id: 2, name: "Alice" });
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
-
|
|
218
|
+
위 예제에서 첫 번째 업데이트는 `id`가 같으므로 무시됩니다. 두 번째 업데이트는 `id`가 다르므로 `$user`를 업데이트하지만, `name`이 같으므로 `$user2`는 업데이트되지 않습니다.
|
|
219
219
|
|
|
220
|
-
###
|
|
220
|
+
### 여러 상태 병합하기
|
|
221
221
|
|
|
222
|
-
|
|
222
|
+
`$$`로 여러 상태를 병합한 새로운 상태를 만들 수 있습니다. `$`와 사용법은 같지만, `$`의 `get` 함수는 `Promise`나 에러를 만났을 때 즉시 throw하는 반면, `$$`의 `get` 함수는 특별한 객체를 반환하여 최소한의 재실행으로 최대한의 의존성을 추적합니다. 또한, 배열이나 객체를 반환 시 한 단계 깊은 비교를 수행합니다.
|
|
223
223
|
|
|
224
|
-
|
|
224
|
+
다음 코드는 `$`로 상태를 병합하면 5초가 걸리는 반면에, `$$`로 상태를 병합하면 단 1초만이 걸립니다.
|
|
225
225
|
|
|
226
226
|
```javascript
|
|
227
227
|
const timer = (time) => new Promise((resolve) => setTimeout(() => resolve(1), time));
|
|
228
|
-
const
|
|
229
|
-
const
|
|
228
|
+
const timerAtoms = [1, 2, 3, 4, 5].map(() => $(() => timer(1000)));
|
|
229
|
+
const $timers = $$((get) => timerAtoms.map(get));
|
|
230
230
|
console.time();
|
|
231
|
-
|
|
231
|
+
$timers.subscribe(() => console.timeEnd());
|
|
232
232
|
```
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
참고로 `$$`의 `get` 함수가 `Promise`나 에러를 만났을 때 throw 대신 반환하는 값은 다음 과정으로 만들어집니다:
|
|
235
235
|
|
|
236
236
|
```javascript
|
|
237
237
|
const o = () => o;
|
|
@@ -242,19 +242,46 @@ Object.setPrototypeOf(
|
|
|
242
242
|
);
|
|
243
243
|
```
|
|
244
244
|
|
|
245
|
-
|
|
245
|
+
이 코드의 `o`는 아무리 프로퍼티 접근 및 호출을 해도 같은 값을 반환합니다. 예를 들어, `o.a.b.c().d()().asdf()()()() === o`는 `true`입니다. 따라서, 셀렉터와 filter/map/reduce 등 간단한 메서드로 이뤄진 대부분의 상태 병합 함수에서 문제 없이 전체 코드를 실행할 수 있게 만듭니다. 하지만 만능은 아니므로 약간의 주의가 필요하며, 가급적 상태 병합에만 사용해야 합니다.
|
|
246
|
+
|
|
247
|
+
또한 상태 병합 시 특정 값들에서 부분씩만 가져와 새로운 객체를 만드는 경우가 흔하므로, `$$`로 병합한 상태의 의존성들이 너무 자주 재실행되지 않도록 `$`의 `equals`를 다음으로 설정합니다:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
const shallowEquals = (a: any, b: any): boolean => {
|
|
251
|
+
if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false;
|
|
252
|
+
const c = a.constructor;
|
|
253
|
+
if (c !== b.constructor) return false;
|
|
254
|
+
|
|
255
|
+
if (c === Array) {
|
|
256
|
+
let i = a.length;
|
|
257
|
+
if (i !== b.length) return false;
|
|
258
|
+
while ((i = (i - 1) | 0) >= 0) if (!Object.is(a[i], b[i])) return false;
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let n = 0;
|
|
263
|
+
for (const k in a) {
|
|
264
|
+
if (!(k in b && Object.is(a[k], b[k]))) return false;
|
|
265
|
+
n = (n + 1) | 0;
|
|
266
|
+
}
|
|
267
|
+
for (const _ in b) if ((n = (n - 1) | 0) < 0) return false;
|
|
268
|
+
return true;
|
|
269
|
+
};
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
정확히 한 단계만 더 비교하므로, `{ arr: [...] }`와 같은 객체의 경우 `arr`의 내용이 같더라도 참조가 다르다면 의존성이 재실행됩니다. 필요하다면 `arr` 부분은 다른 `$$`로 감싼 뒤 합치거나, 상태를 직접 `$`로 `equals`를 주고 만드세요.
|
|
246
273
|
|
|
247
|
-
##
|
|
274
|
+
## 상세
|
|
248
275
|
|
|
249
|
-
###
|
|
276
|
+
### 상태를 얼마나 쪼개는 게 좋나요?
|
|
250
277
|
|
|
251
|
-
|
|
278
|
+
코드의 가독성을 크게 해치지 않는 선에서 최대한 많이 쪼개세요. 또한 최대한 많은 로직을, 최대한 많은 단계의 상태로 감싸세요.
|
|
252
279
|
|
|
253
|
-
|
|
280
|
+
사실, `$`처럼 `get`으로 상태의 값을 가져오는 방식으로 `subscribe`를 만들지 않은 이유 또한 상태를 최대한 많이 쪼개고, `subscribe`는 '최종 상태'만을 다루도록 하기 위함입니다.
|
|
254
281
|
|
|
255
|
-
|
|
282
|
+
암묵적인 '중간 상태'들은 '숨겨져' 있게 되므로 본 라이브러리의 혜택을 상당수 잃고, 불필요한 재계산 및 중간 값 활용 불가능, 복잡해지는 의존성 파악, 코드의 반복, 사이드 이펙트의 멱등성 깨짐, 세세한 생명 주기 및 구독 관리 불가능 등의 문제를 겪을 수 있습니다.
|
|
256
283
|
|
|
257
|
-
|
|
284
|
+
예를 들어, 다음은 상태를 덜 쪼개서 '불필요한 재계산 및 중간 값 활용 불가능'이 발생하는 상황을 보여줍니다.
|
|
258
285
|
|
|
259
286
|
```javascript
|
|
260
287
|
const $userId = $(123);
|
|
@@ -268,7 +295,7 @@ const $pageData = $(async (get, { signal }) => {
|
|
|
268
295
|
});
|
|
269
296
|
```
|
|
270
297
|
|
|
271
|
-
|
|
298
|
+
이 코드는 간단하고 깔끔해 보이지만, `userId`와 `postId` 중 하나만 업데이트되어도 두 개의 요청이 다시 보내지며, `user`와 `post`의 레이턴시가 합산되고 (`Promise.all`로 해결 가능하지만 코드 복잡도가 상승합니다.), 관계 없는 사이드 이펙트가 함께 존재해 맥락이 섞이고, `user`나 `post`의 다른 값이 바뀌지 않더라도 `innerHTML`를 바꾸어 DOM이 완전히 갈아엎어지는 등, 여러 문제가 있습니다. 다음과 같이 쪼개야 합니다.
|
|
272
299
|
|
|
273
300
|
```javascript
|
|
274
301
|
const $userId = $(123);
|
|
@@ -292,17 +319,17 @@ $pageData.subscribe(({ userName, postAuthor }) => {
|
|
|
292
319
|
});
|
|
293
320
|
```
|
|
294
321
|
|
|
295
|
-
|
|
322
|
+
코드 줄 수가 약간 늘어났지만, 앞서 언급한 문제들이 해결되었습니다.
|
|
296
323
|
|
|
297
|
-
|
|
324
|
+
아마 너무 간단한 예시라 별로 와닿지 않을 수도 있지만, 현실에서는 아차 하는 순간 자신도 모르게 상태를 뒤섞기 쉬우며, 또 모든 것을 한 곳에서 처리하려는 욕망을 참기 어려운 경우도 종종 발생합니다.
|
|
298
325
|
|
|
299
|
-
|
|
326
|
+
항상 이를 신경쓰며 상태를 쪼개고, 감싸고, 단계를 나누는 것이 중요합니다.
|
|
300
327
|
|
|
301
|
-
###
|
|
328
|
+
### `onMount`/`onCleanup`은 어떻게 하나요?
|
|
302
329
|
|
|
303
|
-
|
|
330
|
+
상태의 값이 바뀔 때마다가 아니라, 상태가 활성화됐을 때와 비활성화됐을 때(상태를 구독한 곳이 생기기 시작했을 때와 더이상 아무 곳에서도 구독하고 있지 않을 때) 함수를 호출해야 하는 경우가 있습니다. 즉, `onMount`/`onCleanup` (또는 `onDestroy` 등)와 같은 기능이 필요합니다.
|
|
304
331
|
|
|
305
|
-
|
|
332
|
+
이는 두 가지 방법으로 해결할 수 있습니다. 하나는 상태 안에서 상태를 만들어 반환하는 것입니다:
|
|
306
333
|
|
|
307
334
|
```javascript
|
|
308
335
|
const $shared = $((_, { signal }) => {
|
|
@@ -318,7 +345,7 @@ const $a = $((get) => {
|
|
|
318
345
|
});
|
|
319
346
|
```
|
|
320
347
|
|
|
321
|
-
|
|
348
|
+
`$state`의 수정이 `onMount` 및 `onCleanup` 내에서만 발생하는 경우(가령 외부 이벤트를 구독하는 상황), 가장 깔끔한 패턴입니다. 다음은 이를 응용하여 여러 곳에서 공유하는 연결을 다루는 예시입니다:
|
|
322
349
|
|
|
323
350
|
```javascript
|
|
324
351
|
const $wsConnection = $(() => {
|
|
@@ -357,7 +384,7 @@ const $alice = lastMessage("alice");
|
|
|
357
384
|
const $bob = lastMessage("bob");
|
|
358
385
|
```
|
|
359
386
|
|
|
360
|
-
|
|
387
|
+
만약 외부에서 상태를 수정할 수 있어야 한다면, 다음처럼 상태를 두 개 만들 수 있습니다:
|
|
361
388
|
|
|
362
389
|
```javascript
|
|
363
390
|
const $writer = $(0);
|
|
@@ -371,11 +398,11 @@ const $reader = $((get) => {
|
|
|
371
398
|
});
|
|
372
399
|
```
|
|
373
400
|
|
|
374
|
-
|
|
401
|
+
이제 읽기는 `$reader`로, 쓰기는 `$writer`로 하면 됩니다. `$shared`는 `$writer`에 의존하지 않으므로, `$writer`가 수정되더라도 `$shared`는 업데이트되지 않습니다.
|
|
375
402
|
|
|
376
|
-
##
|
|
403
|
+
## 예제
|
|
377
404
|
|
|
378
|
-
####
|
|
405
|
+
#### 디바운스-스로틀링
|
|
379
406
|
|
|
380
407
|
```javascript
|
|
381
408
|
const delayedState = (initial, minDelay, maxDelay) => {
|
|
@@ -414,7 +441,7 @@ inputElm.addEventListener("input", (e) => updateInput(e.currentTarget.value));
|
|
|
414
441
|
$inputValue.subscribe(console.log);
|
|
415
442
|
```
|
|
416
443
|
|
|
417
|
-
###
|
|
444
|
+
### 스크롤 방향 감지
|
|
418
445
|
|
|
419
446
|
```javascript
|
|
420
447
|
const $windowScroll = $((_, { signal }) => {
|
package/dist/atom.d.mts
CHANGED
|
@@ -16,23 +16,27 @@ type AtomSubscribe<Value> = (value: Value, options: AtomSubscriberOptions) => vo
|
|
|
16
16
|
type AtomInit<Value> = Value | AtomGetter<Value>;
|
|
17
17
|
type AtomUpdater<Value> = Value | AtomReducer<Value>;
|
|
18
18
|
type AtomInactiveState<Value> = {
|
|
19
|
-
|
|
19
|
+
active: false;
|
|
20
20
|
error: any;
|
|
21
|
+
promise: undefined;
|
|
21
22
|
value?: Value;
|
|
22
23
|
};
|
|
23
24
|
type AtomPromiseState<Value> = {
|
|
24
|
-
|
|
25
|
+
active: true;
|
|
25
26
|
error: any;
|
|
27
|
+
promise: PromiseLike<Value>;
|
|
26
28
|
value?: Value;
|
|
27
29
|
};
|
|
28
30
|
type AtomSuccessState<Value> = {
|
|
29
|
-
|
|
31
|
+
active: true;
|
|
30
32
|
error: undefined;
|
|
33
|
+
promise: undefined;
|
|
31
34
|
value: Value;
|
|
32
35
|
};
|
|
33
36
|
type AtomErrorState<Value> = {
|
|
34
|
-
|
|
37
|
+
active: true;
|
|
35
38
|
error: any;
|
|
39
|
+
promise: undefined;
|
|
36
40
|
value?: Value;
|
|
37
41
|
};
|
|
38
42
|
type AtomState<Value> = AtomInactiveState<Value> | AtomPromiseState<Value> | AtomSuccessState<Value> | AtomErrorState<Value>;
|
|
@@ -47,10 +51,7 @@ type AtomGetOptions = {
|
|
|
47
51
|
type ThenableSignal = AbortSignal & {
|
|
48
52
|
then: (f: () => void) => void;
|
|
49
53
|
};
|
|
50
|
-
type GetAtom =
|
|
51
|
-
<Value>(anotherAtom: Atom<Value>, unwrap?: true): Value;
|
|
52
|
-
<Value>(anotherAtom: Atom<Value>, unwrap: false): AtomState<Value>;
|
|
53
|
-
};
|
|
54
|
+
type GetAtom = <Value>(anotherAtom: Atom<Value>) => Value;
|
|
54
55
|
type CreateAtom = {
|
|
55
56
|
<Value>(init: AtomGetter<Value>, options?: AtomOptions<Value>): DerivedAtom<Value>;
|
|
56
57
|
<Value>(init: Value, options?: AtomOptions<Value>): PrimitiveAtom<Value>;
|
|
@@ -72,12 +73,11 @@ type AtomScope = {
|
|
|
72
73
|
};
|
|
73
74
|
type SetLike<Key> = Key[] | Set<Key> | (Key extends object ? WeakSet<Key> : never);
|
|
74
75
|
type MapLike<Key, Value> = Map<Key, Value> | (Key extends object ? WeakMap<Key, Value> : never) | (Key extends string | number | symbol ? Record<Key, Value> : never);
|
|
75
|
-
declare const inactive: Promise<never>;
|
|
76
76
|
declare const $: CreateAtom;
|
|
77
77
|
declare const isAtom: (x: unknown) => x is Atom<unknown>;
|
|
78
78
|
declare const isPrimitiveAtom: (x: unknown) => x is PrimitiveAtom<unknown>;
|
|
79
79
|
type AtomValuePair<Value> = [Atom<Value>, Value | PrimitiveAtom<Value>] | [DerivedAtom<Value>, Value | Atom<Value>];
|
|
80
80
|
declare const createScope: <T extends AtomValuePair<unknown>[]>(parentScope?: AtomScope | null, atomValuePairs?: T) => AtomScope;
|
|
81
81
|
//#endregion
|
|
82
|
-
export { $, Atom, AtomEquals, AtomErrorState, AtomGetOptions, AtomGetter, AtomInactiveState, AtomInit, AtomOptions, AtomPromiseState, AtomReducer, AtomScope, AtomState, AtomSubscribe, AtomSubscriberOptions, AtomSuccessState, AtomUpdater, AtomValuePair, AtomWatcher, CommonAtom, DerivedAtom, GetAtom, MapLike, PrimitiveAtom, SetLike, ThenableSignal, createScope,
|
|
82
|
+
export { $, Atom, AtomEquals, AtomErrorState, AtomGetOptions, AtomGetter, AtomInactiveState, AtomInit, AtomOptions, AtomPromiseState, AtomReducer, AtomScope, AtomState, AtomSubscribe, AtomSubscriberOptions, AtomSuccessState, AtomUpdater, AtomValuePair, AtomWatcher, CommonAtom, DerivedAtom, GetAtom, MapLike, PrimitiveAtom, SetLike, ThenableSignal, createScope, isAtom, isPrimitiveAtom };
|
|
83
83
|
//# sourceMappingURL=atom.d.mts.map
|
package/dist/atom.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"atom.d.mts","names":[],"sources":["../src/atom.ts"],"mappings":";KAAY,IAAA,UAAc,aAAA,CAAc,KAAA,IAAS,WAAA,CAAY,KAAA;AAAA,KACjD,UAAA;EAAA,SACD,GAAA,QAAW,KAAA;EAAA,SACX,KAAA,GAAQ,OAAA,EAAS,WAAA;EAAA,SACjB,SAAA,GAAY,UAAA,EAAY,aAAA,CAAc,KAAA;EAAA,SACtC,KAAA,EAAO,SAAA,CAAU,KAAA;AAAA;AAAA,KAEhB,aAAA,UAAuB,UAAA,CAAW,KAAA;EAAA,SACnC,GAAA,GAAM,KAAA,EAAO,WAAA,CAAY,KAAA;EAAA,SACzB,KAAA,EAAO,gBAAA,CAAiB,KAAA;AAAA;AAAA,KAEvB,WAAA,UAAqB,UAAA,CAAW,KAAA;AAAA,KAEhC,WAAA;AAAA,KACA,aAAA,WAAwB,KAAA,EAAO,KAAA,EAAO,OAAA,EAAS,qBAAA;AAAA,KAC/C,QAAA,UAAkB,KAAA,GAAQ,UAAA,CAAW,KAAA;AAAA,KACrC,WAAA,UAAqB,KAAA,GAAQ,WAAA,CAAY,KAAA;AAAA,KAEzC,iBAAA;EACV,
|
|
1
|
+
{"version":3,"file":"atom.d.mts","names":[],"sources":["../src/atom.ts"],"mappings":"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"}
|