@kood/claude-code 0.3.8 → 0.3.10

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 (37) hide show
  1. package/dist/index.js +1 -1
  2. package/package.json +1 -1
  3. package/templates/.claude/agents/code-reviewer.md +16 -1
  4. package/templates/.claude/agents/dependency-manager.md +16 -1
  5. package/templates/.claude/agents/deployment-validator.md +16 -1
  6. package/templates/.claude/agents/git-operator.md +16 -1
  7. package/templates/.claude/agents/implementation-executor.md +16 -1
  8. package/templates/.claude/agents/lint-fixer.md +16 -1
  9. package/templates/.claude/agents/refactor-advisor.md +16 -1
  10. package/templates/.claude/commands/agent-creator.md +16 -1
  11. package/templates/.claude/commands/bug-fix.md +16 -1
  12. package/templates/.claude/commands/command-creator.md +17 -1
  13. package/templates/.claude/commands/docs-creator.md +17 -1
  14. package/templates/.claude/commands/docs-refactor.md +17 -1
  15. package/templates/.claude/commands/execute.md +17 -1
  16. package/templates/.claude/commands/git-all.md +16 -1
  17. package/templates/.claude/commands/git-session.md +17 -1
  18. package/templates/.claude/commands/git.md +17 -1
  19. package/templates/.claude/commands/lint-fix.md +17 -1
  20. package/templates/.claude/commands/lint-init.md +17 -1
  21. package/templates/.claude/commands/plan.md +17 -1
  22. package/templates/.claude/commands/prd.md +17 -1
  23. package/templates/.claude/commands/pre-deploy.md +17 -1
  24. package/templates/.claude/commands/refactor.md +17 -1
  25. package/templates/.claude/commands/version-update.md +17 -1
  26. package/templates/hono/CLAUDE.md +1 -0
  27. package/templates/nextjs/CLAUDE.md +12 -9
  28. package/templates/nextjs/docs/architecture.md +812 -0
  29. package/templates/npx/CLAUDE.md +1 -0
  30. package/templates/tanstack-start/CLAUDE.md +1 -0
  31. package/templates/tanstack-start/docs/library/better-auth/index.md +225 -185
  32. package/templates/tanstack-start/docs/library/prisma/index.md +1025 -41
  33. package/templates/tanstack-start/docs/library/t3-env/index.md +207 -40
  34. package/templates/tanstack-start/docs/library/tanstack-query/index.md +878 -42
  35. package/templates/tanstack-start/docs/library/tanstack-router/index.md +602 -54
  36. package/templates/tanstack-start/docs/library/tanstack-start/index.md +1334 -33
  37. package/templates/tanstack-start/docs/library/zod/index.md +674 -31
@@ -1,22 +1,107 @@
1
1
  # TanStack Router
2
2
 
3
- > 1.x | Type-safe React Router
3
+ > v1 | Type-safe React Router
4
4
 
5
- @navigation.md
6
- @search-params.md
7
- @route-context.md
8
- @hooks.md
9
- @error-handling.md
5
+ ---
6
+
7
+ <context>
8
+
9
+ **Purpose:** File-based routing with full TypeScript type safety for TanStack Start
10
+
11
+ **Version:** v1
12
+
13
+ **Key Features:**
14
+ - File-based routing (`__root.tsx`, `$param.tsx`, `_layout/`)
15
+ - Type-safe hooks (`Route.useLoaderData()`, `Route.useParams()`)
16
+ - Data loading (`loader`, `beforeLoad`)
17
+ - Search params validation (Zod integration)
18
+ - Route context for auth/state sharing
19
+ - Error boundaries and pending states
20
+
21
+ </context>
10
22
 
11
23
  ---
12
24
 
