@kood/claude-code 0.6.6 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/dist/index.js +7 -1
  2. package/package.json +1 -1
  3. package/templates/.claude/agents/analyst.md +5 -0
  4. package/templates/.claude/agents/architect.md +5 -0
  5. package/templates/.claude/agents/build-fixer.md +1 -0
  6. package/templates/.claude/agents/code-reviewer.md +1 -0
  7. package/templates/.claude/agents/critic.md +4 -0
  8. package/templates/.claude/agents/deep-executor.md +1 -0
  9. package/templates/.claude/agents/dependency-manager.md +2 -0
  10. package/templates/.claude/agents/deployment-validator.md +2 -0
  11. package/templates/.claude/agents/designer.md +2 -0
  12. package/templates/.claude/agents/document-writer.md +3 -0
  13. package/templates/.claude/agents/explore.md +1 -0
  14. package/templates/.claude/agents/git-operator.md +2 -0
  15. package/templates/.claude/agents/implementation-executor.md +2 -0
  16. package/templates/.claude/agents/ko-to-en-translator.md +3 -0
  17. package/templates/.claude/agents/lint-fixer.md +2 -0
  18. package/templates/.claude/agents/planner.md +3 -0
  19. package/templates/.claude/agents/pm.md +349 -0
  20. package/templates/.claude/agents/qa-tester.md +1 -0
  21. package/templates/.claude/agents/refactor-advisor.md +4 -0
  22. package/templates/.claude/agents/researcher.md +9 -1
  23. package/templates/.claude/agents/scientist.md +1 -0
  24. package/templates/.claude/agents/security-reviewer.md +1 -0
  25. package/templates/.claude/agents/tdd-guide.md +1 -0
  26. package/templates/.claude/agents/vision.md +1 -0
  27. package/templates/.claude/instructions/agent-patterns/agent-teams-usage.md +376 -0
  28. package/templates/.claude/instructions/sourcing/reliable-search.md +49 -2
  29. package/templates/.claude/scripts/agent-teams/check-availability.sh +238 -0
  30. package/templates/.claude/scripts/agent-teams/setup-tmux.sh +125 -0
  31. package/templates/.claude/skills/agent-teams-setup/SKILL.md +460 -0
  32. package/templates/.claude/skills/brainstorm/SKILL.md +1 -0
  33. package/templates/.claude/skills/bug-fix/SKILL.md +1 -0
  34. package/templates/.claude/skills/crawler/SKILL.md +2 -0
  35. package/templates/.claude/skills/docs-creator/SKILL.md +1 -0
  36. package/templates/.claude/skills/docs-fetch/SKILL.md +6 -4
  37. package/templates/.claude/skills/docs-refactor/SKILL.md +1 -0
  38. package/templates/.claude/skills/elon-musk/SKILL.md +1 -0
  39. package/templates/.claude/skills/execute/SKILL.md +1 -0
  40. package/templates/.claude/skills/feedback/SKILL.md +1 -0
  41. package/templates/.claude/skills/figma-to-code/SKILL.md +1 -0
  42. package/templates/.claude/skills/genius-thinking/SKILL.md +1 -0
  43. package/templates/.claude/skills/global-uiux-design/SKILL.md +1 -0
  44. package/templates/.claude/skills/korea-uiux-design/SKILL.md +1 -0
  45. package/templates/.claude/skills/nextjs-react-best-practices/SKILL.md +1 -0
  46. package/templates/.claude/skills/plan/SKILL.md +1 -0
  47. package/templates/.claude/skills/prd/SKILL.md +1 -0
  48. package/templates/.claude/skills/project-optimizer/AGENTS.md +275 -0
  49. package/templates/.claude/skills/project-optimizer/SKILL.md +375 -0
  50. package/templates/.claude/skills/project-optimizer/rules/arch-config-centralize.md +66 -0
  51. package/templates/.claude/skills/project-optimizer/rules/arch-hot-path.md +35 -0
  52. package/templates/.claude/skills/project-optimizer/rules/arch-interface-segregation.md +51 -0
  53. package/templates/.claude/skills/project-optimizer/rules/arch-module-boundary.md +42 -0
  54. package/templates/.claude/skills/project-optimizer/rules/build-cache.md +57 -0
  55. package/templates/.claude/skills/project-optimizer/rules/build-code-split.md +56 -0
  56. package/templates/.claude/skills/project-optimizer/rules/build-incremental.md +65 -0
  57. package/templates/.claude/skills/project-optimizer/rules/build-minify.md +61 -0
  58. package/templates/.claude/skills/project-optimizer/rules/build-tree-shake.md +60 -0
  59. package/templates/.claude/skills/project-optimizer/rules/code-complexity.md +65 -0
  60. package/templates/.claude/skills/project-optimizer/rules/code-dead-elimination.md +32 -0
  61. package/templates/.claude/skills/project-optimizer/rules/code-duplication.md +54 -0
  62. package/templates/.claude/skills/project-optimizer/rules/code-error-handling.md +75 -0
  63. package/templates/.claude/skills/project-optimizer/rules/code-naming.md +52 -0
  64. package/templates/.claude/skills/project-optimizer/rules/concurrency-defer-await.md +54 -0
  65. package/templates/.claude/skills/project-optimizer/rules/concurrency-parallel.md +90 -0
  66. package/templates/.claude/skills/project-optimizer/rules/concurrency-pipeline.md +68 -0
  67. package/templates/.claude/skills/project-optimizer/rules/concurrency-pool.md +68 -0
  68. package/templates/.claude/skills/project-optimizer/rules/deps-lightweight-alt.md +37 -0
  69. package/templates/.claude/skills/project-optimizer/rules/deps-peer-align.md +44 -0
  70. package/templates/.claude/skills/project-optimizer/rules/deps-security-audit.md +45 -0
  71. package/templates/.claude/skills/project-optimizer/rules/deps-unused-removal.md +25 -0
  72. package/templates/.claude/skills/project-optimizer/rules/deps-version-pin.md +40 -0
  73. package/templates/.claude/skills/project-optimizer/rules/dx-ci-speed.md +47 -0
  74. package/templates/.claude/skills/project-optimizer/rules/dx-dev-server.md +35 -0
  75. package/templates/.claude/skills/project-optimizer/rules/dx-lint-config.md +36 -0
  76. package/templates/.claude/skills/project-optimizer/rules/dx-test-coverage.md +34 -0
  77. package/templates/.claude/skills/project-optimizer/rules/dx-type-safety.md +49 -0
  78. package/templates/.claude/skills/project-optimizer/rules/io-batch-queries.md +67 -0
  79. package/templates/.claude/skills/project-optimizer/rules/io-cache-layer.md +67 -0
  80. package/templates/.claude/skills/project-optimizer/rules/io-connection-reuse.md +67 -0
  81. package/templates/.claude/skills/project-optimizer/rules/io-serialize-minimal.md +61 -0
  82. package/templates/.claude/skills/project-optimizer/rules/io-stream.md +75 -0
  83. package/templates/.claude/skills/project-optimizer/rules/memory-bounded-cache.md +65 -0
  84. package/templates/.claude/skills/project-optimizer/rules/memory-large-data.md +64 -0
  85. package/templates/.claude/skills/project-optimizer/rules/memory-lazy-init.md +78 -0
  86. package/templates/.claude/skills/project-optimizer/rules/memory-leak-prevention.md +79 -0
  87. package/templates/.claude/skills/project-optimizer/rules/memory-pool-reuse.md +70 -0
  88. package/templates/.claude/skills/ralph/SKILL.md +1 -0
  89. package/templates/.claude/skills/refactor/SKILL.md +1 -0
  90. package/templates/.claude/skills/research/SKILL.md +1 -0
  91. package/templates/.claude/skills/sql-optimizer/SKILL.md +438 -0
  92. package/templates/.claude/skills/sql-optimizer/orm-patterns.md +218 -0
  93. package/templates/.claude/skills/startup-validator/SKILL.md +1 -0
  94. package/templates/.claude/skills/tanstack-start-react-best-practices/AGENTS.md +53 -14
  95. package/templates/.claude/skills/tanstack-start-react-best-practices/SKILL.md +94 -27
  96. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/bundle-defer-third-party.md +42 -19
  97. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/client-optimistic-updates.md +109 -0
  98. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/client-suspense-query.md +74 -0
  99. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/client-use-hook.md +81 -0
  100. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/rerender-react-compiler.md +81 -0
  101. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-beforeload-auth.md +121 -0
  102. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-file-conventions.md +104 -0
  103. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-link-navigation.md +119 -0
  104. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-nested-layouts.md +155 -0
  105. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-path-params.md +89 -0
  106. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-pending-component.md +110 -0
  107. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-preload-strategy.md +91 -0
  108. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-router-context.md +120 -0
  109. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-search-params.md +114 -0
  110. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-deferred-data.md +1 -1
  111. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-error-boundaries.md +79 -0
  112. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-middleware.md +85 -0
  113. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-serialization.md +56 -21
  114. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-streaming.md +84 -0
  115. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-validator.md +71 -0
  116. package/templates/.claude/skills/tauri-react-best-practices/AGENTS.md +527 -0
  117. package/templates/.claude/skills/tauri-react-best-practices/SKILL.md +571 -0
  118. package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-barrel-imports.md +140 -0
  119. package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-cargo-profile.md +96 -0
  120. package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-frontend-treeshake.md +242 -0
  121. package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-lazy-components.md +255 -0
  122. package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-remove-unused-commands.md +160 -0
  123. package/templates/.claude/skills/tauri-react-best-practices/rules/deploy-ci-pipeline.md +269 -0
  124. package/templates/.claude/skills/tauri-react-best-practices/rules/deploy-signing.md +207 -0
  125. package/templates/.claude/skills/tauri-react-best-practices/rules/deploy-updater.md +226 -0
  126. package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-async-commands.md +172 -0
  127. package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-batch-commands.md +133 -0
  128. package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-binary-response.md +198 -0
  129. package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-channel-streaming.md +186 -0
  130. package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-error-handling.md +250 -0
  131. package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-type-safe.md +227 -0
  132. package/templates/.claude/skills/tauri-react-best-practices/rules/perf-derived-state.md +231 -0
  133. package/templates/.claude/skills/tauri-react-best-practices/rules/perf-functional-setstate.md +191 -0
  134. package/templates/.claude/skills/tauri-react-best-practices/rules/perf-index-maps.md +276 -0
  135. package/templates/.claude/skills/tauri-react-best-practices/rules/perf-lazy-state-init.md +196 -0
  136. package/templates/.claude/skills/tauri-react-best-practices/rules/plugin-lifecycle.md +265 -0
  137. package/templates/.claude/skills/tauri-react-best-practices/rules/plugin-mobile-compat.md +199 -0
  138. package/templates/.claude/skills/tauri-react-best-practices/rules/plugin-permission-scope.md +193 -0
  139. package/templates/.claude/skills/tauri-react-best-practices/rules/react-error-boundary.md +239 -0
  140. package/templates/.claude/skills/tauri-react-best-practices/rules/react-event-listener.md +151 -0
  141. package/templates/.claude/skills/tauri-react-best-practices/rules/react-file-src.md +155 -0
  142. package/templates/.claude/skills/tauri-react-best-practices/rules/react-invoke-hook.md +139 -0
  143. package/templates/.claude/skills/tauri-react-best-practices/rules/react-optimistic-update.md +211 -0
  144. package/templates/.claude/skills/tauri-react-best-practices/rules/security-capability-split.md +205 -0
  145. package/templates/.claude/skills/tauri-react-best-practices/rules/security-csp.md +207 -0
  146. package/templates/.claude/skills/tauri-react-best-practices/rules/security-least-privilege.md +106 -0
  147. package/templates/.claude/skills/tauri-react-best-practices/rules/security-no-wildcard.md +253 -0
  148. package/templates/.claude/skills/tauri-react-best-practices/rules/security-scope-paths.md +160 -0
  149. package/templates/.claude/skills/tauri-react-best-practices/rules/state-async-mutex.md +270 -0
  150. package/templates/.claude/skills/tauri-react-best-practices/rules/state-mutex-pattern.md +265 -0
  151. package/templates/.claude/skills/tauri-react-best-practices/rules/state-react-sync.md +375 -0
  152. package/templates/.claude/skills/tauri-react-best-practices/rules/state-single-container.md +275 -0
  153. package/templates/tanstack-start/docs/architecture.md +238 -167
  154. package/templates/tanstack-start/docs/library/tanstack-router/error-handling.md +777 -38
  155. package/templates/tanstack-start/docs/library/tanstack-router/hooks.md +549 -37
  156. package/templates/tanstack-start/docs/library/tanstack-router/index.md +895 -111
  157. package/templates/tanstack-start/docs/library/tanstack-router/navigation.md +641 -43
  158. package/templates/tanstack-start/docs/library/tanstack-router/route-context.md +889 -38
  159. package/templates/tanstack-start/docs/library/tanstack-router/search-params.md +891 -29
  160. package/templates/tanstack-start/docs/library/tanstack-start/auth-patterns.md +972 -36
  161. package/templates/tanstack-start/docs/library/tanstack-start/index.md +1525 -881
  162. package/templates/tanstack-start/docs/library/tanstack-start/middleware.md +1099 -20
  163. package/templates/tanstack-start/docs/library/tanstack-start/routing.md +796 -30
  164. package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +953 -35
  165. package/templates/tanstack-start/docs/library/tanstack-start/setup.md +371 -15
  166. package/templates/tauri/CLAUDE.md +189 -0
  167. package/templates/tauri/docs/guides/distribution.md +261 -0
  168. package/templates/tauri/docs/guides/getting-started.md +302 -0
  169. package/templates/tauri/docs/guides/mobile.md +288 -0
  170. package/templates/tauri/docs/library/tauri/index.md +510 -0
