@wooojin/forgen 0.4.8 → 0.4.9
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/.claude-plugin/plugin.json +1 -1
- package/assets/dev-guide/be/README.md +226 -0
- package/assets/dev-guide/be/adapters/build-agents-md.sh +63 -0
- package/assets/dev-guide/be/principles/common.md +433 -0
- package/assets/dev-guide/be/principles/go.md +469 -0
- package/assets/dev-guide/be/principles/node.md +388 -0
- package/assets/dev-guide/be/skills/go/be-build/SKILL.md +262 -0
- package/assets/dev-guide/be/skills/go/be-perf/SKILL.md +308 -0
- package/assets/dev-guide/be/skills/go/be-review/SKILL.md +119 -0
- package/assets/dev-guide/be/skills/go/be-security/SKILL.md +362 -0
- package/assets/dev-guide/be/skills/node/be-build/SKILL.md +239 -0
- package/assets/dev-guide/be/skills/node/be-perf/SKILL.md +272 -0
- package/assets/dev-guide/be/skills/node/be-review/SKILL.md +118 -0
- package/assets/dev-guide/be/skills/node/be-security/SKILL.md +355 -0
- package/assets/dev-guide/be/sources/12factor/INDEX.md +53 -0
- package/assets/dev-guide/be/sources/api-design/INDEX.md +56 -0
- package/assets/dev-guide/be/sources/ddia/INDEX.md +55 -0
- package/assets/dev-guide/be/sources/go-runtime/INDEX.md +62 -0
- package/assets/dev-guide/be/sources/node-runtime/INDEX.md +60 -0
- package/assets/dev-guide/be/sources/otel/INDEX.md +53 -0
- package/assets/dev-guide/be/sources/owasp-api/INDEX.md +52 -0
- package/assets/dev-guide/be/sources/postgres/INDEX.md +55 -0
- package/assets/dev-guide/be/sources/sre-book/INDEX.md +48 -0
- package/assets/dev-guide/fe/README.md +197 -0
- package/assets/dev-guide/fe/adapters/build-agents-md.sh +63 -0
- package/assets/dev-guide/fe/adapters/refresh.sh +68 -0
- package/assets/dev-guide/fe/principles/common.md +160 -0
- package/assets/dev-guide/fe/principles/react.md +183 -0
- package/assets/dev-guide/fe/principles/vue.md +196 -0
- package/assets/dev-guide/fe/skills/react/fe-build/SKILL.md +139 -0
- package/assets/dev-guide/fe/skills/react/fe-perf/SKILL.md +179 -0
- package/assets/dev-guide/fe/skills/react/fe-review/SKILL.md +141 -0
- package/assets/dev-guide/fe/skills/vue/fe-build/SKILL.md +148 -0
- package/assets/dev-guide/fe/skills/vue/fe-perf/SKILL.md +163 -0
- package/assets/dev-guide/fe/skills/vue/fe-review/SKILL.md +136 -0
- package/assets/dev-guide/fe/sources/a11y-dx/INDEX.md +41 -0
- package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-memory.md +150 -0
- package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-performance.md +99 -0
- package/assets/dev-guide/fe/sources/a11y-dx/lighthouse-audits.md +146 -0
- package/assets/dev-guide/fe/sources/a11y-dx/react-devtools-profiler.md +128 -0
- package/assets/dev-guide/fe/sources/a11y-dx/wcag22-new-criteria.md +174 -0
- package/assets/dev-guide/fe/sources/perf/01-core-web-vitals.md +58 -0
- package/assets/dev-guide/fe/sources/perf/02-inp.md +83 -0
- package/assets/dev-guide/fe/sources/perf/03-lcp-cls.md +130 -0
- package/assets/dev-guide/fe/sources/perf/04-speculation-rules.md +148 -0
- package/assets/dev-guide/fe/sources/perf/05-view-transitions.md +153 -0
- package/assets/dev-guide/fe/sources/perf/06-nextjs-caching.md +188 -0
- package/assets/dev-guide/fe/sources/perf/07-server-components.md +181 -0
- package/assets/dev-guide/fe/sources/perf/08-ppr.md +133 -0
- package/assets/dev-guide/fe/sources/perf/09-nextjs-image.md +200 -0
- package/assets/dev-guide/fe/sources/perf/10-optimize-lcp.md +201 -0
- package/assets/dev-guide/fe/sources/perf/INDEX.md +88 -0
- package/assets/dev-guide/fe/sources/react/INDEX.md +41 -0
- package/assets/dev-guide/fe/sources/react/keeping-components-pure.md +135 -0
- package/assets/dev-guide/fe/sources/react/no-effect-patterns.md +183 -0
- package/assets/dev-guide/fe/sources/react/react-compiler.md +182 -0
- package/assets/dev-guide/fe/sources/react/server-components.md +194 -0
- package/assets/dev-guide/fe/sources/react/server-functions.md +192 -0
- package/assets/dev-guide/fe/sources/react/suspense.md +218 -0
- package/assets/dev-guide/fe/sources/react/use-action-state.md +123 -0
- package/assets/dev-guide/fe/sources/react/use-form-status.md +158 -0
- package/assets/dev-guide/fe/sources/react/use-hook.md +153 -0
- package/assets/dev-guide/fe/sources/react/use-optimistic.md +194 -0
- package/assets/dev-guide/fe/sources/toss-ff/INDEX.md +58 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-code-directory.md +79 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-form-fields.md +110 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-magic-number.md +47 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-item-edit-modal.md +124 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-use-bottom-sheet.md +57 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-use-page-state.md +71 -0
- package/assets/dev-guide/fe/sources/toss-ff/overview-4-principles.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-hidden-logic.md +59 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-http.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-use-user.md +110 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-comparison-order.md +52 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-condition-name.md +64 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-login-start-page.md +183 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-magic-number.md +53 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-submit-button.md +73 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-ternary-operator.md +38 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-use-page-state.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-user-policy.md +98 -0
- package/assets/dev-guide/fe/sources/vue/INDEX.md +17 -0
- package/assets/dev-guide/fe/sources/vue/composition-api.md +251 -0
- package/assets/dev-guide/fe/sources/vue/nuxt-data-fetching.md +232 -0
- package/assets/dev-guide/fe/sources/vue/pinia-state-management.md +134 -0
- package/assets/dev-guide/fe/sources/vue/reactivity-pitfalls.md +261 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-a.md +117 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-b.md +231 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-c.md +86 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-d.md +72 -0
- package/dist/cli.js +42 -0
- package/dist/core/dashboard-cli.d.ts +12 -0
- package/dist/core/dashboard-cli.js +226 -0
- package/dist/core/dev-guide-injector.d.ts +26 -0
- package/dist/core/dev-guide-injector.js +137 -0
- package/dist/core/init.js +53 -0
- package/dist/core/lifecycle-classifier.d.ts +23 -0
- package/dist/core/lifecycle-classifier.js +104 -0
- package/dist/core/observability-backfill.d.ts +31 -0
- package/dist/core/observability-backfill.js +178 -0
- package/dist/core/observability-store.d.ts +58 -0
- package/dist/core/observability-store.js +195 -0
- package/dist/core/session-store.js +4 -0
- package/dist/core/spawn.d.ts +17 -0
- package/dist/core/spawn.js +179 -2
- package/dist/core/statusline-cli.js +34 -1
- package/dist/engine/compound-extractor.js +39 -0
- package/dist/engine/compound-loop.js +6 -0
- package/dist/engine/compound-retire.d.ts +20 -0
- package/dist/engine/compound-retire.js +85 -0
- package/dist/hooks/context-guard.js +25 -1
- package/dist/hooks/post-tool-use.js +48 -0
- package/dist/hooks/solution-injector.js +93 -0
- package/dist/host/install-claude.d.ts +6 -2
- package/dist/host/install-claude.js +74 -2
- package/dist/host/install-codex.d.ts +4 -0
- package/dist/host/install-codex.js +71 -0
- package/dist/host/install-orchestrator.js +1 -0
- package/package.json +6 -6
- package/plugin.json +1 -1
- package/scripts/postinstall.js +134 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Suspense & Streaming
|
|
3
|
+
source: https://react.dev/reference/react/Suspense
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
category: suspense
|
|
6
|
+
react_version: 19
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 개요
|
|
10
|
+
|
|
11
|
+
`<Suspense>`는 자식 컴포넌트가 로딩 완료될 때까지 fallback UI를 표시하는 React 컴포넌트.
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
<Suspense fallback={<Loading />}>
|
|
15
|
+
<SomeComponent />
|
|
16
|
+
</Suspense>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Props
|
|
22
|
+
|
|
23
|
+
| Prop | 설명 |
|
|
24
|
+
|------|------|
|
|
25
|
+
| `children` | 렌더링할 실제 UI. suspend 시 fallback으로 전환 |
|
|
26
|
+
| `fallback` | 로딩 중 대체 UI (스피너, 스켈레톤 등 경량 노드) |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 핵심 Caveats
|
|
31
|
+
|
|
32
|
+
1. **State 리셋**: 마운트 전 suspend된 렌더의 state는 보존되지 않음 — 컴포넌트 로드 시 처음부터 재시도
|
|
33
|
+
2. **Fallback 재표시**: Suspense가 콘텐츠를 표시하다가 다시 suspend되면 fallback 재표시 (단, `startTransition`/`useDeferredValue`로 발생한 업데이트는 예외)
|
|
34
|
+
3. **Layout Effects 정리**: 콘텐츠 숨김 시 layout Effect 정리, 다시 표시 시 재실행
|
|
35
|
+
4. **Streaming 내장**: Streaming Server Rendering + Selective Hydration 최적화 포함
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Suspense를 트리거하는 데이터 소스
|
|
40
|
+
|
|
41
|
+
- Relay, Next.js 등 Suspense 지원 프레임워크
|
|
42
|
+
- `lazy()`로 지연 로딩된 컴포넌트
|
|
43
|
+
- `use()`로 읽는 캐시된 Promise
|
|
44
|
+
|
|
45
|
+
> ⚠️ Effect나 이벤트 핸들러 내부의 데이터 페칭은 감지하지 못함.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 사용 패턴
|
|
50
|
+
|
|
51
|
+
### 1. 기본 fallback 표시
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
<Suspense fallback={<Loading />}>
|
|
55
|
+
<Albums />
|
|
56
|
+
</Suspense>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. 함께 나타나는 콘텐츠 그룹
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
<Suspense fallback={<Loading />}>
|
|
63
|
+
<Biography />
|
|
64
|
+
<Panel>
|
|
65
|
+
<Albums />
|
|
66
|
+
</Panel>
|
|
67
|
+
</Suspense>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
> 두 컴포넌트가 서로 기다려 함께 나타남.
|
|
71
|
+
|
|
72
|
+
### 3. 중첩 Suspense로 순차 표시 (Streaming)
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
<Suspense fallback={<BigSpinner />}>
|
|
76
|
+
<Biography />
|
|
77
|
+
<Suspense fallback={<AlbumsGlimmer />}>
|
|
78
|
+
<Panel>
|
|
79
|
+
<Albums />
|
|
80
|
+
</Panel>
|
|
81
|
+
</Suspense>
|
|
82
|
+
</Suspense>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**순서:**
|
|
86
|
+
1. Biography 로딩 중 → `BigSpinner` 표시
|
|
87
|
+
2. Biography 완료 → `AlbumsGlimmer` 표시
|
|
88
|
+
3. Albums 완료 → 최종 UI
|
|
89
|
+
|
|
90
|
+
### 4. 새 콘텐츠 로딩 중 이전 콘텐츠 유지 (`useDeferredValue`)
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
import { Suspense, useState, useDeferredValue } from 'react';
|
|
94
|
+
|
|
95
|
+
export default function App() {
|
|
96
|
+
const [query, setQuery] = useState('');
|
|
97
|
+
const deferredQuery = useDeferredValue(query);
|
|
98
|
+
const isStale = query !== deferredQuery;
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<>
|
|
102
|
+
<label>
|
|
103
|
+
Search albums:
|
|
104
|
+
<input value={query} onChange={e => setQuery(e.target.value)} />
|
|
105
|
+
</label>
|
|
106
|
+
<Suspense fallback={<h2>Loading...</h2>}>
|
|
107
|
+
<div style={{ opacity: isStale ? 0.5 : 1 }}>
|
|
108
|
+
<SearchResults query={deferredQuery} />
|
|
109
|
+
</div>
|
|
110
|
+
</Suspense>
|
|
111
|
+
</>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
입력은 즉시 업데이트, 이전 결과는 opacity 0.5로 표시되다가 새 결과 로드 시 전환.
|
|
117
|
+
|
|
118
|
+
### 5. 표시 중인 콘텐츠가 fallback으로 교체되지 않도록 (`startTransition`)
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
import { useTransition } from 'react';
|
|
122
|
+
|
|
123
|
+
function Router() {
|
|
124
|
+
const [page, setPage] = useState('/');
|
|
125
|
+
const [isPending, startTransition] = useTransition();
|
|
126
|
+
|
|
127
|
+
function navigate(url) {
|
|
128
|
+
startTransition(() => {
|
|
129
|
+
setPage(url);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<Layout isPending={isPending}>
|
|
135
|
+
{content}
|
|
136
|
+
</Layout>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 6. 네비게이션 시 Suspense 경계 리셋 (`key`)
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
<ProfilePage key={queryParams.id} />
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
> `key`가 변경되면 중첩된 모든 Suspense 경계가 리셋됨.
|
|
148
|
+
|
|
149
|
+
### 7. 클라이언트 전용 콘텐츠
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
<Suspense fallback={<Loading />}>
|
|
153
|
+
<Chat />
|
|
154
|
+
</Suspense>
|
|
155
|
+
|
|
156
|
+
function Chat() {
|
|
157
|
+
if (typeof window === 'undefined') {
|
|
158
|
+
throw Error('Chat should only render on the client.');
|
|
159
|
+
}
|
|
160
|
+
// ...
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
서버 HTML에서는 fallback, 클라이언트에서 컴포넌트로 교체.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 완전한 예시
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
import { Suspense, useState, useTransition } from 'react';
|
|
172
|
+
|
|
173
|
+
export default function ArtistPage({ artist }) {
|
|
174
|
+
return (
|
|
175
|
+
<>
|
|
176
|
+
<h1>{artist.name}</h1>
|
|
177
|
+
<Suspense fallback={<LoadingBio />}>
|
|
178
|
+
<Biography artistId={artist.id} />
|
|
179
|
+
</Suspense>
|
|
180
|
+
<Suspense fallback={<LoadingAlbums />}>
|
|
181
|
+
<Albums artistId={artist.id} />
|
|
182
|
+
</Suspense>
|
|
183
|
+
</>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function Biography({ artistId }) {
|
|
188
|
+
const bio = use(fetchData(`/${artistId}/bio`));
|
|
189
|
+
return <section><p>{bio}</p></section>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function Albums({ artistId }) {
|
|
193
|
+
const albums = use(fetchData(`/${artistId}/albums`));
|
|
194
|
+
return (
|
|
195
|
+
<ul>
|
|
196
|
+
{albums.map(album => (
|
|
197
|
+
<li key={album.id}>{album.title} ({album.year})</li>
|
|
198
|
+
))}
|
|
199
|
+
</ul>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Troubleshooting
|
|
207
|
+
|
|
208
|
+
**업데이트 시 원치 않는 fallback 방지:**
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
function handleNextPageClick() {
|
|
212
|
+
startTransition(() => {
|
|
213
|
+
setCurrentPage(currentPage + 1);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
> `startTransition`으로 긴급하지 않은 업데이트 표시 → 충분한 데이터가 로드될 때까지 fallback 표시 지연. 긴급 업데이트는 여전히 즉시 fallback 표시.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: useActionState
|
|
3
|
+
source: https://react.dev/reference/react/useActionState
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
category: api
|
|
6
|
+
react_version: 19
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 개요
|
|
10
|
+
|
|
11
|
+
Action(비동기 작업)의 결과로 state를 관리하는 Hook. 폼 제출 및 비동기 작업에 특히 유용.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 시그니처
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const [state, dispatchAction, isPending] = useActionState(reducerAction, initialState, permalink?);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Parameters
|
|
22
|
+
- `reducerAction`: `(previousState, actionPayload) => newState` — 사이드 이펙트 수행 후 새 state 반환
|
|
23
|
+
- `initialState`: 초기 state (첫 dispatch 이후 무시됨)
|
|
24
|
+
- `permalink?` (optional): Server Components 점진적 향상을 위한 URL
|
|
25
|
+
|
|
26
|
+
### Returns
|
|
27
|
+
1. **현재 state** — 초기엔 `initialState`, 이후엔 `reducerAction` 반환값
|
|
28
|
+
2. **`dispatchAction`** — action을 트리거하는 함수
|
|
29
|
+
3. **`isPending`** — action 처리 중 여부 boolean
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 사용 패턴
|
|
34
|
+
|
|
35
|
+
### 기본 사용
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
const [count, dispatchAction, isPending] = useActionState(
|
|
39
|
+
async (prevCount) => await addToCart(prevCount),
|
|
40
|
+
0
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
function handleClick() {
|
|
44
|
+
startTransition(() => {
|
|
45
|
+
dispatchAction();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 여러 액션 타입 처리
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
async function updateCartAction(prevCount, actionPayload) {
|
|
54
|
+
switch (actionPayload.type) {
|
|
55
|
+
case 'ADD':
|
|
56
|
+
return await addToCart(prevCount);
|
|
57
|
+
case 'REMOVE':
|
|
58
|
+
return await removeFromCart(prevCount);
|
|
59
|
+
}
|
|
60
|
+
return prevCount;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const [count, dispatchAction, isPending] = useActionState(updateCartAction, 0);
|
|
64
|
+
dispatchAction({ type: 'ADD' });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Form Action과 함께 사용
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
<form action={dispatchAction}>
|
|
71
|
+
<input name="quantity" type="number" />
|
|
72
|
+
<button type="submit">Add to Cart</button>
|
|
73
|
+
</form>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
> form과 함께 사용 시 `reducerAction`은 `(previousState, formData)`를 받음.
|
|
77
|
+
|
|
78
|
+
### useOptimistic과 조합
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
const [count, dispatchAction, isPending] = useActionState(updateCartAction, 0);
|
|
82
|
+
const [optimisticCount, setOptimisticCount] = useOptimistic(count);
|
|
83
|
+
|
|
84
|
+
function handleAdd() {
|
|
85
|
+
startTransition(() => {
|
|
86
|
+
setOptimisticCount(c => c + 1);
|
|
87
|
+
dispatchAction({ type: 'ADD' });
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 에러 처리
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
async function updateCartAction(prevState, quantity) {
|
|
96
|
+
const result = await addToCart(prevState.count, quantity);
|
|
97
|
+
if (result.error) {
|
|
98
|
+
return { ...prevState, error: result.error };
|
|
99
|
+
}
|
|
100
|
+
return { count: result.count, error: null };
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 주요 Caveats
|
|
107
|
+
|
|
108
|
+
- 최상위에서만 호출 (loop/조건문 내 불가)
|
|
109
|
+
- `dispatchAction` 호출은 순서대로 큐에 쌓여 실행됨
|
|
110
|
+
- **반드시 Transition 내에서 호출**: `startTransition` 또는 Action prop (form은 자동 래핑)
|
|
111
|
+
- `dispatchAction`은 안정적 참조 → Effect 의존성 트리거 안 함
|
|
112
|
+
- Server 사용 시 `initialState`와 `actionPayload`는 직렬화 가능해야 함
|
|
113
|
+
- `reducerAction`에서 throw 시 큐 취소 + Error Boundary 트리거 → throw 대신 에러 state 반환 권장
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Troubleshooting
|
|
118
|
+
|
|
119
|
+
| 증상 | 원인 | 해결 |
|
|
120
|
+
|------|------|------|
|
|
121
|
+
| `isPending`이 업데이트 안 됨 | `startTransition` 누락 | `dispatchAction`을 `startTransition` 안에서 호출 |
|
|
122
|
+
| form data 못 읽음 | 두 번째 파라미터가 `formData` | `(prevState, formData)` 시그니처 확인 |
|
|
123
|
+
| Action이 스킵됨 | `reducerAction`에서 throw | throw 대신 에러 state 반환 |
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: useFormStatus / form action={} 패턴
|
|
3
|
+
source: https://react.dev/reference/react-dom/hooks/useFormStatus
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
category: api
|
|
6
|
+
react_version: 19
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 개요
|
|
10
|
+
|
|
11
|
+
`useFormStatus`는 가장 가까운 부모 `<form>`의 제출 상태 정보를 제공하는 Hook.
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
const { pending, data, method, action } = useFormStatus();
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 반환값
|
|
20
|
+
|
|
21
|
+
| 속성 | 타입 | 설명 |
|
|
22
|
+
|------|------|------|
|
|
23
|
+
| `pending` | `boolean` | 부모 `<form>` 제출 중이면 `true` |
|
|
24
|
+
| `data` | `FormData \| null` | 제출 중인 폼 데이터; 없으면 `null` |
|
|
25
|
+
| `method` | `string` | HTTP 메서드 (`'get'` \| `'post'`, 기본 `'get'`) |
|
|
26
|
+
| `action` | `function \| null` | 부모 `<form>`의 `action` prop 함수; URI이거나 없으면 `null` |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 핵심 제약 (Critical)
|
|
31
|
+
|
|
32
|
+
> `useFormStatus`를 호출하는 컴포넌트는 반드시 `<form>` **내부**에 렌더링되어야 함.
|
|
33
|
+
> `<form>`을 렌더링하는 **같은 컴포넌트**에서는 호출 불가.
|
|
34
|
+
|
|
35
|
+
❌ 잘못된 패턴:
|
|
36
|
+
```js
|
|
37
|
+
function Form() {
|
|
38
|
+
const { pending } = useFormStatus(); // 🚩 pending은 절대 true가 되지 않음
|
|
39
|
+
return <form action={submit}></form>;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
✅ 올바른 패턴:
|
|
44
|
+
```js
|
|
45
|
+
function Submit() {
|
|
46
|
+
const { pending } = useFormStatus(); // ✅
|
|
47
|
+
return <button disabled={pending}>...</button>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function Form() {
|
|
51
|
+
return (
|
|
52
|
+
<form action={submit}>
|
|
53
|
+
<Submit />
|
|
54
|
+
</form>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 사용 패턴
|
|
62
|
+
|
|
63
|
+
### 제출 중 버튼 비활성화
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
import { useFormStatus } from "react-dom";
|
|
67
|
+
import action from './actions';
|
|
68
|
+
|
|
69
|
+
function Submit() {
|
|
70
|
+
const { pending } = useFormStatus();
|
|
71
|
+
return (
|
|
72
|
+
<button type="submit" disabled={pending}>
|
|
73
|
+
{pending ? "Submitting..." : "Submit"}
|
|
74
|
+
</button>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default function App() {
|
|
79
|
+
return (
|
|
80
|
+
<form action={action}>
|
|
81
|
+
<Submit />
|
|
82
|
+
</form>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 제출 중인 폼 데이터 표시
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
import { useFormStatus } from 'react-dom';
|
|
91
|
+
|
|
92
|
+
export default function UsernameForm() {
|
|
93
|
+
const { pending, data } = useFormStatus();
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div>
|
|
97
|
+
<h3>Request a Username: </h3>
|
|
98
|
+
<input type="text" name="username" disabled={pending} />
|
|
99
|
+
<button type="submit" disabled={pending}>Submit</button>
|
|
100
|
+
<p>{data ? `Requesting ${data?.get("username")}...` : ''}</p>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
import UsernameForm from './UsernameForm';
|
|
108
|
+
import { submitForm } from "./actions.js";
|
|
109
|
+
import { useRef } from 'react';
|
|
110
|
+
|
|
111
|
+
export default function App() {
|
|
112
|
+
const ref = useRef(null);
|
|
113
|
+
return (
|
|
114
|
+
<form
|
|
115
|
+
ref={ref}
|
|
116
|
+
action={async (formData) => {
|
|
117
|
+
await submitForm(formData);
|
|
118
|
+
ref.current.reset();
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
<UsernameForm />
|
|
122
|
+
</form>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## form action={} 패턴
|
|
130
|
+
|
|
131
|
+
React 19에서 `<form>`의 `action` prop에 함수를 직접 전달:
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
// Server Function을 직접 action으로 전달
|
|
135
|
+
import { updateName } from './actions'; // "use server" 함수
|
|
136
|
+
|
|
137
|
+
function UpdateName() {
|
|
138
|
+
return (
|
|
139
|
+
<form action={updateName}>
|
|
140
|
+
<input type="text" name="name" />
|
|
141
|
+
<button type="submit">Update</button>
|
|
142
|
+
</form>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- 성공 시 React가 자동으로 폼 리셋
|
|
148
|
+
- `useActionState`와 조합 시 pending state, 이전 응답 접근 가능
|
|
149
|
+
- JavaScript 로드 전에도 동작하는 점진적 향상 지원
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Troubleshooting
|
|
154
|
+
|
|
155
|
+
**`status.pending`이 항상 `false`인 경우:**
|
|
156
|
+
1. `useFormStatus` 호출 컴포넌트가 `<form>` 자식으로 렌더링되는지 확인
|
|
157
|
+
2. `<form>`을 렌더링하는 같은 컴포넌트에서 호출하지 않았는지 확인
|
|
158
|
+
3. 컴포넌트가 `<form>` 경계 내부에 있는지 확인
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: use() Hook
|
|
3
|
+
source: https://react.dev/reference/react/use
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
category: api
|
|
6
|
+
react_version: 19
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 개요
|
|
10
|
+
|
|
11
|
+
`use`는 Promise 또는 Context 값을 읽을 수 있는 React API.
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
const value = use(resource);
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**일반 Hook과의 차이점:**
|
|
18
|
+
- `if`, `for` 등 조건문·반복문 안에서도 호출 가능
|
|
19
|
+
- Component 또는 Hook 내부에서만 호출 가능
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 시그니처
|
|
24
|
+
|
|
25
|
+
### `use(resource)`
|
|
26
|
+
|
|
27
|
+
#### Parameters
|
|
28
|
+
- `resource`: 읽을 데이터 소스. Promise 또는 Context.
|
|
29
|
+
|
|
30
|
+
#### Returns
|
|
31
|
+
Promise의 resolved 값 또는 Context 값.
|
|
32
|
+
|
|
33
|
+
#### Caveats
|
|
34
|
+
1. Component 또는 Hook 안에서만 호출
|
|
35
|
+
2. Server Component에서는 `use` 대신 `async/await` 선호
|
|
36
|
+
3. Client Component에서 Promise를 생성하면 렌더마다 재생성됨 → Server Component에서 생성해 props로 전달
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 사용 패턴
|
|
41
|
+
|
|
42
|
+
### Context 읽기
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { use } from 'react';
|
|
46
|
+
|
|
47
|
+
function Button() {
|
|
48
|
+
const theme = use(ThemeContext);
|
|
49
|
+
// ...
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**조건부 호출 (일반 Hook은 불가, use는 가능):**
|
|
54
|
+
```js
|
|
55
|
+
function HorizontalRule({ show }) {
|
|
56
|
+
if (show) {
|
|
57
|
+
const theme = use(ThemeContext);
|
|
58
|
+
return <hr className={theme} />;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
> `use(context)`는 컴포넌트 자신이 아닌 가장 가까운 상위 Provider를 탐색.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### 서버→클라이언트 데이터 스트리밍
|
|
69
|
+
|
|
70
|
+
**Server Component:**
|
|
71
|
+
```js
|
|
72
|
+
import { fetchMessage } from './lib.js';
|
|
73
|
+
import { Message } from './message.js';
|
|
74
|
+
|
|
75
|
+
export default function App() {
|
|
76
|
+
const messagePromise = fetchMessage();
|
|
77
|
+
return (
|
|
78
|
+
<Suspense fallback={<p>waiting for message...</p>}>
|
|
79
|
+
<Message messagePromise={messagePromise} />
|
|
80
|
+
</Suspense>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Client Component:**
|
|
86
|
+
```js
|
|
87
|
+
// message.js
|
|
88
|
+
'use client';
|
|
89
|
+
|
|
90
|
+
import { use } from 'react';
|
|
91
|
+
|
|
92
|
+
export function Message({ messagePromise }) {
|
|
93
|
+
const messageContent = use(messagePromise);
|
|
94
|
+
return <p>Here is the message: {messageContent}</p>;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
> Promise resolved 값은 직렬화 가능해야 함 (함수 전달 불가).
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### 거부된 Promise 처리
|
|
103
|
+
|
|
104
|
+
**Error Boundary 사용:**
|
|
105
|
+
```js
|
|
106
|
+
import { use, Suspense } from "react";
|
|
107
|
+
import { ErrorBoundary } from "react-error-boundary";
|
|
108
|
+
|
|
109
|
+
export function MessageContainer({ messagePromise }) {
|
|
110
|
+
return (
|
|
111
|
+
<ErrorBoundary fallback={<p>⚠️Something went wrong</p>}>
|
|
112
|
+
<Suspense fallback={<p>⌛Downloading message...</p>}>
|
|
113
|
+
<Message messagePromise={messagePromise} />
|
|
114
|
+
</Suspense>
|
|
115
|
+
</ErrorBoundary>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Promise.catch 사용:**
|
|
121
|
+
```js
|
|
122
|
+
const messagePromise = new Promise((resolve, reject) => {
|
|
123
|
+
reject();
|
|
124
|
+
}).catch(() => {
|
|
125
|
+
return "no new message found.";
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
> `use`는 try-catch 블록 안에서 호출 불가. Error Boundary 또는 Promise.catch 사용.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Troubleshooting
|
|
134
|
+
|
|
135
|
+
**"Suspense Exception: This is not a real error!"**
|
|
136
|
+
- `use`를 React Component/Hook 외부에서 호출했거나
|
|
137
|
+
- try-catch 블록 안에서 호출한 경우
|
|
138
|
+
|
|
139
|
+
❌ 잘못된 예:
|
|
140
|
+
```jsx
|
|
141
|
+
function MessageComponent({ messagePromise }) {
|
|
142
|
+
function download() {
|
|
143
|
+
const message = use(messagePromise); // 중첩 함수 내부 → 오류
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
✅ 올바른 예:
|
|
149
|
+
```jsx
|
|
150
|
+
function MessageComponent({ messagePromise }) {
|
|
151
|
+
const message = use(messagePromise); // 컴포넌트 최상위 → 정상
|
|
152
|
+
}
|
|
153
|
+
```
|