@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,232 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Nuxt 3 데이터 페칭 (useFetch / useAsyncData / SSR hydration)
|
|
3
|
+
source: https://nuxt.com/docs/getting-started/data-fetching
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
category: nuxt
|
|
6
|
+
vue_version: 3
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Nuxt 3 데이터 페칭 완전 가이드
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 핵심 도구 3가지
|
|
14
|
+
|
|
15
|
+
| 도구 | 용도 |
|
|
16
|
+
|------|------|
|
|
17
|
+
| `$fetch` | 클라이언트 사이드 단순 요청 |
|
|
18
|
+
| `useFetch` | SSR-safe 초기 데이터 페칭 (most common) |
|
|
19
|
+
| `useAsyncData` | 세밀한 제어가 필요할 때 |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 왜 useFetch/useAsyncData가 필요한가?
|
|
24
|
+
|
|
25
|
+
유니버설 렌더링 환경에서 `$fetch`를 setup에서 직접 호출하면 서버와 클라이언트에서 **두 번 실행**된다. → hydration mismatch, 불필요한 네트워크 요청 발생.
|
|
26
|
+
|
|
27
|
+
`useFetch`/`useAsyncData`는 서버에서 페칭한 데이터를 payload에 담아 클라이언트로 전달하여 재요청을 방지한다.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
useNuxtApp().payload ← 서버 → 클라이언트 데이터 전달 경로
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## $fetch
|
|
36
|
+
|
|
37
|
+
```vue
|
|
38
|
+
<script setup lang="ts">
|
|
39
|
+
async function addTodo() {
|
|
40
|
+
const todo = await $fetch('/api/todos', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
body: { /* todo data */ }
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
</script>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**주의:** `$fetch` 단독 사용 시 deduplication과 navigation blocking이 없다. 초기 페이지 데이터에는 사용 금지.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## useFetch
|
|
53
|
+
|
|
54
|
+
```vue
|
|
55
|
+
<script setup lang="ts">
|
|
56
|
+
const { data: count } = await useFetch('/api/count')
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<template>
|
|
60
|
+
<p>Page visits: {{ count }}</p>
|
|
61
|
+
</template>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`useFetch(url)` ≈ `useAsyncData(url, () => event.$fetch(url))` — 가장 흔한 패턴의 syntactic sugar.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## useAsyncData
|
|
69
|
+
|
|
70
|
+
CMS나 서드파티 쿼리 레이어를 사용할 때 적합하다.
|
|
71
|
+
|
|
72
|
+
```vue
|
|
73
|
+
<script setup lang="ts">
|
|
74
|
+
const { data, error } = await useAsyncData('users', () => myGetFunction('users'))
|
|
75
|
+
</script>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**병렬 요청:**
|
|
79
|
+
```ts
|
|
80
|
+
const { data } = await useAsyncData((_nuxtApp, { signal }) => {
|
|
81
|
+
return Promise.all([
|
|
82
|
+
$fetch('/api/comments', { signal }),
|
|
83
|
+
$fetch('/api/author/12', { signal }),
|
|
84
|
+
])
|
|
85
|
+
})
|
|
86
|
+
const comments = computed(() => data.value?.[0])
|
|
87
|
+
const author = computed(() => data.value?.[1])
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**중요:** `useAsyncData`는 Pinia action 호출 등 사이드 이펙트 트리거용이 아니다. 반복 실행 위험이 있으므로 `callOnce`를 사용한다.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 반환값
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const {
|
|
98
|
+
data, // 결과 ref
|
|
99
|
+
refresh, // 수동 재요청
|
|
100
|
+
execute, // 수동 실행 (immediate: false 시)
|
|
101
|
+
clear, // data를 undefined로 리셋
|
|
102
|
+
error, // 에러 객체
|
|
103
|
+
status, // 'idle' | 'pending' | 'success' | 'error'
|
|
104
|
+
} = useFetch('/api/users')
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 주요 옵션
|
|
110
|
+
|
|
111
|
+
### lazy (비차단 페칭)
|
|
112
|
+
|
|
113
|
+
```vue
|
|
114
|
+
<script setup lang="ts">
|
|
115
|
+
const { status, data: posts } = useFetch('/api/posts', { lazy: true })
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<template>
|
|
119
|
+
<div v-if="status === 'pending'">Loading...</div>
|
|
120
|
+
<div v-else>...</div>
|
|
121
|
+
</template>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
단축형: `useLazyFetch`, `useLazyAsyncData`
|
|
125
|
+
|
|
126
|
+
### server: false (클라이언트 전용)
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const { data: comments } = useFetch('/api/comments', {
|
|
130
|
+
lazy: true,
|
|
131
|
+
server: false,
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### pick / transform (페이로드 최소화)
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// pick: 특정 필드만 선택
|
|
139
|
+
const { data: mountain } = await useFetch('/api/mountains/everest', {
|
|
140
|
+
pick: ['title', 'description'],
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// transform: 변환 함수
|
|
144
|
+
const { data: mountains } = await useFetch('/api/mountains', {
|
|
145
|
+
transform: (mountains) =>
|
|
146
|
+
mountains.map((m) => ({ title: m.title, description: m.description }))
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### watch (반응형 재실행)
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const id = ref(1)
|
|
154
|
+
const { data } = await useFetch('/api/users', { watch: [id] })
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### computed URL (동적 URL)
|
|
158
|
+
|
|
159
|
+
```vue
|
|
160
|
+
<script setup lang="ts">
|
|
161
|
+
const id = ref(null)
|
|
162
|
+
const { data, status } = useLazyFetch(() => `/api/users/${id.value}`, {
|
|
163
|
+
immediate: false,
|
|
164
|
+
})
|
|
165
|
+
</script>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### immediate: false (수동 실행)
|
|
169
|
+
|
|
170
|
+
```vue
|
|
171
|
+
<script setup lang="ts">
|
|
172
|
+
const { data, execute, status } = await useLazyFetch('/api/comments', {
|
|
173
|
+
immediate: false,
|
|
174
|
+
})
|
|
175
|
+
</script>
|
|
176
|
+
<template>
|
|
177
|
+
<div v-if="status === 'idle'">
|
|
178
|
+
<button @click="execute">Get data</button>
|
|
179
|
+
</div>
|
|
180
|
+
<div v-else-if="status === 'pending'">Loading...</div>
|
|
181
|
+
<div v-else>{{ data }}</div>
|
|
182
|
+
</template>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## SSR 헤더 & 쿠키
|
|
188
|
+
|
|
189
|
+
**브라우저 요청:** 쿠키가 `$fetch`에 자동 포함.
|
|
190
|
+
|
|
191
|
+
**서버 사이드:** `useFetch`는 `useRequestFetch`로 클라이언트 헤더/쿠키를 자동 프록시.
|
|
192
|
+
|
|
193
|
+
수동 전달:
|
|
194
|
+
```vue
|
|
195
|
+
<script setup lang="ts">
|
|
196
|
+
const headers = useRequestHeaders(['cookie'])
|
|
197
|
+
const user = await $fetch('/api/me', { headers })
|
|
198
|
+
</script>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**절대 프록시하면 안 되는 헤더:** `host`, `accept`, `content-length`, `content-md5`, `content-type`, `x-forwarded-*`, `cf-connecting-ip`, `cf-ray`
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 데이터 직렬화
|
|
206
|
+
|
|
207
|
+
### useAsyncData (서버→클라이언트): `devalue` 사용
|
|
208
|
+
- 지원 타입: 기본 JSON + RegExp, Date, Map, Set, Vue ref, NuxtError
|
|
209
|
+
|
|
210
|
+
### 서버 API 라우트: `JSON.stringify` 사용
|
|
211
|
+
- Date 등 복잡한 타입 직렬화 시 `toJSON()` 또는 `superjson` 활용 필요
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
// 서버
|
|
215
|
+
export default defineEventHandler(() => ({
|
|
216
|
+
createdAt: new Date(),
|
|
217
|
+
toJSON() {
|
|
218
|
+
return { createdAt: { year: this.createdAt.getFullYear(), /* ... */ } }
|
|
219
|
+
}
|
|
220
|
+
}))
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## 핵심 요약
|
|
226
|
+
|
|
227
|
+
1. 클라이언트 인터랙션 → `$fetch`
|
|
228
|
+
2. 초기 페이지 데이터 → `useFetch`
|
|
229
|
+
3. CMS/서드파티 API → `useAsyncData`
|
|
230
|
+
4. navigation 비차단 → `lazy: true` 또는 `useLazyFetch`
|
|
231
|
+
5. 캐시 키를 custom composable에서 명시적으로 지정
|
|
232
|
+
6. 동적 URL은 computed 함수 형태로 전달
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Pinia 상태 관리 패턴
|
|
3
|
+
source: https://pinia.vuejs.org/core-concepts/
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
category: state
|
|
6
|
+
vue_version: 3
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Pinia 상태 관리 핵심 개념
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Store 정의
|
|
14
|
+
|
|
15
|
+
`defineStore()`로 생성하며 고유한 id가 필요하다. 관례적으로 `use`로 시작하고 `Store`로 끝내는 이름을 사용한다.
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { defineStore } from 'pinia'
|
|
19
|
+
export const useAlertsStore = defineStore('alerts', { /* ... */ })
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 두 가지 문법
|
|
25
|
+
|
|
26
|
+
### Option Store (Options API 스타일)
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
export const useCounterStore = defineStore('counter', {
|
|
30
|
+
state: () => ({ count: 0, name: 'Eduardo' }),
|
|
31
|
+
getters: {
|
|
32
|
+
doubleCount: (state) => state.count * 2,
|
|
33
|
+
},
|
|
34
|
+
actions: {
|
|
35
|
+
increment() {
|
|
36
|
+
this.count++
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
| Pinia | Vue Options API |
|
|
43
|
+
|-------|----------------|
|
|
44
|
+
| `state` | `data` |
|
|
45
|
+
| `getters` | `computed` |
|
|
46
|
+
| `actions` | `methods` |
|
|
47
|
+
|
|
48
|
+
### Setup Store (Composition API 스타일)
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
export const useCounterStore = defineStore('counter', () => {
|
|
52
|
+
const count = ref(0)
|
|
53
|
+
const name = ref('Eduardo')
|
|
54
|
+
const doubleCount = computed(() => count.value * 2)
|
|
55
|
+
|
|
56
|
+
function increment() {
|
|
57
|
+
count.value++
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { count, name, doubleCount, increment }
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Composition API | Pinia 매핑 |
|
|
65
|
+
|-----------------|-----------|
|
|
66
|
+
| `ref()` | state |
|
|
67
|
+
| `computed()` | getters |
|
|
68
|
+
| `function` | actions |
|
|
69
|
+
|
|
70
|
+
**주의:** 모든 state 속성은 반드시 반환해야 한다. 미반환 시 SSR, devtools, 플러그인이 정상 동작하지 않는다.
|
|
71
|
+
|
|
72
|
+
Setup Store에서 외부 의존성 주입:
|
|
73
|
+
```ts
|
|
74
|
+
import { inject } from 'vue'
|
|
75
|
+
import { useRoute } from 'vue-router'
|
|
76
|
+
|
|
77
|
+
export const useSearchFilters = defineStore('search-filters', () => {
|
|
78
|
+
const route = useRoute()
|
|
79
|
+
const appProvided = inject('appProvided') // inject 값은 반환하지 않는다
|
|
80
|
+
return { /* ... */ }
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Store 사용
|
|
87
|
+
|
|
88
|
+
```vue
|
|
89
|
+
<script setup>
|
|
90
|
+
import { useCounterStore } from '@/stores/counter'
|
|
91
|
+
|
|
92
|
+
const store = useCounterStore()
|
|
93
|
+
</script>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
store 객체는 반응형이므로 `.value` 불필요. 단, 직접 구조 분해 시 반응성이 끊긴다.
|
|
97
|
+
|
|
98
|
+
```vue
|
|
99
|
+
<script setup>
|
|
100
|
+
const store = useCounterStore()
|
|
101
|
+
|
|
102
|
+
// ❌ 반응성 손실
|
|
103
|
+
const { name, doubleCount } = store
|
|
104
|
+
|
|
105
|
+
// ✅ 반응성 유지
|
|
106
|
+
const doubleValue = computed(() => store.doubleCount)
|
|
107
|
+
</script>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## storeToRefs()로 반응형 구조 분해
|
|
113
|
+
|
|
114
|
+
```vue
|
|
115
|
+
<script setup>
|
|
116
|
+
import { useCounterStore } from '@/stores/counter'
|
|
117
|
+
import { storeToRefs } from 'pinia'
|
|
118
|
+
|
|
119
|
+
const store = useCounterStore()
|
|
120
|
+
|
|
121
|
+
// state/getters: storeToRefs 사용
|
|
122
|
+
const { name, doubleCount } = storeToRefs(store)
|
|
123
|
+
|
|
124
|
+
// actions: 직접 구조 분해 가능
|
|
125
|
+
const { increment } = store
|
|
126
|
+
</script>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 문법 선택 가이드
|
|
132
|
+
|
|
133
|
+
- **Option Store**: 단순하고 입문자에게 친화적
|
|
134
|
+
- **Setup Store**: 복잡한 로직, watchers, composables 활용 시 더 강력하지만 SSR 복잡도 증가
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Vue 3 반응성 함정 (Reactivity Pitfalls)
|
|
3
|
+
source: https://vuejs.org/guide/extras/reactivity-in-depth.html
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
category: reactivity
|
|
6
|
+
vue_version: 3
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Vue 3 반응성 심층 가이드
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 반응성의 작동 원리
|
|
14
|
+
|
|
15
|
+
Vue는 두 가지 방식으로 객체 속성 접근을 가로챈다:
|
|
16
|
+
|
|
17
|
+
1. **Proxy** (Vue 3 `reactive()`)
|
|
18
|
+
2. **Getter/Setter** (Vue 3 `ref()`)
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
function reactive(obj) {
|
|
22
|
+
return new Proxy(obj, {
|
|
23
|
+
get(target, key) {
|
|
24
|
+
track(target, key) // 의존성 추적
|
|
25
|
+
return target[key]
|
|
26
|
+
},
|
|
27
|
+
set(target, key, value) {
|
|
28
|
+
target[key] = value
|
|
29
|
+
trigger(target, key) // 구독자 알림
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ref(value) {
|
|
35
|
+
const refObject = {
|
|
36
|
+
get value() {
|
|
37
|
+
track(refObject, 'value')
|
|
38
|
+
return value
|
|
39
|
+
},
|
|
40
|
+
set value(newValue) {
|
|
41
|
+
value = newValue
|
|
42
|
+
trigger(refObject, 'value')
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return refObject
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 핵심 메커니즘
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
let activeEffect // 현재 실행 중인 effect
|
|
53
|
+
|
|
54
|
+
function track(target, key) {
|
|
55
|
+
if (activeEffect) {
|
|
56
|
+
const effects = getSubscribersForProperty(target, key)
|
|
57
|
+
effects.add(activeEffect)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function trigger(target, key) {
|
|
62
|
+
const effects = getSubscribersForProperty(target, key)
|
|
63
|
+
effects.forEach((effect) => effect())
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## ref vs reactive 선택 가이드
|
|
70
|
+
|
|
71
|
+
| 상황 | 권장 |
|
|
72
|
+
|------|------|
|
|
73
|
+
| 원시값 (string, number, boolean) | `ref()` |
|
|
74
|
+
| 객체/배열의 전체 재할당 필요 | `ref()` |
|
|
75
|
+
| 객체의 속성만 변경 | `reactive()` 또는 `ref()` |
|
|
76
|
+
| 컴포저블/스토어 반환값 | `ref()` (구조 분해 가능) |
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 반응성 함정 (Pitfalls)
|
|
81
|
+
|
|
82
|
+
### 1. reactive() 구조 분해 시 반응성 손실
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
const state = reactive({ count: 0 })
|
|
86
|
+
|
|
87
|
+
// ❌ BROKEN: 구조 분해 후 반응성 끊김
|
|
88
|
+
const { count } = state
|
|
89
|
+
count++ // state.count에 반영되지 않음
|
|
90
|
+
|
|
91
|
+
// ✅ WORKS: 직접 접근
|
|
92
|
+
state.count++
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**비원시 값은 예외 (객체 참조 공유):**
|
|
96
|
+
```js
|
|
97
|
+
const state = reactive({ user: { name: 'John' } })
|
|
98
|
+
|
|
99
|
+
// ✅ WORKS: 객체 참조 공유이므로 변경이 추적됨
|
|
100
|
+
const { user } = state
|
|
101
|
+
user.name = 'Jane' // state.user.name도 변경됨
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 2. props 구조 분해 시 반응성 손실 (Vue 3.5 이전)
|
|
105
|
+
|
|
106
|
+
```vue
|
|
107
|
+
<script setup>
|
|
108
|
+
const props = defineProps({ count: Number })
|
|
109
|
+
|
|
110
|
+
// ❌ Vue 3.4 이하: 반응성 손실
|
|
111
|
+
const { count } = props
|
|
112
|
+
watchEffect(() => console.log(count)) // count 변경 감지 안 됨
|
|
113
|
+
|
|
114
|
+
// ✅ 올바른 방법
|
|
115
|
+
watchEffect(() => console.log(props.count))
|
|
116
|
+
|
|
117
|
+
// ✅ Vue 3.5+: 반응형 구조 분해 지원
|
|
118
|
+
const { count } = defineProps(['count']) // 이제 반응형
|
|
119
|
+
</script>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 3. Proxy 동일성 문제
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
const original = {}
|
|
126
|
+
const reactive_obj = reactive(original)
|
|
127
|
+
|
|
128
|
+
console.log(original === reactive_obj) // false ← Proxy이므로 다름
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**항상 reactive 버전으로만 작업해야 한다.**
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 반응성 디버깅
|
|
136
|
+
|
|
137
|
+
### 컴포넌트 디버그 훅
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
import { onRenderTracked, onRenderTriggered } from 'vue'
|
|
141
|
+
|
|
142
|
+
onRenderTracked((event) => {
|
|
143
|
+
debugger // 의존성 추적 시점
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
onRenderTriggered((event) => {
|
|
147
|
+
debugger // 의존성 변경 → 리렌더 시점
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**DebuggerEvent 구조:**
|
|
152
|
+
```ts
|
|
153
|
+
type DebuggerEvent = {
|
|
154
|
+
effect: ReactiveEffect
|
|
155
|
+
target: object
|
|
156
|
+
type: 'get' | 'has' | 'iterate' | 'set' | 'add' | 'delete' | 'clear'
|
|
157
|
+
key: any
|
|
158
|
+
newValue?: any
|
|
159
|
+
oldValue?: any
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### computed 디버깅
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
const plusOne = computed(() => count.value + 1, {
|
|
167
|
+
onTrack(e) {
|
|
168
|
+
debugger // 의존성 추적 시
|
|
169
|
+
},
|
|
170
|
+
onTrigger(e) {
|
|
171
|
+
debugger // 의존성 변경 시
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### watcher 디버깅
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
watch(source, callback, {
|
|
180
|
+
onTrack(e) { debugger },
|
|
181
|
+
onTrigger(e) { debugger }
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
watchEffect(callback, {
|
|
185
|
+
onTrack(e) { debugger },
|
|
186
|
+
onTrigger(e) { debugger }
|
|
187
|
+
})
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## 외부 상태 시스템 연동
|
|
193
|
+
|
|
194
|
+
외부 라이브러리 상태는 `shallowRef()`를 사용한다. 깊은 변환을 피한다.
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
import { shallowRef } from 'vue'
|
|
198
|
+
|
|
199
|
+
const externalState = shallowRef(externalLibrary.state)
|
|
200
|
+
// 외부 상태 변경 시 ref 값을 교체
|
|
201
|
+
externalState.value = newExternalState
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Immer와 연동:**
|
|
205
|
+
```js
|
|
206
|
+
import { produce } from 'immer'
|
|
207
|
+
import { shallowRef } from 'vue'
|
|
208
|
+
|
|
209
|
+
export function useImmer(baseState) {
|
|
210
|
+
const state = shallowRef(baseState)
|
|
211
|
+
const update = (updater) => {
|
|
212
|
+
state.value = produce(state.value, updater)
|
|
213
|
+
}
|
|
214
|
+
return [state, update]
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 런타임 vs 컴파일 타임 반응성
|
|
221
|
+
|
|
222
|
+
| 방식 | 예시 | 장점 | 단점 |
|
|
223
|
+
|------|------|------|------|
|
|
224
|
+
| **런타임** (Vue) | `ref()`, `reactive()` | 빌드 불필요, 엣지 케이스 적음 | `.value` 필요, JS 문법 제약 |
|
|
225
|
+
| **컴파일 타임** (Svelte) | `let count = 0` (컴파일러 변환) | 더 나은 ergonomics | 빌드 필수, JS 의미론 변경 |
|
|
226
|
+
|
|
227
|
+
Vue는 Reactivity Transform 실험 후 컴파일 타임 방식을 채택하지 않기로 결정했다.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Signal 패턴 (참고)
|
|
232
|
+
|
|
233
|
+
다른 프레임워크의 signal 패턴을 Vue로 구현할 수 있다.
|
|
234
|
+
|
|
235
|
+
**Solid.js 스타일:**
|
|
236
|
+
```js
|
|
237
|
+
import { shallowRef, triggerRef } from 'vue'
|
|
238
|
+
|
|
239
|
+
function createSignal(value, options) {
|
|
240
|
+
const r = shallowRef(value)
|
|
241
|
+
const get = () => r.value
|
|
242
|
+
const set = (v) => {
|
|
243
|
+
r.value = typeof v === 'function' ? v(r.value) : v
|
|
244
|
+
if (options?.equals === false) triggerRef(r)
|
|
245
|
+
}
|
|
246
|
+
return [get, set]
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Angular 스타일:**
|
|
251
|
+
```js
|
|
252
|
+
import { shallowRef } from 'vue'
|
|
253
|
+
|
|
254
|
+
function signal(initialValue) {
|
|
255
|
+
const r = shallowRef(initialValue)
|
|
256
|
+
const s = () => r.value
|
|
257
|
+
s.set = (value) => { r.value = value }
|
|
258
|
+
s.update = (updater) => { r.value = updater(r.value) }
|
|
259
|
+
return s
|
|
260
|
+
}
|
|
261
|
+
```
|