@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
@@ -13,10 +13,14 @@
13
13
  **Key Features:**
14
14
  - File-based routing (`__root.tsx`, `$param.tsx`, `_layout/`)
15
15
  - Type-safe hooks (`Route.useLoaderData()`, `Route.useParams()`)
16
- - Data loading (`loader`, `beforeLoad`)
17
- - Search params validation (Zod integration)
16
+ - Data loading with SWR caching (`loader`, `beforeLoad`)
17
+ - Search params validation (Zod v4 + `zodValidator()`)
18
18
  - Route context for auth/state sharing
19
19
  - Error boundaries and pending states
20
+ - Code splitting (automatic + manual `.lazy.tsx`)
21
+ - Preloading strategies (intent, viewport, render)
22
+ - Deferred data loading with `Await` component
23
+ - Advanced path params (prefix/suffix patterns, optional params)
20
24
 
21
25
  </context>
22
26
 
@@ -28,9 +32,12 @@
28
32
  |------|----------|------|
29
33
  | **Hooks** | `useParams()`/`useSearch()` 타입 수동 지정 | `Route.useParams()`/`Route.useSearch()` 사용 (자동 타입) |
30
34
  | **Search Params** | validateSearch 없이 search params 사용 | 타입 안전성 보장 불가 |
35
+ | **Search Params** | Zod schema `.catch()` 직접 사용 | `fallback()` from `@tanstack/zod-adapter` 사용 |
31
36
  | **Navigation** | `window.location.href` 사용 | `<Link>` 또는 `useNavigate()` 사용 |
32
37
  | **Context** | Context 없이 전역 상태 prop drilling | Route context 또는 Zustand 사용 |
33
- | **Error** | try-catch로 에러 처리 | `errorComponent` 사용 |
38
+ | **Error** | try-catch로 에러 처리 | `errorComponent`, `notFoundComponent` 사용 |
39
+ | **Lazy Route** | `.lazy.tsx`에 loader/beforeLoad/validateSearch | 크리티컬 옵션은 메인 파일에만 |
40
+ | **Code Splitting** | 순환 의존성 발생 시 `Route` import | `getRouteApi()` 사용 |
34
41
 
35
42
  </forbidden>
36
43
 
@@ -40,12 +47,15 @@
40
47
 
41
48
  | 작업 | 필수 행동 |
42
49
  |------|----------|
43
- | **Search Params** | Zod 스키마로 `validateSearch` 정의 |
50
+ | **Search Params** | Zod 스키마로 `validateSearch` 정의, `zodValidator()` + `fallback()` 사용 |
44
51
  | **Type-safe Hooks** | `Route.useLoaderData()`, `Route.useParams()`, `Route.useSearch()` 사용 |
45
52
  | **Protected Routes** | `_authed.tsx` + `beforeLoad`에서 인증 체크 |
46
- | **Data Loading** | `loader`에서 데이터 fetch, TanStack Query와 통합 |
47
- | **Error Handling** | `errorComponent`, `notFoundComponent` 정의 |
53
+ | **Data Loading** | `loader`에서 데이터 fetch, 병렬 로딩은 `Promise.all()` |
54
+ | **Error Handling** | `errorComponent`, `notFoundComponent`, `pendingComponent` 정의 |
48
55
  | **Navigation** | `<Link>`에 `params`, `search` 타입 안전하게 전달 |
56
+ | **Preloading** | 일반 링크는 `preload="intent"`, 목록은 `preload="viewport"` |
57
+ | **Code Splitting** | 무거운 컴포넌트는 `.lazy.tsx` 또는 `lazy()` 사용 |
58
+ | **Deferred Data** | 중요 데이터는 `await`, 비중요 데이터는 Promise 반환 + `Await` 컴포넌트 |
49
59
 
50
60
  </required>
51
61
 
@@ -60,10 +70,14 @@ routes/
60
70
  ├── about.tsx # /about
61
71
  ├── posts/
62
72
  │ ├── index.tsx # /posts
63
- └── $postId.tsx # /posts/:postId
73
+ ├── $postId.tsx # /posts/:postId
74
+ │ └── $postId.lazy.tsx # 코드 스플릿 컴포넌트
64
75
  ├── _authed/ # Protected (pathless)
65
76
  │ ├── dashboard.tsx # /dashboard
66
77
  │ └── settings.tsx # /settings
78
+ ├── (marketing)/ # Route group (정리용)
79
+ │ ├── pricing.tsx # /pricing
80
+ │ └── features.tsx # /features
67
81
  └── $.tsx # Catch-all (404)