@@ -1,74 +1,936 @@
1
1
  # TanStack Router - Search Params
2
2
 
3
- <patterns>
3
+ > TanStack Router v1.159.4
4
+
5
+ Search params 검증, 읽기, 쓰기, 직렬화를 다룬다.
6
+
7
+ ---
8
+
9
+ <why_not_url_search_params>
10
+
11
+ ## 왜 URLSearchParams를 직접 쓰지 않는가
12
+
13
+ TanStack Router는 JSON-first Search Params를 사용한다. URLSearchParams의 한계:
14
+
15
+ - 항상 문자열만 지원
16
+ - 대부분 flat 구조
17
+ - 중첩 객체/배열 지원 미흡
18
+ - 매번 파싱 시 참조 무결성 상실 (React 성능 이슈)
19
+
20
+ TanStack Router의 장점:
21
+
22
+ - 숫자, boolean 등 원시 타입 보존
23
+ - 중첩 객체/배열을 JSON으로 자동 직렬화
24
+ - 첫 번째 레벨은 URLSearchParams 호환 유지
25
+ - Structural Sharing으로 불필요한 리렌더 방지
26
+
27
+ ```tsx
28
+ // 이 네비게이션은:
29
+ <Link
30
+ to="/shop"
31
+ search={{
32
+ pageIndex: 3,
33
+ includeCategories: ['electronics', 'gifts'],
34
+ sortBy: 'price',
35
+ desc: true,
36
+ }}
37
+ />
38
+
39
+ // 이 URL을 생성:
40
+ // /shop?pageIndex=3&includeCategories=%5B%22electronics%22%2C%22gifts%22%5D&sortBy=price&desc=true
41
+
42
+ // 파싱하면 정확히 원래 JSON으로 복원:
43
+ // { pageIndex: 3, includeCategories: ["electronics", "gifts"], sortBy: "price", desc: true }
44
+ ```
45
+
46
+ </why_not_url_search_params>
47
+
48
+ ---
49
+
50
+ <zod_basic>
51
+
52
+ ## Zod 스키마: 기본 접근
53
+
54
+ validateSearch로 search params를 Zod 스키마로 검증. 타입 안전과 기본값 제공.
55
+
56
+ ### 스키마 정의
4
57
 