13
- <quick_reference>
25
+ <forbidden>
26
+
27
+ | 분류 | 금지 행동 | 이유 |
28
+ |------|----------|------|
29
+ | **Hooks** | `useParams()`/`useSearch()` 타입 수동 지정 | `Route.useParams()`/`Route.useSearch()` 사용 (자동 타입) |
30
+ | **Search Params** | validateSearch 없이 search params 사용 | 타입 안전성 보장 불가 |
31
+ | **Navigation** | `window.location.href` 사용 | `<Link>` 또는 `useNavigate()` 사용 |
32
+ | **Context** | Context 없이 전역 상태 prop drilling | Route context 또는 Zustand 사용 |
33
+ | **Error** | try-catch로 에러 처리 | `errorComponent` 사용 |
34
+
35
+ </forbidden>
36
+
37
+ ---
38
+
39
+ <required>
40
+
41
+ | 작업 | 필수 행동 |
42
+ |------|----------|
43
+ | **Search Params** | Zod 스키마로 `validateSearch` 정의 |
44
+ | **Type-safe Hooks** | `Route.useLoaderData()`, `Route.useParams()`, `Route.useSearch()` 사용 |
45
+ | **Protected Routes** | `_authed.tsx` + `beforeLoad`에서 인증 체크 |
46
+ | **Data Loading** | `loader`에서 데이터 fetch, TanStack Query와 통합 |
47
+ | **Error Handling** | `errorComponent`, `notFoundComponent` 정의 |
48
+ | **Navigation** | `<Link>`에 `params`, `search` 타입 안전하게 전달 |
49
+
50
+ </required>
51
+
52
+ ---
53
+
54
+ <structure>
55
+
56
+ ```
57
+ routes/
58
+ ├── __root.tsx # Root layout (createRootRoute)
59
+ ├── index.tsx # /
60
+ ├── about.tsx # /about
61
+ ├── posts/
62
+ │ ├── index.tsx # /posts
63
+ │ └── $postId.tsx # /posts/:postId
64
+ ├── _authed/ # Protected (pathless)
65
+ │ ├── dashboard.tsx # /dashboard
66
+ │ └── settings.tsx # /settings
67
+ └── $.tsx # Catch-all (404)
68
+ ```
69
+
70
+ | 파일명 | 경로 | 설명 |
71
+ |--------|------|------|
72
+ | `__root.tsx` | - | Root layout (필수) |
73
+ | `index.tsx` | `/` | 디렉토리 루트 |
74
+ | `about.tsx` | `/about` | 정적 라우트 |
75
+ | `$postId.tsx` | `/posts/:postId` | 동적 세그먼트 |
76
+ | `_authed/dashboard.tsx` | `/dashboard` | Pathless layout (인증 등) |
77
+ | `$.tsx` | `/*` | Catch-all (404) |
78
+
79
+ </structure>
80
+
81
+ ---
82
+
83
+ <route_options>
84
+
85
+ | 옵션 | 타입 | 설명 |
86
+ |------|------|------|
87
+ | `component` | Component | 렌더링할 컴포넌트 |
88
+ | `loader` | async function | 데이터 로드 (SSR/CSR 모두) |
89
+ | `beforeLoad` | async function | loader 전 실행 (인증, context 추가) |
90
+ | `validateSearch` | Zod schema | Search params 검증 + 타입 추론 |
91
+ | `loaderDeps` | function | search/params 변경 시 loader 재실행 |
92
+ | `errorComponent` | Component | Error throw 시 표시 |
93
+ | `notFoundComponent` | Component | notFound() throw 시 표시 |
94
+ | `pendingComponent` | Component | loader 실행 중 표시 |
95
+ | `pendingMs` | number | pendingComponent 표시 지연 (기본 1000ms) |
96
+ | `pendingMinMs` | number | pendingComponent 최소 표시 시간 (깜빡임 방지) |
14
97
 