68
82
  ```
69
83
 
@@ -73,7 +87,12 @@ routes/
73
87
  | `index.tsx` | `/` | 디렉토리 루트 |
74
88
  | `about.tsx` | `/about` | 정적 라우트 |
75
89
  | `$postId.tsx` | `/posts/:postId` | 동적 세그먼트 |
90
+ | `post-{$postId}.tsx` | `/post-123` | Prefix 패턴 |
91
+ | `{-$locale}/about.tsx` | `/en/about`, `/about` | Optional param |
76
92
  | `_authed/dashboard.tsx` | `/dashboard` | Pathless layout (인증 등) |
93
+ | `(group)/pricing.tsx` | `/pricing` | Route group (정리용) |
94
+ | `-components/` | - | 제외 (라우트 아님) |
95
+ | `route.lazy.tsx` | - | 코드 스플릿 컴포넌트 |
77
96
  | `$.tsx` | `/*` | Catch-all (404) |
78
97
 
79
98
  </structure>
@@ -84,16 +103,23 @@ routes/
84
103
 
85
104
  | 옵션 | 타입 | 설명 |
86
105
  |------|------|------|
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 실행 중 표시 |
106
+ | `component` | Component | 렌더링할 컴포넌트 (non-critical, 코드 스플릿 가능) |
107
+ | `loader` | async function | 데이터 로드 (SSR/CSR 모두, critical) |
108
+ | `beforeLoad` | async function | loader 전 실행 (인증, context 추가, critical) |
109
+ | `validateSearch` | Zod schema | Search params 검증 + 타입 추론 (critical) |
110
+ | `loaderDeps` | function | search/params 변경 시 loader 재실행 트리거 |
111
+ | `errorComponent` | Component | Error throw 시 표시 (non-critical) |
112
+ | `notFoundComponent` | Component | notFound() throw 시 표시 (non-critical) |
113
+ | `pendingComponent` | Component | loader 실행 중 표시 (non-critical) |
95
114
  | `pendingMs` | number | pendingComponent 표시 지연 (기본 1000ms) |
96
115
  | `pendingMinMs` | number | pendingComponent 최소 표시 시간 (깜빡임 방지) |
116
+ | `staleTime` | number | 데이터 fresh 유지 시간 (기본 0ms) |
117
+ | `gcTime` | number | 미사용 데이터 메모리 유지 (기본 30분) |
118
+ | `shouldReload` | boolean \| function | 강제 리로드 조건 |
119
+
120
+ **Critical vs Non-Critical:**
121
+ - **Critical (메인 파일):** `loader`, `beforeLoad`, `validateSearch` → SSR/초기 렌더에 필요
122
+ - **Non-Critical (lazy 파일):** `component`, `errorComponent`, `pendingComponent`, `notFoundComponent` → 클라이언트 전용
97
123
 
98
124
  ```tsx
99
125
  // 기본 라우트
@@ -111,12 +137,16 @@ const PostPage = () => {
111
137
  return <h1>{post.title}</h1>
112
138
  }
113
139
 
114
- // Search Params (Zod)
140
+ // Search Params (Zod v4)
141
+ import { zodValidator, fallback } from '@tanstack/zod-adapter'
142
+
115
143
  export const Route = createFileRoute('/products')({
116
- validateSearch: z.object({
117
- page: z.number().catch(1),
118
- sort: z.enum(['newest', 'price']).catch('newest'),
119
- }),
144
+ validateSearch: zodValidator(
145
+ z.object({
146
+ page: fallback(z.number(), 1),
147
+ sort: fallback(z.enum(['newest', 'price']), 'newest'),
148
+ })
149
+ ),
120
150
  component: ProductsPage,
121
151
  })
122
152
  const ProductsPage = () => {
@@ -141,6 +171,472 @@ const RootLayout = () => (
141
171
 
142
172
  ---
143
173
 
174
+ <swr_caching>
175
+
176
+ ## SWR 캐싱 시스템
177
+
178
+ TanStack Router는 내장 SWR(Stale-While-Revalidate) 캐싱으로 빠른 네비게이션 제공.
179
+
180
+ **캐싱 설정:**
181
+
182
+ | 옵션 | 기본값 | 설명 |
183
+ |------|--------|------|
184
+ | `staleTime` | 0ms | 데이터가 fresh 상태로 유지되는 시간 (fresh = 리페치 안 함) |
185
+ | `preloadStaleTime` | 30s | 프리로드 데이터가 fresh 상태로 유지되는 시간 |
186
+ | `gcTime` | 30분 | 미사용 데이터가 메모리에서 제거되기까지 시간 |
187
+ | `shouldReload` | `true` | 강제 리로드 조건 (boolean 또는 함수) |
188
+
189
+ **라우트별 캐싱:**
190
+
191
+ ```tsx
192
+ export const Route = createFileRoute('/posts')({
193
+ loader: async () => ({ posts: await getPosts() }),
194
+ staleTime: 10_000, // 10초간 fresh (재방문 시 리페치 안 함)
195
+ gcTime: 60_000, // 1분간 메모리 유지
196
+ })
197
+ ```
198
+
199
+ **Router 레벨 기본값:**
200
+
201
+ ```tsx
202
+ const router = createRouter({
203
+ routeTree,
204
+ defaultStaleTime: 5_000, // 모든 라우트 기본 5초
205
+ defaultPreloadStaleTime: 30_000, // 프리로드 30초
206
+ defaultGcTime: 1_800_000, // 30분
207
+ })
208
+ ```
209
+
210
+ **수동 무효화:**
211
+
212
+ ```tsx
213
+ const router = useRouter()
214
+
215
+ // 특정 라우트 무효화
216
+ router.invalidate({
217
+ filter: (route) => route.fullPath.startsWith('/posts'),
218
+ })
219
+
220
+ // 전체 무효화
221
+ router.invalidate()
222
+ ```
223
+
224
+ **동작 원리:**
225
+ 1. 처음 방문: loader 실행 → 데이터 캐싱
226
+ 2. `staleTime` 이내 재방문: 캐시 사용 (리페치 안 함)
227
+ 3. `staleTime` 이후 재방문: 캐시 표시 + 백그라운드 리페치
228
+ 4. `gcTime` 이후: 메모리에서 제거
229
+
230
+ </swr_caching>
231
+
232
+ ---
233
+
234
+ <path_params>
235
+
236
+ ## Path Params
237
+
238
+ **기본 동적 세그먼트:**
239
+
240
+ ```tsx
241
+ // $postId.tsx → /posts/123
242
+ export const Route = createFileRoute('/posts/$postId')({
243
+ loader: async ({ params }) => {
244
+ const postId: string = params.postId // 자동 타입 추론
245
+ return { post: await getPost(postId) }
246
+ },
247
+ })
248
+ ```
249
+
250
+ **Prefix/Suffix 패턴:**
251
+
252
+ ```tsx
253
+ // post-{$postId}.tsx → /post-123
254
+ export const Route = createFileRoute('/post-{$postId}')({
255
+ loader: async ({ params }) => ({ post: await getPost(params.postId) }),
256
+ })
257
+
258
+ // {$category}-products.tsx → /tech-products
259
+ export const Route = createFileRoute('/{$category}-products')({
260
+ loader: async ({ params }) => ({ products: await getProducts(params.category) }),
261
+ })
262
+ ```
263
+
264
+ **Optional Params:**
265
+
266
+ ```tsx
267
+ // {-$locale}/about.tsx → /en/about 또는 /about
268
+ export const Route = createFileRoute('/{-$locale}/about')({
269
+ loader: async ({ params }) => {
270
+ const locale = params.locale ?? 'en' // undefined 가능
271
+ return { content: await getContent(locale) }
272
+ },
273
+ })
274
+ ```
275
+
276
+ **Custom 문자 허용:**
277
+
278
+ ```tsx
279
+ export const Route = createFileRoute('/user/$userId')({
280
+ // 기본: a-zA-Z0-9_-
281
+ // 커스텀: 점(.) 허용 (이메일 등)
282
+ pathParamsAllowedCharacters: ['a-zA-Z0-9_\\-\\.'],
283
+ loader: async ({ params }) => ({ user: await getUserByEmail(params.userId) }),
284
+ })
285
+ ```
286
+
287
+ **Type-safe 접근:**
288
+
289
+ ```tsx
290
+ // 같은 파일
291
+ function PostPage() {
292
+ const { postId } = Route.useParams()
293
+ return <div>{postId}</div>
294
+ }
295
+
296
+ // 다른 파일 (getRouteApi로 순환 의존성 방지)
297
+ import { getRouteApi } from '@tanstack/react-router'
298
+
299
+ const routeApi = getRouteApi('/posts/$postId')
300
+
301
+ function PostDetail() {
302
+ const { postId } = routeApi.useParams()
303
+ const { post } = routeApi.useLoaderData()
304
+ return <div>{post.title}</div>
305
+ }
306
+
307
+ // select로 리렌더 최적화
308
+ const postId = useParams({
309
+ from: '/posts/$postId',
310
+ select: (params) => params.postId,
311
+ })
312
+ ```
313
+
314
+ </path_params>
315
+
316
+ ---
317
+
318
+ <code_splitting>
319
+
320
+ ## Code Splitting
321
+
322
+ **자동 코드 스플리팅:**
323
+
324
+ ```tsx
325
+ // tsr.config.json
326
+ {
327
+ "autoCodeSplitting": true // 모든 라우트 자동 스플릿
328
+ }
329
+ ```
330
+
331
+ **수동 코드 스플리팅 (.lazy.tsx):**
332
+
333
+ ```tsx
334
+ // posts/$postId.tsx - Critical 옵션만
335
+ import { createFileRoute } from '@tanstack/react-router'
336
+
337
+ export const Route = createFileRoute('/posts/$postId')({
338
+ loader: async ({ params }) => ({ post: await getPost(params.postId) }),
339
+ beforeLoad: async ({ context }) => { /* auth */ },
340
+ validateSearch: zodValidator(searchSchema),
341
+ })
342
+
343
+ // posts/$postId.lazy.tsx - Non-critical (컴포넌트)
344
+ import { createLazyFileRoute } from '@tanstack/react-router'
345
+
346
+ export const Route = createLazyFileRoute('/posts/$postId')({
347
+ component: PostDetail,
348
+ errorComponent: PostError,
349
+ pendingComponent: PostSkeleton,
350
+ notFoundComponent: PostNotFound,
351
+ })
352
+
353
+ function PostDetail() {
354
+ const { post } = Route.useLoaderData() // 타입 안전
355
+ return <div>{post.title}</div>
356
+ }
357
+ ```
358
+
359
+ **React.lazy()로 수동 스플릿:**
360
+
361
+ ```tsx
362
+ import { lazy, Suspense } from 'react'
363
+
364
+ const HeavyEditor = lazy(() => import('./components/HeavyEditor'))
365
+
366
+ export const Route = createFileRoute('/editor')({
367
+ component: () => (
368
+ <Suspense fallback={<EditorSkeleton />}>
369
+ <HeavyEditor />
370
+ </Suspense>
371
+ ),
372
+ })
373
+ ```
374
+
375
+ **getRouteApi로 타입 안전 유지:**
376
+
377
+ ```tsx
378
+ // components/PostDetail.tsx (별도 파일)
379
+ import { getRouteApi } from '@tanstack/react-router'
380
+
381
+ const routeApi = getRouteApi('/posts/$postId')
382
+
383
+ export function PostDetail() {
384
+ const { postId } = routeApi.useParams()
385
+ const { post } = routeApi.useLoaderData()
386
+ const search = routeApi.useSearch()
387
+ const context = routeApi.useRouteContext()
388
+
389
+ return <div>{post.title}</div>
390
+ }
391
+ ```
392
+
393
+ **Critical vs Non-Critical 정리:**
394
+
395
+ | Critical (메인 파일) | Non-Critical (lazy 파일) |
396
+ |---------------------|-------------------------|
397
+ | `loader` | `component` |
398
+ | `beforeLoad` | `errorComponent` |
399
+ | `validateSearch` | `pendingComponent` |
400
+ | - | `notFoundComponent` |
401
+
402
+ </code_splitting>
403
+
404
+ ---
405
+
406
+ <external_data_loading>
407
+
408
+ ## External Data Loading
409
+
410
+ **TanStack Query 통합:**
411
+
412
+ ```tsx
413
+ import { createFileRoute } from '@tanstack/react-router'
414
+ import { useSuspenseQuery, queryOptions } from '@tanstack/react-query'
415
+ import { getQueryClient } from '@/lib/query-client'
416
+
417
+ // Query Options 정의
418
+ const postQueryOptions = (postId: string) =>
419
+ queryOptions({
420
+ queryKey: ['posts', postId],
421
+ queryFn: () => getPost(postId),
422
+ })
423
+
424
+ // Loader에서 ensureQueryData로 SSR 지원
425
+ export const Route = createFileRoute('/posts/$postId')({
426
+ loader: async ({ params, context }) => {
427
+ const queryClient = context.queryClient
428
+ await queryClient.ensureQueryData(postQueryOptions(params.postId))
429
+ },
430
+ component: PostPage,
431
+ })
432
+
433
+ function PostPage() {
434
+ const { postId } = Route.useParams()
435
+
436
+ // useSuspenseQuery로 캐시된 데이터 접근
437
+ const { data: post } = useSuspenseQuery(postQueryOptions(postId))
438
+
439
+ return <div>{post.title}</div>
440
+ }
441
+ ```
442
+
443
+ **SSR Hydration:**
444
+
445
+ ```tsx
446
+ // __root.tsx
447
+ import { dehydrate, hydrate, QueryClient } from '@tanstack/react-query'
448
+
449
+ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
450
+ beforeLoad: () => {
451
+ const queryClient = new QueryClient()
452
+ return { queryClient }
453
+ },
454
+ component: RootLayout,
455
+ })
456
+
457
+ function RootLayout() {
458
+ const { queryClient } = Route.useRouteContext()
459
+
460
+ return (
461
+ <html>
462
+ <head>
463
+ <script
464
+ dangerouslySetInnerHTML={{
465
+ __html: `window.__REACT_QUERY_STATE__ = ${JSON.stringify(
466
+ dehydrate(queryClient)
467
+ )}`,
468
+ }}
469
+ />
470
+ </head>
471
+ <body>
472
+ <Outlet />
473
+ </body>
474
+ </html>
475
+ )
476
+ }
477
+ ```
478
+
479
+ </external_data_loading>
480
+
481
+ ---
482
+
483
+ <search_params>
484
+
485
+ ## Search Params
486
+
487
+ **Zod v4 + zodValidator:**
488
+
489
+ ```tsx
490
+ import { createFileRoute } from '@tanstack/react-router'
491
+ import { z } from 'zod'
492
+ import { zodValidator, fallback } from '@tanstack/zod-adapter'
493
+
494
+ // Zod 스키마 정의 (fallback으로 기본값)
495
+ const searchSchema = z.object({
496
+ page: fallback(z.number().int().positive(), 1),
497
+ search: fallback(z.string().optional(), undefined),
498
+ sort: fallback(z.enum(['newest', 'price', 'rating']), 'newest'),
499
+ tags: fallback(z.array(z.string()), []),
500
+ inStock: fallback(z.boolean(), true),
501
+ from: fallback(z.string().datetime().optional(), undefined),
502
+ minPrice: fallback(z.number().min(0), 0),
503
+ })
504
+
505
+ // 라우트에 적용
506
+ export const Route = createFileRoute('/products')({
507
+ validateSearch: zodValidator(searchSchema),
508
+ loaderDeps: ({ search }) => ({ search }), // search 변경 시 loader 재실행
509
+ loader: async ({ deps: { search } }) => fetchProducts(search),
510
+ component: ProductsPage,
511
+ })
512
+
513
+ const ProductsPage = () => {
514
+ const { page, search, sort } = Route.useSearch() // 타입 안전
515
+ return <div>Page: {page}, Sort: {sort}</div>
516
+ }
517
+ ```
518
+
519
+ **Valibot 지원:**
520
+
521
+ ```tsx
522
+ import { valibotValidator } from '@tanstack/valibot-adapter'
523
+ import * as v from 'valibot'
524
+
525
+ const searchSchema = v.object({
526
+ page: v.fallback(v.number(), 1),
527
+ sort: v.fallback(v.picklist(['newest', 'price']), 'newest'),
528
+ })
529
+
530
+ export const Route = createFileRoute('/products')({
531
+ validateSearch: valibotValidator(searchSchema),
532
+ })
533
+ ```
534
+
535
+ **Search Middleware:**
536
+
537
+ ```tsx
538
+ // search params 전처리
539
+ const searchMiddleware = (search: Record<string, unknown>) => {
540
+ return {
541
+ ...search,
542
+ page: Math.max(1, search.page as number), // page >= 1 보장
543
+ }
544
+ }
545
+
546
+ export const Route = createFileRoute('/products')({
547
+ validateSearch: zodValidator(searchSchema),
548
+ searchMiddleware: [searchMiddleware],
549
+ })
550
+ ```
551
+
552
+ **Search Params 업데이트:**
553
+
554
+ ```tsx
555
+ // Link로 업데이트
556
+ <Link to="/products" search={{ page: 1, sort: 'newest' }}>Reset</Link>
557
+ <Link to="/products" search={prev => ({ ...prev, page: 2 })}>Next</Link>
558
+
559
+ // useNavigate로 업데이트
560
+ const Pagination = () => {
561
+ const navigate = useNavigate()
562
+ const { page } = Route.useSearch()
563
+
564
+ const goToPage = (newPage: number) => {
565
+ navigate({ to: '/products', search: prev => ({ ...prev, page: newPage }) })
566
+ }
567
+
568
+ return (
569
+ <div>
570
+ <button onClick={() => goToPage(page - 1)} disabled={page <= 1}>Prev</button>
571
+ <span>Page {page}</span>
572
+ <button onClick={() => goToPage(page + 1)}>Next</button>
573
+ </div>
574
+ )
575
+ }
576
+ ```
577
+
578
+ **select로 리렌더 최적화:**
579
+
580
+ ```tsx
581
+ // ❌ 전체 search 구독 - 아무 param 변경 시 리렌더
582
+ const { page, sort, q } = Route.useSearch()
583
+
584
+ // ✅ select로 page만 구독
585
+ const page = useSearch({
586
+ from: '/products',
587
+ select: (search) => search.page,
588
+ })
589
+ ```
590
+
591
+ </search_params>
592
+
593
+ ---
594
+
595
+ <virtual_file_routes>
596
+
597
+ ## Virtual File Routes
598
+
599
+ `@tanstack/virtual-file-routes`로 프로그래밍 방식 라우트 정의.
600
+
601
+ ```bash
602
+ npm install @tanstack/virtual-file-routes
603
+ ```
604
+
605
+ **기본 사용법:**
606
+
607
+ ```tsx
608
+ // tsr.config.json
609
+ {
610
+ "virtualFileRoutes": true
611
+ }
612
+
613
+ // __virtual.ts
614
+ import { rootRoute, route, index, layout, physical } from '@tanstack/virtual-file-routes'
615
+
616
+ export default rootRoute('__root.tsx', [
617
+ index('index.tsx'),
618
+ route('/about', 'about.tsx'),
619
+ route('/posts', 'posts.tsx', [
620
+ index('posts/index.tsx'),
621
+ route('/$postId', 'posts/$postId.tsx'),
622
+ ]),
623
+ layout('_authenticated', '_authenticated.tsx', [
624
+ route('/dashboard', 'dashboard.tsx'),
625
+ route('/settings', 'settings.tsx'),
626
+ ]),
627
+ physical('/blog/*'), // 물리 파일 그대로 사용
628
+ ])
629
+ ```
630
+
631
+ **장점:**
632
+ - 중앙화된 라우트 구조
633
+ - 동적 라우트 생성
634
+ - 물리 파일과 혼합 가능
635
+
636
+ </virtual_file_routes>
637
+
638
+ ---
639
+
144
640
  <navigation>
145
641
 
146
642
  ## Link Component
@@ -163,9 +659,10 @@ const RootLayout = () => (
163
659
  <Link to="/" activeOptions={{ exact: true }}>Home</Link>
164
660
 
165
661
  // Preloading
166
- <Link to="/posts" preload="intent">Posts</Link> // hover
662
+ <Link to="/posts" preload="intent">Posts</Link> // hover 50ms 후
167
663
  <Link to="/dashboard" preload="render">Dash</Link> // 렌더링 시
168
664
  <Link to="/products" preload="viewport">Prod</Link> // viewport 진입 시
665
+ <Link to="/settings" preload={false}>Settings</Link> // 비활성
169
666
  ```