5
58
  ```tsx
6
- // Zod 스키마 + 라우트
59
+ import { z } from 'zod'
60
+
7
61
  const searchSchema = z.object({
8
- page: z.number().catch(1), // 기본값
9
- search: z.string().optional(), // 선택
62
+ // 기본값 (catch) - 유효하지 않은 값이면 기본값 사용
63
+ page: z.number().catch(1),
10
64
  sort: z.enum(['newest', 'price']).catch('newest'),
11
- tags: z.array(z.string()).catch([]), // 배열
12
- inStock: z.boolean().catch(true), // Boolean
13
- from: z.string().date().optional(), // 날짜
14
- minPrice: z.number().min(0).catch(0), // 범위
65
+
66
+ // 선택 필드
67
+ search: z.string().optional(),
68
+ category: z.string().optional(),
69
+
70
+ // 배열
71
+ tags: z.array(z.string()).catch([]),
72
+
73
+ // Boolean
74
+ inStock: z.boolean().catch(true),
75
+
76
+ // 날짜 (ISO 형식)
77
+ from: z.string().date().optional(),
78
+ to: z.string().date().optional(),
79
+
80
+ // 범위
81
+ minPrice: z.number().min(0).catch(0),
82
+ maxPrice: z.number().max(10000).optional(),
83
+ })
84
+ ```
85
+
86
+ ### 라우트에 적용
87
+
88
+ ```tsx
89
+ export const Route = createFileRoute('/products')({
90
+ validateSearch: searchSchema,
91
+ component: ProductsPage,
92
+ })
93
+
94
+ const ProductsPage = () => {
95
+ // 타입 안전: page, sort 타입 추론됨
96
+ const { page, sort, search } = Route.useSearch()
97
+
98
+ return <div>Page {page}, Sort: {sort}</div>
99
+ }
100
+ ```
101
+
102
+ ### catch() vs default() 선택
103
+
104
+ ```tsx
105
+ // catch(): 검증 실패 시 기본값 (에러 안 남, 권장)
106
+ page: z.number().catch(1)
107
+
108
+ // default(): 값이 undefined일 때 기본값 (검증 실패 시 에러)
109
+ page: z.number().default(1)
110
+ ```
111
+
112
+ > `catch()`를 권장하는 이유: search params가 잘못되었을 때 사용자 경험을 중단시키지 않기 위함. 에러를 보여줘야 한다면 `default()` 사용.
113
+
114
+ ### 검증 실패 시 에러 처리
115
+
116
+ `validateSearch`에서 에러가 throw되면 `error.routerCode`가 `VALIDATE_SEARCH`로 설정되고 `errorComponent`가 렌더링됨.
117
+
118
+ ```tsx
119
+ export const Route = createFileRoute('/products')({
120
+ validateSearch: z.object({
121
+ page: z.number().min(1), // catch 없으면 실패 시 에러
122
+ }),
123
+ errorComponent: ({ error }) => {
124
+ if (error.routerCode === 'VALIDATE_SEARCH') {
125
+ return <div>잘못된 검색 파라미터입니다.</div>
126
+ }
127
+ return <div>{error.message}</div>
128
+ },
129
+ component: ProductsPage,
130
+ })
131
+ ```
132
+
133
+ </zod_basic>
134
+
135
+ ---
136
+
137
+ <zod_adapter>
138
+
139
+ ## Zod Adapter: 고급 검증
140
+
141
+ `@tanstack/zod-adapter`의 `zodValidator()`를 사용하면 `input`/`output` 타입이 정확히 추론됨.
142
+
143
+ ### 설치
144
+
145
+ ```bash
146
+ npm install @tanstack/zod-adapter zod
147
+ ```
148
+
149
+ ### 기본 사용 (default와 함께)
150
+
151
+ ```tsx
152
+ import { zodValidator } from '@tanstack/zod-adapter'
153
+ import { z } from 'zod'
154
+
155
+ const searchSchema = z.object({
156
+ page: z.number().default(1),
157
+ filter: z.string().default(''),
158
+ sort: z.enum(['newest', 'oldest', 'price']).default('newest'),
159
+ })
160
+
161
+ export const Route = createFileRoute('/products')({
162
+ validateSearch: zodValidator(searchSchema),
163
+ component: ProductsPage,
164
+ })
165
+
166
+ // Link에서 search가 선택적으로 됨 (default 덕분)
167
+ <Link to="/products" /> // search 없어도 됨
168
+ ```
169
+
170
+ ### fallback(): 타입 유지 + 검증 실패 기본값
171
+
172
+ `catch()`는 타입을 `unknown`으로 만들 수 있어, adapter의 `fallback()`으로 타입 유지.
173
+
174
+ ```tsx
175
+ import { fallback, zodValidator } from '@tanstack/zod-adapter'
176
+ import { z } from 'zod'
177
+
178
+ const searchSchema = z.object({
179
+ page: fallback(z.number(), 1).default(1),
180
+ filter: fallback(z.string(), '').default(''),
181
+ sort: fallback(z.enum(['newest', 'oldest', 'price']), 'newest').default('newest'),
182
+ })
183
+
184
+ export const Route = createFileRoute('/products')({
185
+ validateSearch: zodValidator(searchSchema),
186
+ component: ProductsPage,
187
+ })
188
+ ```
189
+
190
+ ### input/output 타입 커스텀
191
+
192
+ ```tsx
193
+ // 네비게이션 시 input 타입과 읽기 시 output 타입을 다르게 설정
194
+ export const Route = createFileRoute('/products')({
195
+ validateSearch: zodValidator({
196
+ schema: searchSchema,
197
+ input: 'output', // 네비게이션 시 output 타입 사용
198
+ output: 'input', // 읽기 시 input 타입 사용
199
+ }),
200
+ })
201
+ ```
202
+
203
+ ### 에러 처리
204
+
205
+ ```tsx
206
+ import { SearchParamError } from '@tanstack/router'
207
+
208
+ export const Route = createFileRoute('/products')({
209
+ validateSearch: zodValidator(searchSchema),
210
+ // 검증 실패 시 처리
211
+ errorComponent: ({ error }) => (
212
+ <div>
213
+ <p>Invalid search params</p>
214
+ <pre>{error.message}</pre>
215
+ </div>
216
+ ),
217
+ component: ProductsPage,
218
+ })
219
+ ```
220
+
221
+ </zod_adapter>
222
+
223
+ ---
224
+
225
+ <valibot_support>
226
+
227
+ ## Valibot 지원 (v1.0+)
228
+
229
+ Valibot은 Standard Schema를 native로 지원. adapter 없이 바로 사용 가능.
230
+
231
+ ### 설치
232
+
233
+ ```bash
234
+ npm install valibot
235
+ ```
236
+
237
+ ### 기본 사용
238
+
239
+ ```tsx
240
+ import * as v from 'valibot'
241
+
242
+ const searchSchema = v.object({
243
+ page: v.optional(v.fallback(v.number(), 1), 1),
244
+ filter: v.optional(v.fallback(v.string(), ''), ''),
245
+ sort: v.optional(
246
+ v.fallback(v.picklist(['newest', 'oldest', 'price']), 'newest'),
247
+ 'newest',
248
+ ),
249
+ })
250
+
251
+ export const Route = createFileRoute('/products')({
252
+ validateSearch: searchSchema, // Standard Schema 자동 인식
253
+ component: ProductsPage,
254
+ })
255
+ ```
256
+
257
+ </valibot_support>
258
+
259
+ ---
260
+
261
+ <arktype_support>
262
+
263
+ ## ArkType 지원 (2.0-rc+)
264
+
265
+ ArkType도 Standard Schema를 구현. adapter 없이 사용 가능.
266
+
267
+ ```tsx
268
+ import { type } from 'arktype'
269
+
270
+ const searchSchema = type({
271
+ page: 'number = 1',
272
+ filter: 'string = ""',
273
+ sort: '"newest" | "oldest" | "price" = "newest"',
15
274
  })
16
275
 
17
276
  export const Route = createFileRoute('/products')({
18
277
  validateSearch: searchSchema,
19
- loaderDeps: ({ search }) => ({ search }), // search 변경 시 loader 재실행
20
- loader: async ({ deps: { search } }) => fetchProducts(search),
278
+ component: ProductsPage,
279
+ })
280
+ ```
281
+
282
+ </arktype_support>
283
+
284
+ ---
285
+
286
+ <loader_deps>
287
+
288
+ ## loaderDeps: Search 의존 Loader
289
+
290
+ search params가 변경될 때 loader를 자동으로 재실행.
291
+
292
+ 중요: **사용하는 deps만 포함해야 한다.** 불필요한 deps 추가 시 불필요한 재실행 발생.
293
+
294
+ ### 기본 예시
295
+
296
+ ```tsx
297
+ const postsSchema = z.object({
298
+ page: z.number().catch(1),
299
+ search: z.string().optional(),
300
+ category: z.string().optional(),
301
+ })
302
+
303
+ export const Route = createFileRoute('/posts')({
304
+ validateSearch: postsSchema,
305
+ // loaderDeps: search 변경 시 loader 재실행
306
+ loaderDeps: ({ search }) => ({ search }),
307
+ loader: async ({ deps: { search } }) => {
308
+ // search 변경될 때마다 재실행
309
+ return fetchPosts({
310
+ page: search.page,
311
+ query: search.search,
312
+ category: search.category,
313
+ })
314
+ },
315
+ component: PostsPage,
316
+ })
317
+ ```
318
+
319
+ ### params와 함께 사용
320
+
321
+ ```tsx
322
+ export const Route = createFileRoute('/users/$userId/posts')({
323
+ validateSearch: z.object({
324
+ page: z.number().catch(1),
325
+ }),
326
+ // 사용하는 deps만 포함
327
+ loaderDeps: ({ search, params }) => ({
328
+ page: search.page,
329
+ userId: params.userId,
330
+ }),
331
+ loader: async ({ deps: { page, userId } }) => {
332
+ return fetchUserPosts(userId, page)
333
+ },
334
+ component: UserPostsPage,
335
+ })
336
+
337
+ const UserPostsPage = () => {
338
+ const posts = Route.useLoaderData()
339
+ const { page } = Route.useSearch()
340
+ const { userId } = Route.useParams()
341
+
342
+ return <div>{posts.length} posts for user {userId}</div>
343
+ }
344
+ ```
345
+
346
+ ### 주의: 불필요한 deps 피하기
347
+
348
+ ```tsx
349
+ // 잘못된 예: 전체 search 반환 (안 쓰는 필드 변경에도 리로드)
350
+ loaderDeps: ({ search }) => search,
351
+ loader: ({ deps }) => fetchPosts({ page: deps.page }), // page만 쓰는데!
352
+
353
+ // 올바른 예: 사용하는 것만 포함
354
+ loaderDeps: ({ search }) => ({
355
+ page: search.page,
356
+ limit: search.limit,
357
+ }),
358
+ loader: ({ deps }) => fetchPosts(deps),
359
+ ```
360
+
361
+ </loader_deps>
362
+
363
+ ---
364
+
365
+ <reading>
366
+
367
+ ## Search Params 읽기
368
+
369
+ ### 컴포넌트에서 읽기 (Route.useSearch)
370
+
371
+ 라우트 내부에서 타입 안전하게 접근.
372
+
373
+ ```tsx
374
+ export const Route = createFileRoute('/products')({
375
+ validateSearch: z.object({
376
+ page: z.number().catch(1),
377
+ sort: z.enum(['newest', 'price']).catch('newest'),
378
+ }),
21
379
  component: ProductsPage,
22
380
  })
23
381
 
24
382
  const ProductsPage = () => {
25
- const { page, search, sort } = Route.useSearch()
26
- return <div>Page: {page}, Sort: {sort}</div>
383
+ // 타입 안전: page, sort 타입 자동 추론
384
+ const { page, sort } = Route.useSearch()
385
+
386
+ return (
387
+ <div>
388
+ <h1>Products</h1>
389
+ <p>Page {page}, Sorted by {sort}</p>
390
+ </div>
391
+ )
27
392
  }
393
+ ```
394
+
395
+ ### 부모 라우트의 Search Params 상속
396
+
397
+ 부모 라우트의 search params 타입이 자식 라우트에서도 접근 가능.
398
+
399
+ ```tsx
400
+ // /routes/shop/products.tsx
401
+ export const Route = createFileRoute('/shop/products')({
402
+ validateSearch: z.object({
403
+ page: z.number().catch(1),
404
+ sort: z.enum(['newest', 'price']).catch('newest'),
405
+ }),
406
+ })
407
+
408
+ // /routes/shop/products/$productId.tsx
409
+ export const Route = createFileRoute('/shop/products/$productId')({
410
+ beforeLoad: ({ search }) => {
411
+ search // ProductSearch 타입 상속됨
412
+ },
413
+ })
414
+ ```
415
+
416
+ ### Loader에서 읽기 (loaderDeps)
417
+
418
+ ```tsx
419
+ export const Route = createFileRoute('/products')({
420
+ validateSearch: z.object({
421
+ page: z.number().catch(1),
422
+ category: z.string().optional(),
423
+ }),
424
+ loaderDeps: ({ search }) => ({ search }),
425
+ loader: async ({ deps: { search } }) => {
426
+ // Loader에서 search 접근
427
+ const products = await fetchProducts({
428
+ page: search.page,
429
+ category: search.category,
430
+ })
431
+ return { products }
432
+ },
433
+ component: ProductsPage,
434
+ })
435
+ ```
436
+
437
+ ### 라우트 외부에서 읽기 (useSearch)
28
438
 