15
98
  ```tsx
16
- // Basic route
17
- export const Route = createFileRoute('/about')({ component: AboutPage })
99
+ // 기본 라우트
100
+ export const Route = createFileRoute('/about')({
101
+ component: AboutPage,
102
+ })
18
103
 
19
- // Loader + dynamic params
104
+ // Loader + 동적 파라미터
20
105
  export const Route = createFileRoute('/posts/$postId')({
21
106
  loader: async ({ params }) => ({ post: await getPost(params.postId) }),
22
107
  component: PostPage,
@@ -50,66 +135,529 @@ const RootLayout = () => (
50
135
  <main><Outlet /></main>
51
136
  </div>
52
137
  )
138
+ ```
53
139
 
54
- // Navigation
55
- <Link to="/posts/$postId" params={{ postId: '123' }}>Post</Link>
56
- <Link to="/products" search={{ page: 1 }}>Products</Link>
140
+ </route_options>
57
141
 
58
- const navigate = useNavigate()
59
- navigate({ to: '/posts/$postId', params: { postId: '123' } })
60
- navigate({ to: '/products', search: prev => ({ ...prev, page: 2 }) })
142
+ ---
143
+
144
+ <navigation>
145
+
146
+ ## Link Component
147
+
148
+ ```tsx
149
+ // 기본 사용법
150
+ <Link to="/about">About</Link>
151
+ <Link to="/posts/$postId" params={{ postId: '123' }}>Post 123</Link>
152
+ <Link to="/products" search={{ page: 1, sort: 'newest' }}>Products</Link>
153
+ <Link to="/products" search={prev => ({ ...prev, page: 2 })}>Next</Link>
154
+
155
+ // Active 스타일
156
+ <Link
157
+ to="/about"
158
+ activeProps={{ className: 'text-blue-500 font-bold' }}
159
+ inactiveProps={{ className: 'text-gray-500' }}
160
+ >
161
+ About
162
+ </Link>
163
+ <Link to="/" activeOptions={{ exact: true }}>Home</Link>
164
+
165
+ // Preloading
166
+ <Link to="/posts" preload="intent">Posts</Link> // hover 시
167
+ <Link to="/dashboard" preload="render">Dash</Link> // 렌더링 시
168
+ <Link to="/products" preload="viewport">Prod</Link> // viewport 진입 시
61
169
  ```
62
170
 
63
- </quick_reference>
171
+ | Link Props | 타입 | 설명 |
172
+ |------------|------|------|
173
+ | `to` | string | 목적지 경로 |
174
+ | `params` | object | Path 파라미터 |
175
+ | `search` | object \| function | Search params (함수로 이전 값 접근) |
176
+ | `hash` | string | Hash |
177
+ | `replace` | boolean | history.replace 사용 |
178
+ | `preload` | 'intent' \| 'render' \| 'viewport' | Preload 전략 |
179
+ | `activeProps` | object | Active 시 props |
180
+ | `inactiveProps` | object | Inactive 시 props |
181
+ | `activeOptions` | object | Active 조건 (`exact` 등) |
64
182
 