170
667
 
171
668
  | Link Props | 타입 | 설명 |
@@ -175,7 +672,8 @@ const RootLayout = () => (
175
672
  | `search` | object \| function | Search params (함수로 이전 값 접근) |
176
673
  | `hash` | string | Hash |
177
674
  | `replace` | boolean | history.replace 사용 |
178
- | `preload` | 'intent' \| 'render' \| 'viewport' | Preload 전략 |
675
+ | `preload` | 'intent' \| 'render' \| 'viewport' \| false | Preload 전략 |
676
+ | `preloadDelay` | number | intent 모드 지연 (기본 50ms) |
179
677
  | `activeProps` | object | Active 시 props |
180
678
  | `inactiveProps` | object | Inactive 시 props |
181
679
  | `activeOptions` | object | Active 조건 (`exact` 등) |
@@ -224,88 +722,202 @@ const SubmitButton = () => {
224
722
 
225
723
  ---
226
724
 
227
- <search_params>
725
+ <preloading>
726
+
727
+ ## Preloading
728
+
729
+ **Link 프리로딩 모드:**
228
730
 
229
- ## Zod 스키마 + validateSearch
731
+ | 모드 | 트리거 | 사용 시점 |
732
+ |------|--------|---------|
733
+ | `'intent'` | hover/touch 50ms 후 | 일반적인 네비게이션 링크 (권장) |
734
+ | `'viewport'` | 뷰포트 진입 시 (IntersectionObserver) | 목록 페이지, 무한 스크롤 |
735
+ | `'render'` | 컴포넌트 마운트 시 | 확실히 이동할 링크 |
736
+ | `false` | 비활성 | 인증/외부 링크 |
737
+
738
+ **Router 레벨 기본값:**
230
739
 
231
740
  ```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), // 범위
741
+ const router = createRouter({
742
+ routeTree,
743
+ defaultPreload: 'intent', // 전역 기본값
744
+ defaultPreloadDelay: 50, // intent 모드 지연 (ms)
745
+ defaultPreloadStaleTime: 30_000, // 프리로드 데이터 30초간 fresh
241
746
  })
747
+ ```
242
748
 
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
- })
749
+ **Link별 오버라이드:**
250
750
 