29
- // Link로 업데이트
30
- <Link to="/products" search={{ page: 1, sort: 'newest' }}>Reset</Link>
31
- <Link to="/products" search={prev => ({ ...prev, page: 2 })}>Next</Link>
439
+ 헤더, 사이드바 등 다른 라우트의 search params 접근.
32
440
 
33
- // useNavigate로 업데이트
441
+ ```tsx
442
+ // `/products` 라우트의 search params를 다른 곳에서 읽기
443
+ const Header = () => {
444
+ // from 명시로 타입 안전
445
+ const { search } = useSearch({ from: '/products' })
446
+
447
+ return <div>Search: {search}</div>
448
+ }
449
+
450
+ // 현재 라우트의 search params (무조건적)
451
+ const Sidebar = () => {
452
+ const search = useSearch({ strict: false }) // 모든 search 접근 가능
453
+ return <div>{JSON.stringify(search)}</div>
454
+ }
455
+
456
+ // getRouteApi로 파일 분리 시 타입 안전
457
+ import { getRouteApi } from '@tanstack/react-router'
458
+ const routeApi = getRouteApi('/products')
459
+
460
+ const Filter = () => {
461
+ const { page, sort } = routeApi.useSearch()
462
+ return <div>Page {page}</div>
463
+ }
464
+ ```
465
+
466
+ </reading>
467
+
468
+ ---
469
+
470
+ <writing>
471
+
472
+ ## Search Params 쓰기
473
+
474
+ ### Link로 업데이트
475
+
476
+ ```tsx
477
+ // 절대값 설정
478
+ <Link to="/products" search={{ page: 1, sort: 'newest' }}>
479
+ Reset
480
+ </Link>
481
+
482
+ // 함수로 이전 값 기반 업데이트
483
+ <Link to="/products" search={prev => ({ ...prev, page: 2 })}>
484
+ Next Page
485
+ </Link>
486
+
487
+ // from 지정으로 to 생략 (현재 페이지 search 업데이트)
488
+ <Link from={Route.fullPath} search={prev => ({ page: prev.page + 1 })}>
489
+ Next Page
490
+ </Link>
491
+
492
+ // to="."으로 느슨한 타입의 search 업데이트 (범용 컴포넌트)
493
+ <Link to="." search={prev => ({ ...prev, page: prev.page + 1 })}>
494
+ Next Page
495
+ </Link>
496
+ ```
497
+
498
+ ### useNavigate로 업데이트
499
+
500
+ ```tsx
34
501
  const Pagination = () => {
35
502
  const navigate = useNavigate()
36
503
  const { page } = Route.useSearch()
37
504
 
38
505
  const goToPage = (newPage: number) => {
39
- navigate({ to: '/products', search: prev => ({ ...prev, page: newPage }) })
506
+ navigate({
507
+ to: '/products',
508
+ search: prev => ({ ...prev, page: newPage }),
509
+ })
40
510
  }
41
511
 
42
512
  return (
43
513
  <div>
44
- <button onClick={() => goToPage(page - 1)} disabled={page <= 1}>Prev</button>
514
+ <button onClick={() => goToPage(page - 1)} disabled={page <= 1}>
515
+ Previous
516
+ </button>
45
517
  <span>Page {page}</span>
46
- <button onClick={() => goToPage(page + 1)}>Next</button>
518
+ <button onClick={() => goToPage(page + 1)}>
519
+ Next
520
+ </button>
47
521
  </div>
48
522
  )
49
523
  }
524
+ ```
525
+
526
+ ### router.navigate로 직접 업데이트
527
+
528
+ ```tsx
529
+ const Component = () => {
530
+ const router = useRouter()
531
+
532
+ const clearFilters = () => {
533
+ router.navigate({
534
+ to: '/products',
535
+ search: { page: 1, sort: 'newest' },
536
+ })
537
+ }
538
+
539
+ return <button onClick={clearFilters}>Clear Filters</button>
540
+ }
541
+ ```
542
+
543
+ </writing>
544
+
545
+ ---
546
+
547
+ <search_middleware>
548
+
549
+ ## Search Middleware: 자동 변환
550
+
551
+ Search params를 링크 생성 시 및 네비게이션 후 자동 변환.
552
+
553
+ ### retainSearchParams: search 유지
554
+
555
+ 특정 search params를 모든 하위 링크에서 자동 유지.
556
+
557
+ ```tsx
558
+ import { retainSearchParams } from '@tanstack/react-router'
559
+ import { zodValidator } from '@tanstack/zod-adapter'
560
+ import { z } from 'zod'
561
+
562
+ const searchSchema = z.object({
563
+ rootValue: z.string().optional(),
564
+ })
565
+
566
+ export const Route = createRootRoute({
567
+ validateSearch: zodValidator(searchSchema),
568
+ search: {
569
+ middlewares: [retainSearchParams(['rootValue'])],
570
+ },
571
+ })
572
+ ```
573
+
574
+ ### stripSearchParams: 기본값 제거
575
+
576
+ 기본값과 동일한 search params를 URL에서 자동 제거 (깔끔한 URL).
577
+
578
+ ```tsx
579
+ import { stripSearchParams } from '@tanstack/react-router'
580
+ import { zodValidator } from '@tanstack/zod-adapter'
581
+ import { z } from 'zod'
582
+
583
+ const defaultValues = {
584
+ one: 'abc',
585
+ two: 'xyz',
586
+ }
587
+
588
+ const searchSchema = z.object({
589
+ one: z.string().default(defaultValues.one),
590
+ two: z.string().default(defaultValues.two),
591
+ })
592
+
593
+ export const Route = createFileRoute('/hello')({
594
+ validateSearch: zodValidator(searchSchema),
595
+ search: {
596
+ middlewares: [stripSearchParams(defaultValues)],
597
+ },
598
+ })
599
+ ```
600
+
601
+ ### 여러 미들웨어 체이닝
602
+
603
+ ```tsx
604
+ export const Route = createFileRoute('/search')({
605
+ validateSearch: zodValidator(
606
+ z.object({
607
+ retainMe: z.string().optional(),
608
+ arrayWithDefaults: z.string().array().default(['foo', 'bar']),
609
+ required: z.string(),
610
+ }),
611
+ ),
612
+ search: {
613
+ middlewares: [
614
+ retainSearchParams(['retainMe']),
615
+ stripSearchParams({ arrayWithDefaults: ['foo', 'bar'] }),
616
+ ],
617
+ },
618
+ })
619
+ ```
620
+
621
+ ### 커스텀 미들웨어
622
+
623
+ ```tsx
624
+ export const Route = createRootRoute({
625
+ validateSearch: zodValidator(searchSchema),
626
+ search: {
627
+ middlewares: [
628
+ ({ search, next }) => {
629
+ const result = next(search)
630
+ return {
631
+ rootValue: search.rootValue, // 항상 유지
632
+ ...result,
633
+ }
634
+ },
635
+ ],
636
+ },
637
+ })
638
+ ```
639
+
640
+ ### beforeLoad에서 search 정규화
641
+
642
+ ```tsx
643
+ export const Route = createFileRoute('/products')({
644
+ validateSearch: z.object({
645
+ page: z.number().catch(1),
646
+ sortBy: z.enum(['asc', 'desc']).catch('asc'),
647
+ }),
648
+ // beforeLoad에서 search 정규화
649
+ beforeLoad: async ({ search }) => {
650
+ // search.page가 음수면 1로 고정
651
+ const sanitized = {
652
+ ...search,
653
+ page: Math.max(1, search.page),
654
+ }
655
+ return { sanitized }
656
+ },
657
+ loader: async ({ context }) => {
658
+ // context.sanitized 사용
659
+ return fetchProducts(context.sanitized)
660
+ },
661
+ component: ProductsPage,
662
+ })
663
+ ```
664
+
665
+ </search_middleware>
666
+
667
+ ---
668
+
669
+ <custom_serialization>
670
+
671
+ ## 커스텀 직렬화
672
+
673
+ 기본 JSON.stringify/JSON.parse 대신 다른 직렬화 라이브러리 사용 가능.
674
+
675
+ ### Base64 인코딩
676
+
677
+ ```tsx
678
+ import { parseSearchWith, stringifySearchWith } from '@tanstack/react-router'
679
+
680
+ const router = createRouter({
681
+ routeTree,
682
+ parseSearch: parseSearchWith(value => JSON.parse(decodeFromBinary(value))),
683
+ stringifySearch: stringifySearchWith(value => encodeToBinary(JSON.stringify(value))),
684
+ })
685
+
686
+ // 안전한 바이너리 인코딩/디코딩
687
+ function encodeToBinary(str: string): string {
688
+ return btoa(
689
+ encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (_, p1) =>
690
+ String.fromCharCode(parseInt(p1, 16)),
691
+ ),
692
+ )
693
+ }
694
+
695
+ function decodeFromBinary(str: string): string {
696
+ return decodeURIComponent(
697
+ Array.prototype.map
698
+ .call(atob(str), (c) =>
699
+ '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2),
700
+ )
701
+ .join(''),
702
+ )
703
+ }
704
+ ```
705
+
706
+ ### JSURL2 (압축 + 가독성)
707
+
708
+ ```tsx
709
+ import { parse, stringify } from 'jsurl2'
710
+
711
+ const router = createRouter({
712
+ parseSearch: parseSearchWith(parse),
713
+ stringifySearch: stringifySearchWith(stringify),
714
+ })
715
+ // 결과: ?filters=(author~tanner~min*_words~800)~
716
+ ```
717
+
718
+ ### query-string
719
+
720
+ ```tsx
721
+ import qs from 'query-string'
722
+
723
+ const router = createRouter({
724
+ stringifySearch: stringifySearchWith(value => qs.stringify(value)),
725
+ parseSearch: parseSearchWith(value => qs.parse(value)),
726
+ })
727
+ ```
728
+
729
+ </custom_serialization>
730
+
731
+ ---
732
+
733
+ <error_handling>
734
+
735
+ ## 에러 처리
736
+
737
+ ### routerCode 활용
738
+
739
+ ```tsx
740
+ export const Route = createFileRoute('/products')({
741
+ validateSearch: z.object({
742
+ page: z.number().min(1).catch(1),
743
+ }),
744
+ errorComponent: ({ error }) => {
745
+ // routerCode로 에러 타입 구분
746
+ if (error.routerCode === 'VALIDATE_SEARCH') {
747
+ return <div>Invalid search parameters. Using defaults.</div>
748
+ }
749
+ return <div>{error.message}</div>
750
+ },
751
+ component: ProductsPage,
752
+ })
753
+ ```
754
+
755
+ ### 유효하지 않은 값 처리
756
+
757
+ ```tsx
758
+ const searchSchema = z.object({
759
+ page: z
760
+ .number()
761
+ .int()
762
+ .positive()
763
+ .catch(1), // 유효하지 않으면 1로 설정
764
+
765
+ sort: z
766
+ .enum(['newest', 'price'])
767
+ .catch('newest'), // 유효하지 않은 enum 값이면 기본값
768
+ })
769
+
770
+ // 또는 명시적 검증
771
+ const strictSearchSchema = z.object({
772
+ page: z.number().int().positive(), // 실패 시 에러
773
+ sort: z.enum(['newest', 'price']),
774
+ }).catch({ page: 1, sort: 'newest' })
775
+ ```
776
+
777
+ </error_handling>
778
+
779
+ ---
780
+
781
+ <practical_example>
782
+
783
+ ## 실전: 필터 + 정렬 + 페이지네이션
784
+
785
+ 완전한 구현 예시.
786
+
787
+ ### 스키마 정의
788
+
789
+ ```tsx
790
+ // /src/routes/posts/-search-schema.ts
791
+ import { z } from 'zod'
792
+
793
+ export const postsSearchSchema = z.object({
794
+ page: z.number().int().positive().catch(1),
795
+ search: z.string().optional(),
796
+ category: z.enum(['tech', 'lifestyle', 'business']).optional(),
797
+ sort: z.enum(['newest', 'oldest', 'popular']).catch('newest'),
798
+ tags: z.array(z.string()).catch([]),
799
+ })
800
+
801
+ export type PostsSearch = z.infer<typeof postsSearchSchema>
802
+ ```
803
+
804
+ ### 라우트 정의
805
+
806
+ ```tsx
807
+ // /src/routes/posts/index.tsx
808
+ import { createFileRoute } from '@tanstack/react-router'
809
+ import { postsSearchSchema } from './-search-schema'
810
+
811
+ export const Route = createFileRoute('/posts')({
812
+ validateSearch: postsSearchSchema,
813
+ loaderDeps: ({ search }) => ({ search }),
814
+ loader: async ({ deps: { search } }) => {
815
+ const posts = await fetchPosts({
816
+ page: search.page,
817
+ query: search.search,
818
+ category: search.category,
819
+ sort: search.sort,
820
+ tags: search.tags,
821
+ })
822
+ return posts
823
+ },
824
+ component: PostsPage,
825
+ })
50
826
 
51
- // 실전: 필터 + 정렬 + 페이지네이션
52
827
  const PostsPage = () => {
53
- const { page, search, category, sort } = Route.useSearch()
54
828
  const posts = Route.useLoaderData()
829
+ const { page, search, category, sort } = Route.useSearch()
55
830
  const navigate = useNavigate()
56
831
 
57
- const updateSearch = (updates: Partial<z.infer<typeof searchSchema>>) => {
58
- navigate({ to: '/posts', search: prev => ({ ...prev, ...updates, page: 1 }) })
832
+ const updateSearch = (updates: Partial<PostsSearch>) => {
833
+ navigate({
834
+ to: '/posts',
835
+ search: prev => ({ ...prev, ...updates, page: 1 }),
836
+ })
59
837
  }
60
838
 
61
839
  return (
62
- <div>
63
- <input value={search} onChange={e => updateSearch({ search: e.target.value })} />
64
- <select value={category} onChange={e => updateSearch({ category: e.target.value as any })}>
65
- <option value="all">All</option>
840
+ <div className="space-y-6">
841
+ {/* 검색 입력 */}
842
+ <input
843
+ type="text"
844
+ value={search ?? ''}
845
+ onChange={e => updateSearch({ search: e.target.value })}
846
+ placeholder="Search posts..."
847
+ />
848
+
849
+ {/* 카테고리 필터 */}
850
+ <select
851
+ value={category ?? ''}
852
+ onChange={e => updateSearch({ category: e.target.value as any })}
853
+ >
854
+ <option value="">All Categories</option>
66
855
  <option value="tech">Tech</option>
856
+ <option value="lifestyle">Lifestyle</option>
857
+ <option value="business">Business</option>
67
858
  </select>
68
- {posts.map(post => <div key={post.id}>{post.title}</div>)}
859
+
860
+ {/* 정렬 선택 */}
861
+ <select
862
+ value={sort}
863
+ onChange={e => updateSearch({ sort: e.target.value as any })}
864
+ >
865
+ <option value="newest">Newest</option>
866
+ <option value="oldest">Oldest</option>
867
+ <option value="popular">Popular</option>
868
+ </select>
869
+
870
+ {/* 태그 필터 (여러 선택) */}
871
+ <div>
872
+ {['react', 'typescript', 'design'].map(tag => (
873
+ <label key={tag}>
874
+ <input
875
+ type="checkbox"
876
+ checked={tags.includes(tag)}
877
+ onChange={e => {
878
+ const newTags = e.target.checked
879
+ ? [...tags, tag]
880
+ : tags.filter(t => t !== tag)
881
+ updateSearch({ tags: newTags })
882
+ }}
883
+ />
884
+ {tag}
885
+ </label>
886
+ ))}
887
+ </div>
888
+
889
+ {/* 포스트 목록 */}
890
+ <div>
891
+ {posts.map(post => (
892
+ <Link key={post.id} to={`/posts/${post.id}`}>
893
+ <h3>{post.title}</h3>
894
+ </Link>
895
+ ))}
896
+ </div>
897
+
898
+ {/* 페이지네이션 */}
899
+ <div className="flex gap-2">
900
+ <button
901
+ onClick={() => updateSearch({ page: page - 1 })}
902
+ disabled={page <= 1}
903
+ >
904
+ Previous
905
+ </button>
906
+ <span>Page {page}</span>
907
+ <button onClick={() => updateSearch({ page: page + 1 })}>
908
+ Next
909
+ </button>
910
+ </div>
69
911
  </div>
70
912
  )
71
913
  }
72
914
  ```
