bansa 0.0.24 → 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 CHANGED
@@ -1,82 +1,68 @@
1
1
  # Bansa
2
2
 
3
- English | [한국어](https://github.com/cgiosy/bansa/blob/main/README.ko.md)
3
+ [English](https://github.com/cgiosy/bansa/blob/main/README.md) | 한국어
4
4
 
5
- ## Introduction
5
+ ## 소개
6
6
 
7
- Bansa is a library that makes it easy to manage derived state, asynchronous values, dependencies and subscriptions, lifecycles, and side effects. Similar to [Jotai](https://jotai.org/), it follows a bottom-up approach using atoms.
7
+ Bansa 파생 상태, 비동기 값, 의존성과 구독, 생명 주기, 사이드 이펙트를 쉽게 관리할 있는 라이브러리입니다. [Jotai](https://jotai.org/) 유사하게, atom을 사용한 상향식 접근 방식을 따릅니다.
8
8
 
9
- It is a framework-independent library that can be used in a pure JavaScript environment without any other libraries or frameworks, as well as with React, Vue, Svelte, and others.
9
+ 어떤 라이브러리나 프레임워크도 사용하지 않는 순수 JavaScript 환경은 물론이고, React, Vue, Svelte 등에서도 사용 가능한 프레임워크 독립적 라이브러리입니다.
10
10
 
11
- ## Concepts
11
+ ## 개념
12
12
 
13
- ### State
13
+ ### 상태
14
14
 
15
- You can create a state with the `$` function. There are two types of states.
15
+ `$` 함수로 상태를 만들 있습니다. 상태는 종류가 있습니다.
16
16
 
17
- #### Primitive State
17
+ #### 원시 상태
18
18
 
19
- The most basic unit of state. It can be updated to any value using the `.set` method.
19
+ 가장 기본적인 상태 단위로, 값이 정적이며, `.set` 메서드를 통해 임의의 값으로 업데이트할 있습니다.
20
20
 
21
- It is created by passing a normal value (number/string/object, etc.) to the `$` function.
21
+ 일반 (숫자/문자열/객체 ) `$` 함수에 전달하여 생성합니다.
22
22
 
23
23
  ```javascript
24
- import { $ } from 'bansa';
24
+ import { $ } from "bansa";
25
25
 
26
26
  const $count = $(42);
27
27
 
28
- const $user = $({ name: 'John Doe', age: 30 });
28
+ const $user = $({ name: "John Doe", age: 30 });
29
29
  ```
30
30
 
31
- #### Derived State
31
+ #### 파생 상태
32
32
 
33
- A state whose value is computed by a function and has a lifecycle. It cannot be updated directly; it can only be re-executed when the value of a state it depends on changes. If the state is not active (i.e., there are no subscribers), the function will not run, and it is treated as having no dependencies.
33
+ 값을 함수로 계산하며, 생명 주기를 가지는 상태입니다. 값을 직접 업데이트할 없으며, 의존 중인 상태의 값이 바뀔 때에만 재실행될 있습니다. 활성화된 상태가 아니라면, 해당 상태를 구독 중인 곳이 존재하지 않는다면 함수는 실행되지 않으며, 의존성 또한 없는 것으로 취급됩니다.
34
34
 
35
- It is created by passing a function to `$`. The arguments to this function are a `get` function, which can read the values of other states, and `{ signal }`, which represents the state's lifetime. The `signal` will be discussed in more detail in another section.
35
+ `$`에 함수를 전달하여 생성합니다. 전달하는 함수의 인자로는 다른 상태의 값을 읽을 있는 `get` 함수와 상태의 수명을 나타내는 `{ signal }`이 주어집니다. `signal`에 대해선 다른 파트에서 자세히 다룹니다.
36
36
 
37
37
  ```javascript
38
38
  const $countDouble = $((get) => get($count) * 2);
39
39
 
40
40
  const $userMessage = $((get) => {
41
- if (get($count) < 50) return 'no hello.';
42
- return `Hello, ${get($user).name}!`;
41
+ if (get($count) < 50) return "no hello.";
42
+ return `Hello, ${get($user).name}!`;
43
43
  });
44
44
 
45
45
  const $signalExample = $((_, { signal }) => {
46
- signal.then(() => console.log("$signalExample died"));
47
- return fetch(`/users/${get($count)}`, { signal }).then((res) => res.json());
46
+ signal.then(() => console.log("$signalExample died"));
47
+ return fetch(`/users/${get($count)}`, { signal }).then((res) => res.json());
48
48
  });
49
49
  ```
50
50
 
51
- Here, `$countDouble` depends on `$count`, so if the value of `$count` changes, the value of `$countDouble` can be automatically recalculated.
51
+ `$countDouble`은 `$count`에 의존하므로, `$count`의 값이 변경되면 `$countDouble`의 값이 자동으로 다시 계산될 수 있습니다.
52
52
 
53
- `$userMessage` depends on `$count`, and if the value of `$count` is not less than `50`, it also depends on `$user`. This means if `$count` is less than `50`, `$userMessage` will not be recalculated even if the value of `$user` changes.
53
+ `$userMessage`는 `$count`에 의존하며, `$count`의 값이 `50` 미만이 아니라면 추가로 `$user`에도 의존합니다. 즉, `$count`가 `50` 미만이라면 `$user`의 값이 바뀌더라도 다시 계산되지 않습니다.
54
54
 
55
- This explanation describes the behavior when the state is active. It will not execute in either case until it is subscribed to using the `.subscribe()` or `.watch()` methods, which will be discussed later.
55
+ 설명은 상태가 활성화된 상황일 때를 설명한 것이며, 후술할 `.subscribe()` 또는 `.watch()` 메서드로 구독되기 전까지는 어느 쪽이든 실행되지 않습니다.
56
56
 
57
- ##### Reading `state` (Preventing unwrap)
57
+ ##### 활성 상태로 유지하기
58
58
 
59
- The second parameter of `get` is an optional `unwrap` option. The default value is `true`, so it always returns the unwrapped value. If set to `false`, it returns the `state` of type `AtomState<Value>`.
59
+ `$`의 번째 파라미터로 옵션을 전달할 있습니다. 옵션 객체에서 `persist`가 `true`로 설정되어 있다면, 해당 객체는 활성화되면 다시 비활성화되지 않습니다. 활성 상태를 유지하려고 무의미한 구독을 추가하는 대신 사용할 있습니다.
60
60
 
61
- `state` is a read-only object representing the current status of the state. It is useful for handling situations where the value is not ready, such as with asynchronous states or expected errors (e.g., for showing a placeholder). `value` holds the last successfully resolved value. `promise` and `error` hold their respective values if the state is currently loading or has encountered an error. The exact type is as follows:
61
+ 값이 거의 바뀌지 않고, 언제든 다시 있게 준비해둬야 하는 경우 유용합니다. 대표적으론 정적인 에셋을 `fetch` 또는 `import`하는 상황이 있습니다.
62
62
 
63
- ```typescript
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
- ### Reading State Directly
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,14 +71,27 @@ console.log($countDouble.get()); // 84
85
71
  console.log($countDouble.state); // { promise: undefined, error: undefined, value: 84 }
86
72
  ```
87
73
 
88
- For derived states, `.get()` can throw. It throws the `Promise` during asynchronous loading and throws the error when in an error state. This is useful when you want to primarily handle the success case and push all exception handling into a `catch` block or similar.
74
+ 파생 상태는 `.get()` 했을 throw 있습니다. 비동기 로딩 중일 해당 `Promise`를 throw하며, 오류 상태일 때는 해당 오류를 throw합니다. 값이 성공적으로 계산된 상황 위주로 처리하고, 예외 상황은 전부 `catch` 블록 등으로 밀어넣고 싶은 상황에서 유용합니다.
75
+
76
+ `.get()` 메서드는 상태가 비활성화된 경우, 매우 잠시 동안 해당 상태를 살아 있는 상태로 전환합니다. 당연히 해당 상태와 모든 의존성이 새로 실행됩니다. 매우 잠시 동안은 적어도 현재 마이크로태스크가 끝나기까지를 의미합니다. 즉, 동기적으로 연속해서 `.get()`을 호출하더라도 매번 모든 것이 다시 실행되지는 않습니다.
89
77
 
90
- If a state is inactive, the `.get()` method temporarily transitions it to an active state. Naturally, the state and all its dependencies will be re-executed. "Temporarily" means at least until the end of the current microtask. This means that calling `.get()` synchronously multiple times in a row will not cause everything to re-execute each time.
78
+ 비동기 상태를 다룰 때에는 거의 사용할 일이 없습니다. 대신 `.subscribe`, 또는 `.watch`와 `.state`을 사용하세요.
91
79
 
80
+ ##### `.state`
92
81
 
93
- ### Updating State
82
+ `state`은 상태의 현재 상태를 나타내는 읽기 전용 객체입니다. 비동기 상태거나 오류가 예상되는, 값이 준비되지 않은 상황을 처리(placeholder를 보여주는 등)해야 하는 상황에서 유용합니다. `value`는 마지막으로 성공했을 때의 값을 가집니다. `error`와 `promise`는 현재 로딩 중이거나 에러가 발생한 경우 해당 값을 가집니다. `active`는 활성 상태 여부를 나타냅니다. 정확한 타입은 다음과 같습니다:
94
83
 
95
- You can update the value of a primitive state using the `.set(updater)` method. If `updater` is a normal value, the state is updated to that value. If it's a function, the state is updated with `updater(nextValue)`, where `nextValue` is the state's 'pending value'.
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)`로 업데이트합니다.
96
95
 
97
96
  ```javascript
98
97
  console.log($count.get()); // 42
@@ -106,9 +105,9 @@ $count.set(increment);
106
105
  console.log($count.get()); // 101
107
106
  ```
108
107
 
109
- All updates are batched per microtask. This means multiple synchronous updates are processed at once. In particular, if a single state is updated multiple times, it is treated as if it were updated only once with the final value.
108
+ 모든 업데이트는 마이크로태스크를 단위로 배치 처리됩니다. 즉, 동기적으로 발생하는 여러 업데이트는 번에 처리되며, 특히 하나의 상태가 여러 업데이트됐을 경우 마지막 번만 업데이트한 것으로 취급됩니다.
110
109
 
111
- If the `updater` is a function, it can access the last received 'pending value' `nextValue`. Therefore, when `.set` is called multiple times synchronously as shown below, `$count` will be incremented by `3`, but the update still happens only once.
110
+ `updater`가 함수라면, 마지막으로 들어온 '예비 ' `nextValue`에 접근할 있습니다. 따라서, 다음과 같이 동기적으로 여러 번의 `.set`을 호출했을 `$count`는 `3`만큼 증가하게 됩니다. 단, 업데이트는 여전히 번만 됩니다.
112
111
 
113
112
  ```javascript
114
113
  $count.set(increment);
@@ -116,23 +115,23 @@ $count.set(increment);
116
115
  $count.set(increment);
117
116
  ```
118
117
 
119
- If you must update based on the current value, you can use `.get()` or `.state`, like `$count.set($count.state.value + 1)`.
118
+ 반드시 현재 값을 기준으로 업데이트해야 한다면, `$count.set($count.state.value + 1)` 같이 `.get()` 또는 `.state`을 사용할 있습니다.
120
119
 
121
- ### Subscribing to State
120
+ ### 상태 구독
122
121
 
123
- You can detect updates with the `.subscribe(listener)` or `.watch(listener)` methods. Each method returns an unsubscribe function.
122
+ `.subscribe(listener)` 또는 `.watch(listener)` 메서드로 업데이트를 감지할 있습니다. 메서드는 구독 중단 함수를 반환합니다.
124
123
 
125
- Upon subscription, if the state was inactive, an update is scheduled. During the update, the state and all its dependencies are activated. Upon unsubscription, if there are no more subscribers to the state, its deactivation is scheduled, and its dependencies are also checked for deactivation.
124
+ 구독 해당 상태가 비활성화된 상태였다면 업데이트가 예약되며, 업데이트 해당 상태와 의존성까지 모두 활성화됩니다. 구독 해제 해당 상태를 구독하는 곳이 더이상 없다면 비활성화가 예약되며, 의존성도 비활성화 대상인지 확인됩니다.
126
125
 
127
- `.subscribe` calls the given function when the state is successfully updated. If the state has already been successfully updated, the function is called once with the current value upon subscription. The listener is called with the state's value as the first argument and `{ signal }` as the second. The `signal` is linked to the state's lifetime.
126
+ `.subscribe`는 상태가 성공적으로 업데이트되었을 주어진 함수를 호출합니다. 이미 업데이트가 성공적으로 상태일 경우 구독 해당 값으로 호출합니다. 호출 번째 인자는 해당 상태의 값, 번째 인자는 `{ signal }`이 주어지며, `signal`은 상태의 수명과 연동됩니다.
128
127
 
129
- `.watch` calls the given function whenever the state changes. It can be used when you need to handle error or asynchronous states as well.
128
+ `.watch`는 해당 상태가 변화할 주어진 함수를 호출합니다. 오류나 비동기 상태를 추가적으로 처리하려는 경우 있습니다.
130
129
 
131
130
  ```javascript
132
131
  const $count = $(0);
133
132
  const unsubscribe = $count.subscribe((value, { signal }) => {
134
- console.log('value', value);
135
- signal.then(() => console.log('value end', value));
133
+ console.log("value", value);
134
+ signal.then(() => console.log("value end", value));
136
135
  });
137
136
 
138
137
  // value 0
@@ -145,162 +144,192 @@ unsubscribe();
145
144
  // value end 1
146
145
 
147
146
  $count.set(2);
148
- // (no output)
147
+ // (출력 없음)
149
148
  ```
150
149
 
151
- `.subscribe()` returns a function that can be used to unsubscribe. It is important to call this function when a component unmounts to prevent memory leaks.
150
+ `.subscribe()`는 구독을 해제할 있는 함수를 반환합니다. 컴포넌트가 언마운트될 함수를 호출하여 메모리 누수를 방지하는 것이 중요합니다.
152
151
 
153
- If you want to subscribe to multiple states simultaneously, you should declare another state.
152
+ 만약 여러 상태를 동시에 구독하고 싶다면, 상태를 하나 선언해야 합니다.
154
153
 
155
154
  ```javascript
156
155
  const $merged = $((get) => ({
157
- count: get($count),
158
- countDouble: get($countDouble),
156
+ count: get($count),
157
+ countDouble: get($countDouble),
159
158
  }));
160
159
 
161
160
  $merged.subscribe(({ count, countDouble }) => console.log(`${count} * 2 = ${countDouble}`));
162
161
  ```
163
162
 
164
- ### Asynchronous State
163
+ ### 비동기 상태
165
164
 
166
- For a derived state where the function returns a `Promise`, you can use the automatically unwrapped value when you `get` or `subscribe` to it. If you want to handle loading or failure cases, you can use `watch` or `state`.
165
+ 함수에서 `Promise`가 반환된 파생 상태의 경우, 해당 상태를 `get`하거나 `subscribe`했을 자동으로 unwrap된 값을 있습니다. 로딩이나 실패했을 때를 다루고 싶다면 `watch`나 `state`을 사용할 수 있습니다.
167
166
 
168
167
  ```javascript
169
168
  const $user = $(async (get) => {
170
- const response = await fetch(`/users/${get($count)}`);
171
- if (!response.ok) throw new Error('Failed to fetch user');
172
- return response.json();
169
+ const response = await fetch(`/users/${get($count)}`);
170
+ if (!response.ok) throw new Error("Failed to fetch user");
171
+ return response.json();
173
172
  });
174
173
  $user.watch(() => {
175
- console.log($user.state);
174
+ console.log($user.state);
176
175
  });
177
176
 
178
177
  const $userName = $((get) => get($user).name);
179
178
 
180
- const faultyAtom = $(() => Promise.reject(new Error('Something went wrong')));
179
+ const faultyAtom = $(() => Promise.reject(new Error("Something went wrong")));
181
180
  faultyAtom.watch(() => {
182
- if (!faultyAtom.state.promise && faultyAtom.state.error) {
183
- console.error('An error occurred:', faultyAtom.state.error.message);
184
- }
181
+ if (!faultyAtom.state.promise && faultyAtom.state.error) {
182
+ console.error("An error occurred:", faultyAtom.state.error.message);
183
+ }
185
184
  });
186
185
  ```
187
186
 
188
- ### State Lifetime (`signal`)
187
+ ### 상태의 수명 (`signal`)
189
188
 
190
- The `options.signal` passed as an argument to a derived function can be used like an `AbortSignal` and a `Promise` (strictly speaking, a thenable). It is `abort`ed and `resolve`d when the state's lifetime changes, such as when the state is updated or deactivated.
189
+ 파생 함수의 인자로 전달되는 `options.signal`은 `AbortSignal` `Promise` (엄밀히는 thenable)처럼 사용 가능합니다. 상태가 업데이트됐거나, 상태가 비활성화되는 상태의 수명이 변했을 `abort` `resolve`됩니다.
191
190
 
192
- Like an `AbortSignal`, it can be passed to existing web APIs like `fetch` or `addEventListener` for cancellation or unsubscription. Like a `Promise`, you can use `signal.then` to write your own cleanup functions.
191
+ `AbortSignal`처럼 `fetch`나 `addEventListener`같은 기존 API에 전달하여 취소나 구독 중단 등에 사용할 있으며, `Promise`처럼 `signal.then`을 통해 자체 cleanup 함수를 수도 있습니다.
193
192
 
194
193
  ```javascript
195
194
  const $user = $(async (get, { signal }) => {
196
- const count = get($count);
197
- const json = await fetch(`/users/${count}`, { signal }).then((res) => res.json());
198
- signal.then(() => {
199
- console.log(count, json, "not used");
200
- });
201
- return json;
195
+ const count = get($count);
196
+ const json = await fetch(`/users/${count}`, { signal }).then((res) => res.json());
197
+ signal.then(() => {
198
+ console.log(count, json, "not used");
199
+ });
200
+ return json;
202
201
  });
203
202
  ```
204
203
 
205
- ### Custom Update Condition (Equality Check)
204
+ ### 커스텀 업데이트 조건(동등성 확인)
206
205
 
207
- By default, equality is checked with `Object.is`, so for objects or arrays, an update can occur if the reference is different even if the content is the same. To perform additional equality checks, you can provide an `equals` option when declaring the state. In this case, it first checks with `Object.is`, and if they are different, it checks again with the `equals` function. If either returns true, the value change is ignored.
206
+ 기본적으로 `Object.is`로 동등성을 체크하므로 객체나 배열의 경우 참조가 다르면 내용이 같더라도 업데이트가 발생할 있으며, 추가로 동등성을 확인하기 위해 상태 선언 `equals`를 옵션으로 있습니다. 경우 `Object.is`로 같은지 확인하고, 다르다면 `equals` 함수로 다시 확인합니다. 하나라도 참을 반환하는 경우 값 변경은 무시됩니다.
208
207
 
209
208
  ```javascript
210
- const $user = $(
211
- { id: 1, name: 'Alice' },
212
- { equals: (next, prev) => next.id === prev.id },
213
- );
209
+ const $user = $({ id: 1, name: "Alice" }, { equals: (next, prev) => next.id === prev.id });
214
210
 
215
- const $user2 = $(
216
- (get) => get($user),
217
- { equals: (next, prev) => next.name === prev.name },
218
- );
211
+ const $user2 = $((get) => get($user), { equals: (next, prev) => next.name === prev.name });
219
212
 
220
- userAtom.set({ id: 1, name: 'Bob' });
213
+ userAtom.set({ id: 1, name: "Bob" });
221
214
 
222
- userAtom.set({ id: 2, name: 'Alice' });
215
+ userAtom.set({ id: 2, name: "Alice" });
223
216
  ```
224
217
 
225
- In the example above, the first update is ignored because the `id` is the same. The second update has a different `id`, so `$user` is updated, but since the `name` is the same, `$user2` is not updated.
218
+ 예제에서 번째 업데이트는 `id`가 같으므로 무시됩니다. 번째 업데이트는 `id`가 다르므로 `$user`를 업데이트하지만, `name`이 같으므로 `$user2`는 업데이트되지 않습니다.
226
219
 
227
- ### Merging Multiple States
220
+ ### 여러 상태 병합하기
228
221
 
229
- You can create a new state by merging multiple states with `$$`. It's actually the same as `$`, but while `$`'s `get` function throws immediately when it encounters a `Promise` or an error, `$$`'s `get` function returns a special object to track maximum dependencies with minimum re-executions.
222
+ `$$`로 여러 상태를 병합한 새로운 상태를 만들 있습니다. `$`와 사용법은 같지만, `$`의 `get` 함수는 `Promise`나 에러를 만났을 즉시 throw하는 반면, `$$`의 `get` 함수는 특별한 객체를 반환하여 최소한의 재실행으로 최대한의 의존성을 추적합니다. 또한, 배열이나 객체를 반환 단계 깊은 비교를 수행합니다.
230
223
 
231
- The following code takes 5 seconds to merge states with `$`, whereas it takes only 1 second with `$$`.
224
+ 다음 코드는 `$`로 상태를 병합하면 5초가 걸리는 반면에, `$$`로 상태를 병합하면 1초만이 걸립니다.
232
225
 
233
226
  ```javascript
234
227
  const timer = (time) => new Promise((resolve) => setTimeout(() => resolve(1), time));
235
- const a = [1, 2, 3, 4, 5].map(() => $(() => timer(1000)));
236
- const merged = $$((get) => a.map(get));
228
+ const timerAtoms = [1, 2, 3, 4, 5].map(() => $(() => timer(1000)));
229
+ const $timers = $$((get) => timerAtoms.map(get));
237
230
  console.time();
238
- merged.subscribe(() => console.timeEnd());
231
+ $timers.subscribe(() => console.timeEnd());
239
232
  ```
240
233
 
241
- For reference, the value that `$$`'s `get` function returns instead of throwing when it encounters a `Promise` or an error is created through the following process:
234
+ 참고로 `$$`의 `get` 함수가 `Promise`나 에러를 만났을 throw 대신 반환하는 값은 다음 과정으로 만들어집니다:
242
235
 
243
236
  ```javascript
244
237
  const o = () => o;
245
238
  const toUndefined = () => undefined;
246
- Object.setPrototypeOf(o, new Proxy(o, { get: (_, k) => k === Symbol.toPrimitive ? toUndefined : o }));
239
+ Object.setPrototypeOf(
240
+ o,
241
+ new Proxy(o, { get: (_, k) => (k === Symbol.toPrimitive ? toUndefined : o) }),
242
+ );
247
243
  ```
248
244
 
249
- The `o` in this code returns the same value no matter how many properties are accessed or functions are called. For example, `o.a.b.c().d()().asdf()()()() === o` is `true`. Therefore, it allows most state-merging functions composed of selectors and simple methods like filter/map/reduce to execute without issues. However, it's not a silver bullet, so some caution is needed, and it should preferably be used only for state merging.
245
+ 코드의 `o`는 아무리 프로퍼티 접근 호출을 해도 같은 값을 반환합니다. 예를 들어, `o.a.b.c().d()().asdf()()()() === o`는 `true`입니다. 따라서, 셀렉터와 filter/map/reduce 간단한 메서드로 이뤄진 대부분의 상태 병합 함수에서 문제 없이 전체 코드를 실행할 있게 만듭니다. 하지만 만능은 아니므로 약간의 주의가 필요하며, 가급적 상태 병합에만 사용해야 합니다.
246
+
247
+ 또한 상태 병합 시 특정 값들에서 부분씩만 가져와 새로운 객체를 만드는 경우가 흔하므로, `$$`로 병합한 상태의 의존성들이 너무 자주 재실행되지 않도록 `$`의 `equals`를 다음으로 설정합니다:
250
248
 
251
- ## In-Depth
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
+ ```
252
271
 
253
- ### How much should I split the state?
272
+ 정확히 단계만 비교하므로, `{ arr: [...] }`와 같은 객체의 경우 `arr`의 내용이 같더라도 참조가 다르다면 의존성이 재실행됩니다. 필요하다면 `arr` 부분은 다른 `$$`로 감싼 뒤 합치거나, 상태를 직접 `$`로 `equals`를 주고 만드세요.
254
273
 
255
- Split your state as much as possible, as long as it doesn't significantly harm code readability. Also, wrap as much logic as you can in as many layers of state as possible.
274
+ ## 상세
256
275
 
257
- In fact, the reason `subscribe` wasn't designed to take a `get` function like `$` is to encourage splitting states as much as possible, so that `subscribe` only deals with the 'final state'.
276
+ ### 상태를 얼마나 쪼개는 좋나요?
258
277
 
259
- Implicit 'intermediate states' remain 'hidden,' causing you to lose many of the library's benefits and potentially face issues like unnecessary recalculations, inability to reuse intermediate values, complicated dependency tracking, code repetition, broken side-effect idempotency, and the inability to manage fine-grained lifecycles and subscriptions.
278
+ 코드의 가독성을 크게 해치지 않는 선에서 최대한 많이 쪼개세요. 또한 최대한 많은 로직을, 최대한 많은 단계의 상태로 감싸세요.
260
279
 
261
- For example, the following shows a situation where not splitting the state enough leads to 'unnecessary recalculations and inability to reuse intermediate values'.
280
+ 사실, `$`처럼 `get`으로 상태의 값을 가져오는 방식으로 `subscribe`를 만들지 않은 이유 또한 상태를 최대한 많이 쪼개고, `subscribe`는 '최종 상태'만을 다루도록 하기 위함입니다.
281
+
282
+ 암묵적인 '중간 상태'들은 '숨겨져' 있게 되므로 본 라이브러리의 혜택을 상당수 잃고, 불필요한 재계산 및 중간 값 활용 불가능, 복잡해지는 의존성 파악, 코드의 반복, 사이드 이펙트의 멱등성 깨짐, 세세한 생명 주기 및 구독 관리 불가능 등의 문제를 겪을 수 있습니다.
283
+
284
+ 예를 들어, 다음은 상태를 덜 쪼개서 '불필요한 재계산 및 중간 값 활용 불가능'이 발생하는 상황을 보여줍니다.
262
285
 
263
286
  ```javascript
264
287
  const $userId = $(123);
265
288
  const $postId = $(456);
266
289
  const $pageData = $(async (get, { signal }) => {
267
- const user = await fetch(`/users/${get($userId)}`, { signal }).then((res) => res.json());
268
- const post = await fetch(`/posts/${get($postId)}`, { signal }).then((res) => res.json());
269
- userElm.innerHTML = user.name;
270
- postElm.innerHTML = post.html;
271
- commentElm.innerHTML = `Hello ${user.name}! Comment to ${post.author}.`;
290
+ const user = await fetch(`/users/${get($userId)}`, { signal }).then((res) => res.json());
291
+ const post = await fetch(`/posts/${get($postId)}`, { signal }).then((res) => res.json());
292
+ userElm.innerHTML = user.name;
293
+ postElm.innerHTML = post.html;
294
+ commentElm.innerHTML = `Hello ${user.name}! Comment to ${post.author}.`;
272
295
  });
273
296
  ```
274
297
 
275
- This code looks simple and clean, but if only one of `userId` or `postId` is updated, both requests are sent again. The latencies of `user` and `post` are summed up (which can be solved with `Promise.all`, but this increases code complexity). Unrelated side effects coexist, mixing contexts. And even if other values in `user` or `post` don't change, the `innerHTML` is updated, causing the DOM to be completely replaced. There are several problems. It should be split as follows:
298
+ 코드는 간단하고 깔끔해 보이지만, `userId`와 `postId` 하나만 업데이트되어도 개의 요청이 다시 보내지며, `user`와 `post`의 레이턴시가 합산되고 (`Promise.all`로 해결 가능하지만 코드 복잡도가 상승합니다.), 관계 없는 사이드 이펙트가 함께 존재해 맥락이 섞이고, `user`나 `post`의 다른 값이 바뀌지 않더라도 `innerHTML`를 바꾸어 DOM 완전히 갈아엎어지는 등, 여러 문제가 있습니다. 다음과 같이 쪼개야 합니다.
276
299
 
277
300
  ```javascript
278
301
  const $userId = $(123);
279
302
  const $user = $((get) => fetch(`/users/${get($userId)}`, { signal }).then((res) => res.json()));
280
- $user.subscribe((user) => { userElm.innerHTML = user.name; });
303
+ $user.subscribe((user) => {
304
+ userElm.innerHTML = user.name;
305
+ });
281
306
 
282
307
  const $postId = $(456);
283
308
  const $post = $((get) => fetch(`/posts/${get($postId)}`, { signal }).then((res) => res.json()));
284
- $post.subscribe((post) => { postElm.innerHTML = post.html; });
309
+ $post.subscribe((post) => {
310
+ postElm.innerHTML = post.html;
311
+ });
285
312
 
286
313
  const $pageData = $$((get) => ({
287
- userName: get($user).name,
288
- postAuthor: get($post).author,
314
+ userName: get($user).name,
315
+ postAuthor: get($post).author,
289
316
  }));
290
- $pageData.subscribe(({ userName, postAuthor }) => { commentElm.innerHTML = `Hello ${userName}! Comment to ${postAuthor}.`; });
317
+ $pageData.subscribe(({ userName, postAuthor }) => {
318
+ commentElm.innerHTML = `Hello ${userName}! Comment to ${postAuthor}.`;
319
+ });
291
320
  ```
292
321
 
293
- The number of lines of code has increased slightly, but the previously mentioned problems have been resolved.
322
+ 코드 수가 약간 늘어났지만, 앞서 언급한 문제들이 해결되었습니다.
294
323
 
295
- This might be too simple of an example to be fully convincing, but in real-world scenarios, it's easy to accidentally mix states in a moment of carelessness. Also, the desire to handle everything in one place can often be hard to resist.
324
+ 아마 너무 간단한 예시라 별로 와닿지 않을 수도 있지만, 현실에서는 아차 하는 순간 자신도 모르게 상태를 뒤섞기 쉬우며, 모든 것을 곳에서 처리하려는 욕망을 참기 어려운 경우도 종종 발생합니다.
296
325
 
297
- It's important to always be mindful of this and to split, wrap, and layer your states.
326
+ 항상 이를 신경쓰며 상태를 쪼개고, 감싸고, 단계를 나누는 것이 중요합니다.
298
327
 
299
- ### How to implement `onMount`/`onCleanup`?
328
+ ### `onMount`/`onCleanup`은 어떻게 하나요?
300
329
 
301
- Sometimes you need to call a function not every time a state's value changes, but when the state becomes active and inactive (i.e., when subscribers start appearing and when there are no longer any subscribers). In other words, you need functionality like `onMount`/`onCleanup` (or `onDestroy`, etc.).
330
+ 상태의 값이 바뀔 때마다가 아니라, 상태가 활성화됐을 때와 비활성화됐을 (상태를 구독한 곳이 생기기 시작했을 때와 더이상 아무 곳에서도 구독하고 있지 않을 ) 함수를 호출해야 하는 경우가 있습니다. 즉, `onMount`/`onCleanup` (또는 `onDestroy` )와 같은 기능이 필요합니다.
302
331
 
303
- This can be solved in two ways. One is to create and return a state from within another state:
332
+ 이는 가지 방법으로 해결할 있습니다. 하나는 상태 안에서 상태를 만들어 반환하는 것입니다:
304
333
 
305
334
  ```javascript
306
335
  const $shared = $((_, { signal }) => {
@@ -316,7 +345,7 @@ const $a = $((get) => {
316
345
  });
317
346
  ```
318
347
 
319
- If modifications to `$state` only occur within `onMount` and `onCleanup` (for example, when subscribing to external events), this is the cleanest pattern. The following is an example that applies this to manage a connection shared by multiple places:
348
+ `$state`의 수정이 `onMount` `onCleanup` 내에서만 발생하는 경우(가령 외부 이벤트를 구독하는 상황), 가장 깔끔한 패턴입니다. 다음은 이를 응용하여 여러 곳에서 공유하는 연결을 다루는 예시입니다:
320
349
 
321
350
  ```javascript
322
351
  const $wsConnection = $(() => {
@@ -355,7 +384,7 @@ const $alice = lastMessage("alice");
355
384
  const $bob = lastMessage("bob");
356
385
  ```
357
386
 
358
- If the state needs to be modifiable from the outside, you can create two states like this:
387
+ 만약 외부에서 상태를 수정할 있어야 한다면, 다음처럼 상태를 만들 있습니다:
359
388
 
360
389
  ```javascript
361
390
  const $writer = $(0);
@@ -369,38 +398,40 @@ const $reader = $((get) => {
369
398
  });
370
399
  ```
371
400
 
372
- Now, you can read from `$reader` and write to `$writer`. Since `$shared` does not depend on `$writer`, `$shared` will not be updated even if `$writer` is modified.
401
+ 이제 읽기는 `$reader`로, 쓰기는 `$writer`로 하면 됩니다. `$shared`는 `$writer`에 의존하지 않으므로, `$writer`가 수정되더라도 `$shared`는 업데이트되지 않습니다.
373
402
 
374
- ## Examples
403
+ ## 예제
375
404
 
376
- #### Debounce-Throttling
405
+ #### 디바운스-스로틀링
377
406
 
378
407
  ```javascript
379
408
  const delayedState = (initial, minDelay, maxDelay) => {
380
- const $value = $(initial);
381
- const $delayedValue = $(initial);
382
-
383
- const $eventStartTime = $(0);
384
- const $eventLastTime = $(0);
385
- const $delayedTime = $((get) => Math.min(get($eventStartTime) + maxDelay, get($eventLastTime) + minDelay));
386
- const $delayedInfo = $((get) => ({
387
- value: get($value),
388
- time: get($delayedTime),
389
- }));
390
- $delayedInfo.subscribe(({ value, time }, { signal }) => {
391
- const timeout = Math.max(0, time - Date.now());
392
- const timer = setTimeout(() => $delayedValue.set(value), timeout);
393
- signal.then(() => clearTimeout(timer));
394
- });
395
-
396
- const update = (value, eager = false) => {
397
- const now = eager ? -Infinity : Date.now();
398
- if ($value.get() === $delayedValue.get()) $eventStartTime.set(now);
399
- $eventLastTime.set(now);
400
- $value.set(value);
401
- };
402
-
403
- return [$delayedValue, update];
409
+ const $value = $(initial);
410
+ const $delayedValue = $(initial);
411
+
412
+ const $eventStartTime = $(0);
413
+ const $eventLastTime = $(0);
414
+ const $delayedTime = $((get) =>
415
+ Math.min(get($eventStartTime) + maxDelay, get($eventLastTime) + minDelay),
416
+ );
417
+ const $delayedInfo = $((get) => ({
418
+ value: get($value),
419
+ time: get($delayedTime),
420
+ }));
421
+ $delayedInfo.subscribe(({ value, time }, { signal }) => {
422
+ const timeout = Math.max(0, time - Date.now());
423
+ const timer = setTimeout(() => $delayedValue.set(value), timeout);
424
+ signal.then(() => clearTimeout(timer));
425
+ });
426
+
427
+ const update = (value, eager = false) => {
428
+ const now = eager ? -Infinity : Date.now();
429
+ if ($value.get() === $delayedValue.get()) $eventStartTime.set(now);
430
+ $eventLastTime.set(now);
431
+ $value.set(value);
432
+ };
433
+
434
+ return [$delayedValue, update];
404
435
  };
405
436
  ```
406
437
 
@@ -410,50 +441,50 @@ inputElm.addEventListener("input", (e) => updateInput(e.currentTarget.value));
410
441
  $inputValue.subscribe(console.log);
411
442
  ```
412
443
 
413
- ### Scroll Direction Detection
444
+ ### 스크롤 방향 감지
414
445
 
415
446
  ```javascript
416
447
  const $windowScroll = $((_, { signal }) => {
417
- let lastTime = Date.now();
418
- const $scrollY = $(window.scrollY);
419
- const $scrollOnTop = $((get) => get($scrollY) === 0);
420
-
421
- const $scrollMovingAvgY = $(0);
422
- const $scrollDirectionY = $((get) => Math.sign(get($scrollMovingAvgY)));
423
-
424
- const onScrollChange = () => {
425
- const now = Date.now();
426
- lastTime = now;
427
-
428
- const alpha = 0.995 ** (now - lastTime);
429
- const scrollY = window.scrollY;
430
- const deltaY = scrollY - $scrollY.get();
431
- const movingAvgY = alpha * $scrollMovingAvgY.get() + (1 - alpha) * deltaY;
432
-
433
- $scrollY.set(scrollY);
434
- $scrollMovingAvgY.set(movingAvgY);
435
- };
436
- window.addEventListener('scroll', onScrollChange, {
437
- passive: true,
438
- signal,
439
- });
440
- window.addEventListener('resize', onScrollChange, {
441
- passive: true,
442
- signal,
443
- });
444
-
445
- return {
446
- $scrollY,
447
- $scrollOnTop,
448
- $scrollMovingAvgY,
449
- $scrollDirectionY,
450
- };
448
+ let lastTime = Date.now();
449
+ const $scrollY = $(window.scrollY);
450
+ const $scrollOnTop = $((get) => get($scrollY) === 0);
451
+
452
+ const $scrollMovingAvgY = $(0);
453
+ const $scrollDirectionY = $((get) => Math.sign(get($scrollMovingAvgY)));
454
+
455
+ const onScrollChange = () => {
456
+ const now = Date.now();
457
+ lastTime = now;
458
+
459
+ const alpha = 0.995 ** (now - lastTime);
460
+ const scrollY = window.scrollY;
461
+ const deltaY = scrollY - $scrollY.get();
462
+ const movingAvgY = alpha * $scrollMovingAvgY.get() + (1 - alpha) * deltaY;
463
+
464
+ $scrollY.set(scrollY);
465
+ $scrollMovingAvgY.set(movingAvgY);
466
+ };
467
+ window.addEventListener("scroll", onScrollChange, {
468
+ passive: true,
469
+ signal,
470
+ });
471
+ window.addEventListener("resize", onScrollChange, {
472
+ passive: true,
473
+ signal,
474
+ });
475
+
476
+ return {
477
+ $scrollY,
478
+ $scrollOnTop,
479
+ $scrollMovingAvgY,
480
+ $scrollDirectionY,
481
+ };
451
482
  });
452
483
 
453
484
  const $navHidden = $((get) => {
454
- const { $scrollOnTop, $scrollDirectionY } = get($windowScroll);
455
- const scrollOnTop = get($scrollOnTop);
456
- const directionY = get($scrollDirectionY);
457
- return !scrollOnTop && directionY > 0;
485
+ const { $scrollOnTop, $scrollDirectionY } = get($windowScroll);
486
+ const scrollOnTop = get($scrollOnTop);
487
+ const directionY = get($scrollDirectionY);
488
+ return !scrollOnTop && directionY > 0;
458
489
  });
459
- ```
490
+ ```