251
- const ProductsPage = () => {
252
- const { page, search, sort } = Route.useSearch() // 타입 안전
253
- return <div>Page: {page}, Sort: {sort}</div>
751
+ ```tsx
752
+ // 목록 아이템은 viewport 프리로딩
753
+ {posts.map(post => (
754
+ <Link
755
+ key={post.id}
756
+ to="/posts/$postId"
757
+ params={{ postId: post.id }}
758
+ preload="viewport"
759
+ >
760
+ {post.title}
761
+ </Link>
762
+ ))}
763
+
764
+ // 특정 링크는 프리로딩 비활성
765
+ <Link to="/settings" preload={false}>Settings</Link>
766
+ ```
767
+
768
+ **프로그래밍 방식 프리로딩:**
769
+
770
+ ```tsx
771
+ const router = useRouter()
772
+
773
+ // 검색 결과 hover 시 수동 프리로드
774
+ const handleMouseEnter = (postId: string) => {
775
+ router.preloadRoute({
776
+ to: '/posts/$postId',
777
+ params: { postId },
778
+ })
254
779
  }
255
780
  ```
256
781
 
257
- ## Search Params 업데이트
782
+ **커스텀 프리로드 지연:**
258
783
 
259
784
  ```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>
785
+ <Link to="/posts" preload="intent" preloadDelay={100}>
786
+ Posts (100ms 지연)
787
+ </Link>
788
+ ```
263
789
 
264
- // useNavigate로 업데이트
265
- const Pagination = () => {
266
- const navigate = useNavigate()
267
- const { page } = Route.useSearch()
790
+ </preloading>
268
791
 
269
- const goToPage = (newPage: number) => {
270
- navigate({ to: '/products', search: prev => ({ ...prev, page: newPage }) })
271
- }
792
+ ---
272
793
 
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
- }
794
+ <route_loading_lifecycle>
795
+
796
+ ## Route Loading Lifecycle
797
+
798
+ **로딩 단계:**
799
+
800
+ 1. **Route Matching:** URL과 라우트 매칭
801
+ 2. **Route Pre-Loading (순차):** 매칭된 라우트의 `beforeLoad` 순차 실행 (부모 → 자식)
802
+ 3. **Route Loading (병렬):**
803
+ - `component.preload()` (lazy component)
804
+ - `loader()` (data fetching)
805
+ - **병렬 실행** → 가장 긴 작업이 완료되면 렌더링
806
+
807
+ **Loader Params:**
808
+
809
+ ```tsx
810
+ export const Route = createFileRoute('/posts/$postId')({
811
+ loader: async ({
812
+ abortController, // AbortController for fetch cancellation
813
+ cause, // 'enter' | 'preload' | 'stay' (로딩 원인)
814
+ context, // Router context + beforeLoad context
815
+ deps, // loaderDeps 반환값
816
+ location, // Current location object
817
+ params, // Path params { postId: string }
818
+ preload, // boolean (프리로드 여부)
819
+ route, // Route metadata
820
+ }) => {
821
+ // cause로 로딩 원인 구분
822
+ if (cause === 'preload') {
823
+ // 프리로드 시 가벼운 데이터만
824
+ return { post: await getPostPreview(params.postId) }
825
+ }
826
+
827
+ // abortController로 취소 가능한 fetch
828
+ const post = await fetch(`/api/posts/${params.postId}`, {
829
+ signal: abortController.signal,
830
+ })
831
+
832
+ return { post }
833
+ },
834
+ })
281
835
  ```
282
836
 
283
- ## 실전: 필터 + 정렬 + 페이지네이션
837
+ **Loader 최적화 (병렬):**
284
838
 
285
839
  ```tsx
286
- const PostsPage = () => {
287
- const { page, search, category, sort } = Route.useSearch()
288
- const posts = Route.useLoaderData()
289
- const navigate = useNavigate()
840
+ export const Route = createFileRoute('/dashboard')({
841
+ loader: async () => {
842
+ // 순차 실행 (느림)
843
+ const user = await getUser()
844
+ const stats = await getStats()
845
+ const posts = await getPosts()
846
+
847
+ // ✅ 병렬 실행 (빠름)
848
+ const [user, stats, posts] = await Promise.all([
849
+ getUser(),
850
+ getStats(),
851
+ getPosts(),
852
+ ])
853
+
854
+ return { user, stats, posts }
855
+ },
856
+ })
857
+ ```
290
858
 
291
- const updateSearch = (updates: Partial<z.infer<typeof searchSchema>>) => {
292
- navigate({ to: '/posts', search: prev => ({ ...prev, ...updates, page: 1 }) })
293
- }
859
+ </route_loading_lifecycle>
860
+
861
+ ---
862
+
863
+ <deferred_data>
864
+
865
+ ## Deferred Data Loading
866
+
867
+ 중요한 데이터는 `await`, 비중요 데이터는 Promise 반환 → `Await` 컴포넌트로 처리.
868
+
869
+ ```tsx
870
+ import { createFileRoute, Await } from '@tanstack/react-router'
871
+ import { Suspense } from 'react'
872
+
873
+ export const Route = createFileRoute('/posts/$postId')({
874
+ loader: async ({ params }) => {
875
+ // 중요: 즉시 await (페이지 렌더 차단)
876
+ const post = await getPost(params.postId) // 50ms
877
+
878
+ // 비중요: Promise 그대로 반환 (차단 안 함)
879
+ const deferredComments = getComments(params.postId) // 3초
880
+ const deferredRecommendations = getRecommendations(params.postId) // 2초
881
+
882
+ return { post, deferredComments, deferredRecommendations }
883
+ },
884
+ component: PostPage,
885
+ })
886
+
887
+ function PostPage() {
888
+ const { post, deferredComments, deferredRecommendations } = Route.useLoaderData()
294
889
 
295
890
  return (
296
891
  <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>)}
892
+ {/* 즉시 렌더링 (50ms) */}
893
+ <PostContent post={post} />
894
+
895
+ {/* 스트리밍 (3초) */}
896
+ <Suspense fallback={<CommentsSkeleton />}>
897
+ <Await promise={deferredComments}>
898
+ {(comments) => <Comments comments={comments} />}
899
+ </Await>
900
+ </Suspense>
901
+
902
+ {/* 스트리밍 (2초) */}
903
+ <Suspense fallback={<RecommendationsSkeleton />}>
904
+ <Await promise={deferredRecommendations}>
905
+ {(recommendations) => <Recommendations items={recommendations} />}
906
+ </Await>
907
+ </Suspense>
303
908
  </div>
304
909
  )
305
910
  }
306
911
  ```
307
912
 
308
- </search_params>
913
+ **장점:**
914
+ - 초기 페이지 렌더: 3초 → 50ms
915
+ - 비중요 데이터는 백그라운드 스트리밍
916
+ - 사용자는 즉시 핵심 콘텐츠 확인 가능
917
+
918
+ **사용 시점:** 분석, 추천, 사용자 활동, 소셜 기능, 댓글
919
+
920
+ </deferred_data>
309
921
 
310
922
  ---
311
923
 
@@ -356,7 +968,7 @@ export const Route = createFileRoute('/_authed/dashboard')({
356
968
  component: DashboardPage,
357
969
  })
358
970
  const DashboardPage = () => {
359
- const { user } = Route.useRouteContext() // _authed에서 전달된 context
971
+ const { user } = Route.useRouteContext() // _authed에서 전달된 context
360
972
  return <h1>Welcome, {user.name}!</h1>
361
973
  }
362
974
  ```
@@ -388,6 +1000,18 @@ throw redirect({ to: '/posts/$postId', params: { postId: '123' } })
388
1000
  throw redirect({ to: '/home', replace: true })
389
1001
  ```
390
1002
 
1003
+ ## router.invalidate()로 Context 재계산
1004
+
1005
+ ```tsx
1006
+ const router = useRouter()
1007
+
1008
+ // 로그인 후 context 재계산
1009
+ const handleLogin = async () => {
1010
+ await login()
1011
+ router.invalidate() // 모든 beforeLoad 재실행 → context 업데이트
1012
+ }
1013
+ ```
1014
+
391
1015
  ## Context 접근
392
1016
 
393
1017
  | 위치 | 접근 방법 |
@@ -406,10 +1030,10 @@ throw redirect({ to: '/home', replace: true })
406
1030
 
407
1031
  ```tsx
408
1032
  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
1033
+ const { post } = Route.useLoaderData() // Loader 반환값
1034
+ const { postId } = Route.useParams() // Path params
1035
+ const { page, sort } = Route.useSearch() // Search params
1036
+ const { user } = Route.useRouteContext() // Route context
413
1037
  return <h1>{post.title}</h1>
414
1038
  }
415
1039
  ```
@@ -427,11 +1051,11 @@ if (postMatch) return <span>Post: {postMatch.params.postId}</span>
427
1051
 
428
1052
  // useParams (Global)
429
1053
  const { postId } = useParams({ from: '/posts/$postId' })
430
- const params = useParams({ strict: false }) // 모든 params
1054
+ const params = useParams({ strict: false }) // 모든 params
431
1055
 
432
1056
  // useSearch (Global)
433
1057
  const { page } = useSearch({ from: '/products' })
434
- const search = useSearch({ strict: false }) // 현재 search
1058
+ const search = useSearch({ strict: false }) // 현재 search
435
1059
 
436
1060
  // useRouterState
437
1061
  const pathname = useRouterState({ select: state => state.location.pathname })
@@ -439,8 +1063,13 @@ const isLoading = useRouterState({ select: state => state.isLoading })
439
1063
 
440
1064
  // useLocation
441
1065
  const location = useLocation()
442
- console.log(location.pathname) // '/posts/123'
443
- console.log(location.search) // { page: 1 }
1066
+ console.log(location.pathname) // '/posts/123'
1067
+ console.log(location.search) // { page: 1 }
1068
+
1069
+ // useRouter
1070
+ const router = useRouter()
1071
+ router.invalidate() // 캐시 무효화
1072
+ router.preloadRoute({ to: '/posts/$postId', params: { postId: '123' } })
444
1073
  ```
445
1074
 
446
1075
  ## Hooks Reference
@@ -457,6 +1086,7 @@ console.log(location.search) // { page: 1 }
457
1086
  | `useNavigate()` | Global | Auto | 네비게이션 |
458
1087
  | `useRouterState()` | Global | Manual | 라우터 상태 (pathname, isLoading) |
459
1088
  | `useLocation()` | Global | Auto | 현재 location (pathname, search, hash) |
1089
+ | `useRouter()` | Global | Auto | Router 인스턴스 (invalidate, preloadRoute) |
460
1090
 
461
1091
  </hooks>
462
1092
 
@@ -486,8 +1116,38 @@ const PostError = ({ error, reset }: ErrorComponentProps) => (
486
1116
  )
487
1117
  ```
488
1118
 
1119
+ **에러 타입 구분:**
1120
+
1121
+ ```tsx
1122
+ import { ErrorComponent } from '@tanstack/react-router'
1123
+
1124
+ const CustomError = ({ error, reset }: ErrorComponentProps) => {
1125
+ if (error instanceof TypeError && error.message.includes('fetch')) {
1126
+ return <div><p>Network error</p><button onClick={reset}>Retry</button></div>
1127
+ }
1128
+ if (error.message.includes('unauthorized')) {
1129
+ return <Navigate to="/login" />
1130
+ }
1131
+ return <ErrorComponent error={error} />
1132
+ }
1133
+ ```
1134
+
1135
+ **Router 레벨 onError:**
1136
+
1137
+ ```tsx
1138
+ const router = createRouter({
1139
+ routeTree,
1140
+ onError: (error) => {
1141
+ console.error('Router error:', error)
1142
+ // 에러 로깅 서비스에 전송
1143
+ },
1144
+ })
1145
+ ```
1146
+
489
1147
  ## notFoundComponent
490
1148
 
1149
+ **Fuzzy Mode (기본):** 가장 가까운 `notFoundComponent` 표시
1150
+
491
1151
  ```tsx
492
1152
  export const Route = createFileRoute('/posts/$postId')({
493
1153
  loader: async ({ params }) => {
@@ -498,27 +1158,56 @@ export const Route = createFileRoute('/posts/$postId')({
498
1158
  notFoundComponent: ({ data }) => <p>Post {data?.searchedId} not found</p>,
499
1159
  component: PostPage,
500
1160
  })
1161
+ ```
501
1162
 
502
- // Root 404
1163
+ **Root Mode:** Root `notFoundComponent` 표시
1164
+
1165
+ ```tsx
503
1166
  export const Route = createRootRoute({
504
- component: RootLayout,
1167
+ notFoundMode: 'root', // fuzzy (기본) 또는 root
505
1168
  notFoundComponent: () => (
506
1169
  <div>
507
1170
  <h1>404</h1>
508
1171
  <Link to="/">Go Home</Link>
509
1172
  </div>
510
1173
  ),
1174
+ component: RootLayout,
511
1175
  })
512
1176
  ```
513
1177
 
1178
+ **특정 라우트 타겟팅:**
1179
+
1180
+ ```tsx
1181
+ throw notFound({
1182
+ routeId: '/posts/$postId', // 특정 라우트의 notFoundComponent 사용
1183
+ data: { searchedId: '123' },
1184
+ })
1185
+ ```
1186
+
1187
+ **CatchNotFound 컴포넌트:**
1188
+
1189
+ ```tsx
1190
+ import { CatchNotFound } from '@tanstack/react-router'
1191
+
1192
+ function PostsLayout() {
1193
+ return (
1194
+ <div>
1195
+ <CatchNotFound>
1196
+ <Outlet />
1197
+ </CatchNotFound>
1198
+ </div>
1199
+ )
1200
+ }
1201
+ ```
1202
+
514
1203
  ## pendingComponent
515
1204
 
516
1205
  ```tsx
517
1206
  export const Route = createFileRoute('/posts')({
518
1207
  loader: async () => fetchPosts(),
519
1208
  pendingComponent: () => <Spinner />,
520
- pendingMs: 200, // 200ms 후 표시
521
- pendingMinMs: 500, // 최소 500ms 유지 (깜빡임 방지)
1209
+ pendingMs: 200, // 200ms 후 표시
1210
+ pendingMinMs: 500, // 최소 500ms 유지 (깜빡임 방지)
522
1211
  component: PostsPage,
523
1212
  })
524
1213
  ```
@@ -534,20 +1223,6 @@ export const Route = createFileRoute('/$')({
534
1223
  })
535
1224
  ```
536
1225
 
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
1226
  ## 우선순위
552
1227
 
553
1228
  | 우선순위 | 컴포넌트 | 조건 |
@@ -559,6 +1234,16 @@ const CustomError = ({ error, reset }: ErrorComponentProps) => {
559
1234
 
560
1235
  **에러 전파:** 하위 → 상위 (errorComponent 없으면 부모로 전파)
561
1236
 
1237
+ **onCatch:** 에러 발생 시 실행할 콜백
1238
+
1239
+ ```tsx
1240
+ export const Route = createFileRoute('/posts/$postId')({
1241
+ onCatch: (error) => {
1242
+ console.error('Route error:', error)
1243
+ },
1244
+ })
1245
+ ```
1246
+
562
1247
  </error_handling>
563
1248
 
564
1249
  ---
@@ -571,6 +1256,7 @@ const CustomError = ({ error, reset }: ErrorComponentProps) => {
571
1256
  |-------|---------|
572
1257
  | `Route.useParams()` (타입 안전) | `useParams()` (수동 타입) |
573
1258
  | `Route.useSearch()` (타입 안전) | `useSearch()` (수동 타입) |
1259
+ | `zodValidator()` + `fallback()` | Zod schema `.catch()` 직접 |
574
1260
  | `validateSearch`로 Search params 검증 | Search params 검증 없이 사용 |
575
1261
  | `beforeLoad`에서 인증 체크 | `loader`에서 인증 체크 |
576
1262
  | `errorComponent`로 에러 처리 | try-catch로 에러 처리 |
@@ -579,6 +1265,14 @@ const CustomError = ({ error, reset }: ErrorComponentProps) => {
579
1265
  | `loaderDeps`로 search 변경 감지 | useEffect로 수동 refetch |
580
1266
  | `pendingComponent`로 로딩 표시 | useQuery의 isLoading |
581
1267
  | `notFound()` throw | Error throw + 문자열 비교 |
1268
+ | `preload="intent"` (일반 링크) | preload 없이 사용 |
1269
+ | `preload="viewport"` (목록) | 모든 링크에 render preload |
1270
+ | `Promise.all()` (병렬 loader) | 순차 await (waterfall) |
1271
+ | 중요 데이터 `await`, 비중요 Promise 반환 | 모든 데이터 await (차단) |
1272
+ | `.lazy.tsx` (코드 스플릿) | 모든 컴포넌트 메인 파일에 |
1273
+ | `getRouteApi()` (순환 의존성 방지) | Route import (순환 의존성) |
1274
+ | `router.invalidate()` (캐시 무효화) | 수동 상태 관리 |
1275
+ | `staleTime` 설정 (캐싱) | 매번 리페치 |
582
1276
 
583
1277
  </dos_donts>
584
1278
 
@@ -602,12 +1296,16 @@ const PostPage = () => {
602
1296
  return <h1>{post.title}</h1>
603
1297
  }
604
1298
 
605
- // ===== Search Params (Zod) =====
1299
+ // ===== Search Params (Zod v4) =====
1300
+ import { zodValidator, fallback } from '@tanstack/zod-adapter'
1301
+
606
1302
  export const Route = createFileRoute('/products')({
607
- validateSearch: z.object({
608
- page: z.number().catch(1),
609
- sort: z.enum(['newest', 'price']).catch('newest'),
610
- }),
1303
+ validateSearch: zodValidator(
1304
+ z.object({
1305
+ page: fallback(z.number(), 1),
1306
+ sort: fallback(z.enum(['newest', 'price']), 'newest'),
1307
+ })
1308
+ ),
611
1309
  component: ProductsPage,
612
1310
  })
613
1311
  const ProductsPage = () => {
@@ -628,7 +1326,7 @@ const RootLayout = () => (
628
1326
  )
629
1327
 
630
1328
  // ===== Navigation =====
631
- <Link to="/posts/$postId" params={{ postId: '123' }}>Post</Link>
1329
+ <Link to="/posts/$postId" params={{ postId: '123' }} preload="intent">Post</Link>
632
1330
  <Link to="/products" search={{ page: 1 }}>Products</Link>
633
1331
 
634
1332
  const navigate = useNavigate()
@@ -653,11 +1351,97 @@ export const Route = createFileRoute('/posts/$postId')({
653
1351
  if (!post) throw notFound()
654
1352
  return { post }
655
1353
  },
656
- errorComponent: ({ error, reset }) => <div>{error.message}<button onClick={reset}>Retry</button></div>,
1354
+ errorComponent: ({ error, reset }) => (
1355
+ <div>
1356
+ {error.message}
1357
+ <button onClick={reset}>Retry</button>
1358
+ </div>
1359
+ ),
657
1360
  notFoundComponent: () => <div>Post not found</div>,
658
1361
  pendingComponent: () => <Spinner />,
659
1362
  component: PostPage,
660
1363
  })
1364
+
1365
+ // ===== Code Splitting =====
1366
+ // posts/$postId.tsx
1367
+ export const Route = createFileRoute('/posts/$postId')({
1368
+ loader: async ({ params }) => ({ post: await getPost(params.postId) }),
1369
+ })
1370
+
1371
+ // posts/$postId.lazy.tsx
1372
+ export const Route = createLazyFileRoute('/posts/$postId')({
1373
+ component: PostDetail,
1374
+ })
1375
+
1376
+ // getRouteApi (순환 의존성 방지)
1377
+ import { getRouteApi } from '@tanstack/react-router'
1378
+ const routeApi = getRouteApi('/posts/$postId')
1379
+
1380
+ function PostDetail() {
1381
+ const { postId } = routeApi.useParams()
1382
+ const { post } = routeApi.useLoaderData()
1383
+ return <div>{post.title}</div>
1384
+ }
1385
+
1386
+ // ===== Deferred Data =====
1387
+ export const Route = createFileRoute('/posts/$postId')({
1388
+ loader: async ({ params }) => {
1389
+ const post = await getPost(params.postId) // 중요: await
1390
+ const deferredComments = getComments(params.postId) // 비중요: Promise
1391
+ return { post, deferredComments }
1392
+ },
1393
+ })
1394
+
1395
+ function PostPage() {
1396
+ const { post, deferredComments } = Route.useLoaderData()
1397
+ return (
1398
+ <div>
1399
+ <PostContent post={post} />
1400
+ <Suspense fallback={<CommentsSkeleton />}>
1401
+ <Await promise={deferredComments}>
1402
+ {(comments) => <Comments comments={comments} />}
1403
+ </Await>
1404
+ </Suspense>
1405
+ </div>
1406
+ )
1407
+ }
1408
+
1409
+ // ===== Preloading =====
1410
+ const router = createRouter({
1411
+ routeTree,
1412
+ defaultPreload: 'intent', // hover 50ms 후 프리로드
1413
+ defaultPreloadDelay: 50,
1414
+ defaultPreloadStaleTime: 30_000, // 30초간 fresh
1415
+ })
1416
+
1417
+ // ===== SWR Caching =====
1418
+ export const Route = createFileRoute('/posts')({
1419
+ loader: async () => ({ posts: await getPosts() }),
1420
+ staleTime: 10_000, // 10초간 fresh
1421
+ gcTime: 60_000, // 1분간 메모리 유지
1422
+ })
1423
+
1424
+ const router = useRouter()
1425
+ router.invalidate() // 캐시 무효화
1426
+
1427
+ // ===== TanStack Query 통합 =====
1428
+ const postQueryOptions = (postId: string) =>
1429
+ queryOptions({
1430
+ queryKey: ['posts', postId],
1431
+ queryFn: () => getPost(postId),
1432
+ })
1433
+
1434
+ export const Route = createFileRoute('/posts/$postId')({
1435
+ loader: async ({ params, context }) => {
1436
+ await context.queryClient.ensureQueryData(postQueryOptions(params.postId))
1437
+ },
1438
+ })
1439
+
1440
+ function PostPage() {
1441
+ const { postId } = Route.useParams()
1442
+ const { data: post } = useSuspenseQuery(postQueryOptions(postId))
1443
+ return <div>{post.title}</div>
1444
+ }
661
1445
  ```
662
1446
 
663
1447
  </patterns>