73
915
 
74
- </patterns>
916
+ </practical_example>
917
+
918
+ ---
919
+
920
+ <dos_donts>
921
+
922
+ ## Do's & Don'ts
923
+
924
+ | Do | Don't |
925
+ |-------|---------|
926
+ | `validateSearch` 스키마 정의 | 검증 없이 search params 사용 |
927
+ | `Route.useSearch()` (타입 안전) | `useSearch()` + 수동 타입 지정 |
928
+ | `catch()` 또는 `.optional()` 사용 | 필수 필드만 넣기 (URL 없을 수 있음) |
929
+ | `loaderDeps`에 사용하는 값만 포함 | 모든 search 포함 (불필요한 재실행) |
930
+ | 함수로 이전 값 기반 업데이트 | 값을 하드코딩해서 다른 params 덮어쓰기 |
931
+ | `z.enum()` 또는 `z.picklist()` | 문자열 비교로 유효성 확인 |
932
+ | Zod adapter (`zodValidator`) + `fallback()` | raw Zod의 `catch()`로 타입 손실 |
933
+ | `retainSearchParams` / `stripSearchParams` 미들웨어 | search params 수동 전파 |
934
+ | Standard Schema 라이브러리 (Valibot, ArkType) | adapter 없는 복잡한 타입 조작 |
935
+
936
+ </dos_donts>