65
- <structure>
183
+ ## useNavigate
184
+
185
+ ```tsx
186
+ const Component = () => {
187
+ const navigate = useNavigate()
188
+
189
+ const goToAbout = () => navigate({ to: '/about' })
190
+ const goToPost = (id: string) => navigate({ to: '/posts/$postId', params: { postId: id } })
191
+ const updateSearch = () => navigate({ to: '/products', search: prev => ({ ...prev, page: 2 }) })
192
+ const replaceRoute = () => navigate({ to: '/login', replace: true })
193
+ const goUp = () => navigate({ to: '..' })
194
+
195
+ return <button onClick={goToAbout}>Go</button>
196
+ }
197
+
198
+ // 조건부 네비게이션
199
+ const SubmitButton = () => {
200
+ const navigate = useNavigate()
201
+ const [isPending, startTransition] = useTransition()
202
+
203
+ const handleSubmit = async () => {
204
+ const result = await submitForm()
205
+ if (result.success) {
206
+ startTransition(() => navigate({ to: '/success' }))
207
+ }
208
+ }
209
+
210
+ return <button onClick={handleSubmit} disabled={isPending}>Submit</button>
211
+ }
212
+ ```
213
+
214
+ | navigate 옵션 | 타입 | 설명 |
215
+ |---------------|------|------|
216
+ | `to` | string | 목적지 경로 |
217
+ | `params` | object | Path 파라미터 |
218
+ | `search` | object \| function | Search params |
219
+ | `hash` | string | Hash |
220
+ | `replace` | boolean | history.replace 사용 |
221
+ | `resetScroll` | boolean | 스크롤 리셋 (기본 true) |
222
+
223
+ </navigation>
224
+
225
+ ---
226
+
227
+ <search_params>
228
+
229
+ ## Zod 스키마 + validateSearch
230
+
231
+ ```tsx
232
+ // Zod 스키마 정의
233
+ const searchSchema = z.object({
234
+ page: z.number().catch(1), // 기본값
235
+ search: z.string().optional(), // 선택
236
+ sort: z.enum(['newest', 'price']).catch('newest'),
237
+ tags: z.array(z.string()).catch([]), // 배열
238
+ inStock: z.boolean().catch(true), // Boolean
239
+ from: z.string().date().optional(), // 날짜
240
+ minPrice: z.number().min(0).catch(0), // 범위
241
+ })
242
+
243
+ // 라우트에 적용
244
+ export const Route = createFileRoute('/products')({
245
+ validateSearch: searchSchema,
246
+ loaderDeps: ({ search }) => ({ search }), // search 변경 시 loader 재실행
247
+ loader: async ({ deps: { search } }) => fetchProducts(search),
248
+ component: ProductsPage,
249
+ })
250
+
251
+ const ProductsPage = () => {
252
+ const { page, search, sort } = Route.useSearch() // 타입 안전
253
+ return <div>Page: {page}, Sort: {sort}</div>
254
+ }
255
+ ```
256
+
257
+ ## Search Params 업데이트
258
+
259
+ ```tsx
260
+ // Link로 업데이트
261
+ <Link to="/products" search={{ page: 1, sort: 'newest' }}>Reset</Link>
262
+ <Link to="/products" search={prev => ({ ...prev, page: 2 })}>Next</Link>
263
+
264
+ // useNavigate로 업데이트
265
+ const Pagination = () => {
266
+ const navigate = useNavigate()
267
+ const { page } = Route.useSearch()
268
+
269
+ const goToPage = (newPage: number) => {
270
+ navigate({ to: '/products', search: prev => ({ ...prev, page: newPage }) })
271
+ }
272
+
273
+ return (
274
+ <div>
275
+ <button onClick={() => goToPage(page - 1)} disabled={page <= 1}>Prev</button>
276
+ <span>Page {page}</span>
277
+ <button onClick={() => goToPage(page + 1)}>Next</button>
278
+ </div>
279
+ )
280
+ }
281
+ ```
282
+
283
+ ## 실전: 필터 + 정렬 + 페이지네이션
284
+
285
+ ```tsx
286
+ const PostsPage = () => {
287
+ const { page, search, category, sort } = Route.useSearch()
288
+ const posts = Route.useLoaderData()
289
+ const navigate = useNavigate()
290
+
291
+ const updateSearch = (updates: Partial<z.infer<typeof searchSchema>>) => {
292
+ navigate({ to: '/posts', search: prev => ({ ...prev, ...updates, page: 1 }) })
293
+ }
294
+
295
+ return (
296
+ <div>
297
+ <input value={search} onChange={e => updateSearch({ search: e.target.value })} />
298
+ <select value={category} onChange={e => updateSearch({ category: e.target.value as any })}>
299
+ <option value="all">All</option>
300
+ <option value="tech">Tech</option>
301
+ </select>
302
+ {posts.map(post => <div key={post.id}>{post.title}</div>)}
303
+ </div>
304
+ )
305
+ }
306
+ ```
307
+
308
+ </search_params>
309
+
310
+ ---
311
+
312
+ <route_context>
313
+
314
+ ## beforeLoad + Context
315
+
316
+ ```tsx
317
+ // beforeLoad: 인증 체크 + context 추가
318
+ export const Route = createFileRoute('/dashboard')({
319
+ beforeLoad: async ({ context, location }) => {
320
+ if (!context.auth.isAuthenticated) {
321
+ throw redirect({ to: '/login', search: { redirect: location.href } })
322
+ }
323
+ return { userPermissions: await fetchPermissions(context.auth.user.id) }
324
+ },
325
+ loader: async ({ context }) => fetchDashboardData(context.userPermissions),
326
+ component: DashboardPage,
327
+ })
328
+ ```
329
+
330
+ ## Protected Routes: _authed.tsx (pathless layout)
66
331
 
67
332
  ```
68
333
  routes/
69
- ├── __root.tsx # Root layout
70
- ├── index.tsx # /
71
- ├── about.tsx # /about
72
- ├── posts/
73
- ├── index.tsx # /posts
74
- │ └── $postId.tsx # /posts/:postId
75
- ├── _authed/ # Protected (pathless)
76
- │ ├── dashboard.tsx # /dashboard
77
- │ └── settings.tsx # /settings
78
- └── $.tsx # Catch-all (404)
334
+ ├── _authed.tsx # Protected layout
335
+ ├── _authed/
336
+ ├── dashboard.tsx # /dashboard (protected)
337
+ ├── settings.tsx # /settings (protected)
338
+ └── profile.tsx # /profile (protected)
339
+ ├── login.tsx # /login (public)
340
+ └── index.tsx # / (public)
79
341
  ```
80
342
 
81
- | Filename | Path |
82
- |--------|------|
83
- | `index.tsx` | Directory root |
84
- | `$param.tsx` | Dynamic segment |
85
- | `_layout/` | Pathless layout |
86
- | `$.tsx` | Catch-all |
343
+ ```tsx
344
+ // _authed.tsx
345
+ export const Route = createFileRoute('/_authed')({
346
+ beforeLoad: async ({ location }) => {
347
+ const user = await getCurrentUser()
348
+ if (!user) throw redirect({ to: '/login', search: { redirect: location.href } })
349
+ return { user }
350
+ },
351
+ component: () => <Outlet />,
352
+ })
87
353
 
88
- </structure>
354
+ // _authed/dashboard.tsx
355
+ export const Route = createFileRoute('/_authed/dashboard')({
356
+ component: DashboardPage,
357
+ })
358
+ const DashboardPage = () => {
359
+ const { user } = Route.useRouteContext() // _authed에서 전달된 context
360
+ return <h1>Welcome, {user.name}!</h1>
361
+ }
362
+ ```
363
+
364
+ ## Root Context
365
+
366
+ ```tsx
367
+ interface RouterContext {
368
+ queryClient: QueryClient
369
+ auth: { isAuthenticated: boolean; user: User | null }
370
+ }
89
371
 
90
- <options>
372
+ export const Route = createRootRouteWithContext<RouterContext>()({
373
+ component: RootLayout,
374
+ })
91
375
 
92
- | Option | Description |
93
- |------|------|
94
- | `component` | Component to render |
95
- | `loader` | Data loading function |
96
- | `beforeLoad` | Run before loading (auth, etc) |
97
- | `validateSearch` | Search params schema |
98
- | `pendingComponent` | Loading state component |
99
- | `errorComponent` | Error component |
100
- | `notFoundComponent` | Not found component |
376
+ const router = createRouter({
377
+ routeTree,
378
+ context: { queryClient, auth: { isAuthenticated: false, user: null } },
379
+ })
380
+ ```
101
381
 
102
- </options>
382
+ ## redirect()
383
+
384
+ ```tsx
385
+ throw redirect({ to: '/login' })
386
+ throw redirect({ to: '/login', search: { redirect: '/dashboard' } })
387
+ throw redirect({ to: '/posts/$postId', params: { postId: '123' } })
388
+ throw redirect({ to: '/home', replace: true })
389
+ ```
390
+
391
+ ## Context 접근
392
+
393
+ | 위치 | 접근 방법 |
394
+ |------|----------|
395
+ | `beforeLoad` | `{ context }` 파라미터 |
396
+ | `loader` | `{ context }` 파라미터 |
397
+ | `component` | `Route.useRouteContext()` |
398
+
399
+ </route_context>
400
+
401
+ ---
103
402
 
104
403
  <hooks>
105
404
 
106
- | Hook | Purpose |
107
- |------|------|
108
- | `Route.useLoaderData()` | Loader return value |
109
- | `Route.useParams()` | Path parameters |
110
- | `Route.useSearch()` | Search params |
111
- | `Route.useRouteContext()` | Route context |
112
- | `useNavigate()` | Programmatic navigation |
113
- | `useMatch({ from })` | Route match info |
405
+ ## Route-Scoped Hooks (Type-safe, Recommended)
406
+
407
+ ```tsx
408
+ const PostPage = () => {
409
+ const { post } = Route.useLoaderData() // Loader 반환값
410
+ const { postId } = Route.useParams() // Path params
411
+ const { page, sort } = Route.useSearch() // Search params
412
+ const { user } = Route.useRouteContext() // Route context
413
+ return <h1>{post.title}</h1>
414
+ }
415
+ ```
416
+
417
+ ## Global Hooks (Manual type)
418
+
419
+ ```tsx
420
+ // useNavigate
421
+ const navigate = useNavigate()
422
+ navigate({ to: '/posts/$postId', params: { postId: '123' } })
423
+
424
+ // useMatch
425
+ const postMatch = useMatch({ from: '/posts/$postId', shouldThrow: false })
426
+ if (postMatch) return <span>Post: {postMatch.params.postId}</span>
427
+
428
+ // useParams (Global)
429
+ const { postId } = useParams({ from: '/posts/$postId' })
430
+ const params = useParams({ strict: false }) // 모든 params
431
+
432
+ // useSearch (Global)
433
+ const { page } = useSearch({ from: '/products' })
434
+ const search = useSearch({ strict: false }) // 현재 search
435
+
436
+ // useRouterState
437
+ const pathname = useRouterState({ select: state => state.location.pathname })
438
+ const isLoading = useRouterState({ select: state => state.isLoading })
439
+
440
+ // useLocation
441
+ const location = useLocation()
442
+ console.log(location.pathname) // '/posts/123'
443
+ console.log(location.search) // { page: 1 }
444
+ ```
445
+
446
+ ## Hooks Reference
447
+
448
+ | Hook | Scope | Type | 용도 |
449
+ |------|-------|------|------|
450
+ | `Route.useLoaderData()` | Route | Auto | Loader 데이터 |
451
+ | `Route.useParams()` | Route | Auto | Path params (타입 안전) |
452
+ | `Route.useSearch()` | Route | Auto | Search params (타입 안전) |
453
+ | `Route.useRouteContext()` | Route | Auto | Route context |
454
+ | `useParams({ from })` | Global | Manual | 다른 라우트 params |
455
+ | `useSearch({ from })` | Global | Manual | 다른 라우트 search |
456
+ | `useMatch({ from })` | Global | Manual | 라우트 매치 정보 |
457
+ | `useNavigate()` | Global | Auto | 네비게이션 |
458
+ | `useRouterState()` | Global | Manual | 라우터 상태 (pathname, isLoading) |
459
+ | `useLocation()` | Global | Auto | 현재 location (pathname, search, hash) |
114
460
 
115
461
  </hooks>
462
+
463
+ ---
464
+
465
+ <error_handling>
466
+
467
+ ## errorComponent
468
+
469
+ ```tsx
470
+ export const Route = createFileRoute('/posts/$postId')({
471
+ loader: async ({ params }) => {
472
+ const post = await getPost(params.postId)
473
+ if (!post) throw new Error('Post not found')
474
+ return { post }
475
+ },
476
+ errorComponent: PostError,
477
+ component: PostPage,
478
+ })
479
+
480
+ const PostError = ({ error, reset }: ErrorComponentProps) => (
481
+ <div>
482
+ <h2>Error loading post</h2>
483
+ <p>{error.message}</p>
484
+ <button onClick={reset}>Retry</button>
485
+ </div>
486
+ )
487
+ ```
488
+
489
+ ## notFoundComponent
490
+
491
+ ```tsx
492
+ export const Route = createFileRoute('/posts/$postId')({
493
+ loader: async ({ params }) => {
494
+ const post = await getPost(params.postId)
495
+ if (!post) throw notFound({ data: { searchedId: params.postId } })
496
+ return { post }
497
+ },
498
+ notFoundComponent: ({ data }) => <p>Post {data?.searchedId} not found</p>,
499
+ component: PostPage,
500
+ })
501
+
502
+ // Root 404
503
+ export const Route = createRootRoute({
504
+ component: RootLayout,
505
+ notFoundComponent: () => (
506
+ <div>
507
+ <h1>404</h1>
508
+ <Link to="/">Go Home</Link>
509
+ </div>
510
+ ),
511
+ })
512
+ ```
513
+
514
+ ## pendingComponent
515
+
516
+ ```tsx
517
+ export const Route = createFileRoute('/posts')({
518
+ loader: async () => fetchPosts(),
519
+ pendingComponent: () => <Spinner />,
520
+ pendingMs: 200, // 200ms 후 표시
521
+ pendingMinMs: 500, // 최소 500ms 유지 (깜빡임 방지)
522
+ component: PostsPage,
523
+ })
524
+ ```
525
+
526
+ ## Catch-all (routes/$.tsx)
527
+
528
+ ```tsx
529
+ export const Route = createFileRoute('/$')({
530
+ component: () => {
531
+ const { _splat } = Route.useParams()
532
+ return <div>Page Not Found: /{_splat}</div>
533
+ },
534
+ })
535
+ ```
536
+
537
+ ## 에러 타입 구분
538
+
539
+ ```tsx
540
+ const CustomError = ({ error, reset }: ErrorComponentProps) => {
541
+ if (error instanceof TypeError && error.message.includes('fetch')) {
542
+ return <div><p>Network error</p><button onClick={reset}>Retry</button></div>
543
+ }
544
+ if (error.message.includes('unauthorized')) {
545
+ return <Navigate to="/login" />
546
+ }
547
+ return <div><p>Something went wrong</p><button onClick={reset}>Retry</button></div>
548
+ }
549
+ ```
550
+
551
+ ## 우선순위
552
+
553
+ | 우선순위 | 컴포넌트 | 조건 |
554
+ |---------|---------|------|
555
+ | 1 | `errorComponent` | loader/beforeLoad에서 Error throw |
556
+ | 2 | `notFoundComponent` | `notFound()` throw |
557
+ | 3 | `pendingComponent` | loader 실행 중 (pendingMs 이후) |
558
+ | 4 | `component` | 정상 렌더링 |
559
+
560
+ **에러 전파:** 하위 → 상위 (errorComponent 없으면 부모로 전파)
561
+
562
+ </error_handling>
563
+
564
+ ---
565
+
566
+ <dos_donts>
567
+
568
+ ## Do's & Don'ts
569
+
570
+ | ✅ Do | ❌ Don't |
571
+ |-------|---------|
572
+ | `Route.useParams()` (타입 안전) | `useParams()` (수동 타입) |
573
+ | `Route.useSearch()` (타입 안전) | `useSearch()` (수동 타입) |
574
+ | `validateSearch`로 Search params 검증 | Search params 검증 없이 사용 |
575
+ | `beforeLoad`에서 인증 체크 | `loader`에서 인증 체크 |
576
+ | `errorComponent`로 에러 처리 | try-catch로 에러 처리 |
577
+ | `<Link>` 또는 `useNavigate()` | `window.location.href` |
578
+ | `_authed/` layout으로 Protected Routes | 모든 라우트에 인증 로직 중복 |
579
+ | `loaderDeps`로 search 변경 감지 | useEffect로 수동 refetch |
580
+ | `pendingComponent`로 로딩 표시 | useQuery의 isLoading |
581
+ | `notFound()` throw | Error throw + 문자열 비교 |
582
+
583
+ </dos_donts>
584
+
585
+ ---
586
+
587
+ <patterns>
588
+
589
+ ## Quick Reference
590
+
591
+ ```tsx
592
+ // ===== 기본 라우트 =====
593
+ export const Route = createFileRoute('/about')({ component: AboutPage })
594
+
595
+ // ===== Loader + 동적 파라미터 =====
596
+ export const Route = createFileRoute('/posts/$postId')({
597
+ loader: async ({ params }) => ({ post: await getPost(params.postId) }),
598
+ component: PostPage,
599
+ })
600
+ const PostPage = () => {
601
+ const { post } = Route.useLoaderData()
602
+ return <h1>{post.title}</h1>
603
+ }
604
+
605
+ // ===== Search Params (Zod) =====
606
+ export const Route = createFileRoute('/products')({
607
+ validateSearch: z.object({
608
+ page: z.number().catch(1),
609
+ sort: z.enum(['newest', 'price']).catch('newest'),
610
+ }),
611
+ component: ProductsPage,
612
+ })
613
+ const ProductsPage = () => {
614
+ const { page, sort } = Route.useSearch()
615
+ return <div>Page {page}, Sort: {sort}</div>
616
+ }
617
+
618
+ // ===== Root Route =====
619
+ export const Route = createRootRoute({
620
+ component: RootLayout,
621
+ notFoundComponent: () => <div>404</div>,
622
+ })
623
+ const RootLayout = () => (
624
+ <div>
625
+ <nav>{/* ... */}</nav>
626
+ <main><Outlet /></main>
627
+ </div>
628
+ )
629
+
630
+ // ===== Navigation =====
631
+ <Link to="/posts/$postId" params={{ postId: '123' }}>Post</Link>
632
+ <Link to="/products" search={{ page: 1 }}>Products</Link>
633
+
634
+ const navigate = useNavigate()
635
+ navigate({ to: '/posts/$postId', params: { postId: '123' } })
636
+ navigate({ to: '/products', search: prev => ({ ...prev, page: 2 }) })
637
+
638
+ // ===== Protected Routes =====
639
+ // _authed.tsx
640
+ export const Route = createFileRoute('/_authed')({
641
+ beforeLoad: async ({ location }) => {
642
+ const user = await getCurrentUser()
643
+ if (!user) throw redirect({ to: '/login', search: { redirect: location.href } })
644
+ return { user }
645
+ },
646
+ component: () => <Outlet />,
647
+ })
648
+
649
+ // ===== Error Handling =====
650
+ export const Route = createFileRoute('/posts/$postId')({
651
+ loader: async ({ params }) => {
652
+ const post = await getPost(params.postId)
653
+ if (!post) throw notFound()
654
+ return { post }
655
+ },
656
+ errorComponent: ({ error, reset }) => <div>{error.message}<button onClick={reset}>Retry</button></div>,
657
+ notFoundComponent: () => <div>Post not found</div>,
658
+ pendingComponent: () => <Spinner />,
659
+ component: PostPage,
660
+ })
661
+ ```
662
+
663
+ </patterns>