@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.
- package/dist/index.js +7 -1
- package/package.json +1 -1
- package/templates/.claude/agents/analyst.md +5 -0
- package/templates/.claude/agents/architect.md +5 -0
- package/templates/.claude/agents/build-fixer.md +1 -0
- package/templates/.claude/agents/code-reviewer.md +1 -0
- package/templates/.claude/agents/critic.md +4 -0
- package/templates/.claude/agents/deep-executor.md +1 -0
- package/templates/.claude/agents/dependency-manager.md +2 -0
- package/templates/.claude/agents/deployment-validator.md +2 -0
- package/templates/.claude/agents/designer.md +2 -0
- package/templates/.claude/agents/document-writer.md +3 -0
- package/templates/.claude/agents/explore.md +1 -0
- package/templates/.claude/agents/git-operator.md +2 -0
- package/templates/.claude/agents/implementation-executor.md +2 -0
- package/templates/.claude/agents/ko-to-en-translator.md +3 -0
- package/templates/.claude/agents/lint-fixer.md +2 -0
- package/templates/.claude/agents/planner.md +3 -0
- package/templates/.claude/agents/pm.md +349 -0
- package/templates/.claude/agents/qa-tester.md +1 -0
- package/templates/.claude/agents/refactor-advisor.md +4 -0
- package/templates/.claude/agents/researcher.md +9 -1
- package/templates/.claude/agents/scientist.md +1 -0
- package/templates/.claude/agents/security-reviewer.md +1 -0
- package/templates/.claude/agents/tdd-guide.md +1 -0
- package/templates/.claude/agents/vision.md +1 -0
- package/templates/.claude/instructions/agent-patterns/agent-teams-usage.md +376 -0
- package/templates/.claude/instructions/sourcing/reliable-search.md +49 -2
- package/templates/.claude/scripts/agent-teams/check-availability.sh +238 -0
- package/templates/.claude/scripts/agent-teams/setup-tmux.sh +125 -0
- package/templates/.claude/skills/agent-teams-setup/SKILL.md +460 -0
- package/templates/.claude/skills/brainstorm/SKILL.md +1 -0
- package/templates/.claude/skills/bug-fix/SKILL.md +1 -0
- package/templates/.claude/skills/crawler/SKILL.md +2 -0
- package/templates/.claude/skills/docs-creator/SKILL.md +1 -0
- package/templates/.claude/skills/docs-fetch/SKILL.md +6 -4
- package/templates/.claude/skills/docs-refactor/SKILL.md +1 -0
- package/templates/.claude/skills/elon-musk/SKILL.md +1 -0
- package/templates/.claude/skills/execute/SKILL.md +1 -0
- package/templates/.claude/skills/feedback/SKILL.md +1 -0
- package/templates/.claude/skills/figma-to-code/SKILL.md +1 -0
- package/templates/.claude/skills/genius-thinking/SKILL.md +1 -0
- package/templates/.claude/skills/global-uiux-design/SKILL.md +1 -0
- package/templates/.claude/skills/korea-uiux-design/SKILL.md +1 -0
- package/templates/.claude/skills/nextjs-react-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/plan/SKILL.md +1 -0
- package/templates/.claude/skills/prd/SKILL.md +1 -0
- package/templates/.claude/skills/project-optimizer/AGENTS.md +275 -0
- package/templates/.claude/skills/project-optimizer/SKILL.md +375 -0
- package/templates/.claude/skills/project-optimizer/rules/arch-config-centralize.md +66 -0
- package/templates/.claude/skills/project-optimizer/rules/arch-hot-path.md +35 -0
- package/templates/.claude/skills/project-optimizer/rules/arch-interface-segregation.md +51 -0
- package/templates/.claude/skills/project-optimizer/rules/arch-module-boundary.md +42 -0
- package/templates/.claude/skills/project-optimizer/rules/build-cache.md +57 -0
- package/templates/.claude/skills/project-optimizer/rules/build-code-split.md +56 -0
- package/templates/.claude/skills/project-optimizer/rules/build-incremental.md +65 -0
- package/templates/.claude/skills/project-optimizer/rules/build-minify.md +61 -0
- package/templates/.claude/skills/project-optimizer/rules/build-tree-shake.md +60 -0
- package/templates/.claude/skills/project-optimizer/rules/code-complexity.md +65 -0
- package/templates/.claude/skills/project-optimizer/rules/code-dead-elimination.md +32 -0
- package/templates/.claude/skills/project-optimizer/rules/code-duplication.md +54 -0
- package/templates/.claude/skills/project-optimizer/rules/code-error-handling.md +75 -0
- package/templates/.claude/skills/project-optimizer/rules/code-naming.md +52 -0
- package/templates/.claude/skills/project-optimizer/rules/concurrency-defer-await.md +54 -0
- package/templates/.claude/skills/project-optimizer/rules/concurrency-parallel.md +90 -0
- package/templates/.claude/skills/project-optimizer/rules/concurrency-pipeline.md +68 -0
- package/templates/.claude/skills/project-optimizer/rules/concurrency-pool.md +68 -0
- package/templates/.claude/skills/project-optimizer/rules/deps-lightweight-alt.md +37 -0
- package/templates/.claude/skills/project-optimizer/rules/deps-peer-align.md +44 -0
- package/templates/.claude/skills/project-optimizer/rules/deps-security-audit.md +45 -0
- package/templates/.claude/skills/project-optimizer/rules/deps-unused-removal.md +25 -0
- package/templates/.claude/skills/project-optimizer/rules/deps-version-pin.md +40 -0
- package/templates/.claude/skills/project-optimizer/rules/dx-ci-speed.md +47 -0
- package/templates/.claude/skills/project-optimizer/rules/dx-dev-server.md +35 -0
- package/templates/.claude/skills/project-optimizer/rules/dx-lint-config.md +36 -0
- package/templates/.claude/skills/project-optimizer/rules/dx-test-coverage.md +34 -0
- package/templates/.claude/skills/project-optimizer/rules/dx-type-safety.md +49 -0
- package/templates/.claude/skills/project-optimizer/rules/io-batch-queries.md +67 -0
- package/templates/.claude/skills/project-optimizer/rules/io-cache-layer.md +67 -0
- package/templates/.claude/skills/project-optimizer/rules/io-connection-reuse.md +67 -0
- package/templates/.claude/skills/project-optimizer/rules/io-serialize-minimal.md +61 -0
- package/templates/.claude/skills/project-optimizer/rules/io-stream.md +75 -0
- package/templates/.claude/skills/project-optimizer/rules/memory-bounded-cache.md +65 -0
- package/templates/.claude/skills/project-optimizer/rules/memory-large-data.md +64 -0
- package/templates/.claude/skills/project-optimizer/rules/memory-lazy-init.md +78 -0
- package/templates/.claude/skills/project-optimizer/rules/memory-leak-prevention.md +79 -0
- package/templates/.claude/skills/project-optimizer/rules/memory-pool-reuse.md +70 -0
- package/templates/.claude/skills/ralph/SKILL.md +1 -0
- package/templates/.claude/skills/refactor/SKILL.md +1 -0
- package/templates/.claude/skills/research/SKILL.md +1 -0
- package/templates/.claude/skills/sql-optimizer/SKILL.md +438 -0
- package/templates/.claude/skills/sql-optimizer/orm-patterns.md +218 -0
- package/templates/.claude/skills/startup-validator/SKILL.md +1 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/AGENTS.md +53 -14
- package/templates/.claude/skills/tanstack-start-react-best-practices/SKILL.md +94 -27
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/bundle-defer-third-party.md +42 -19
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/client-optimistic-updates.md +109 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/client-suspense-query.md +74 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/client-use-hook.md +81 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/rerender-react-compiler.md +81 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-beforeload-auth.md +121 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-file-conventions.md +104 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-link-navigation.md +119 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-nested-layouts.md +155 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-path-params.md +89 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-pending-component.md +110 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-preload-strategy.md +91 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-router-context.md +120 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-search-params.md +114 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-deferred-data.md +1 -1
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-error-boundaries.md +79 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-middleware.md +85 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-serialization.md +56 -21
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-streaming.md +84 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-validator.md +71 -0
- package/templates/.claude/skills/tauri-react-best-practices/AGENTS.md +527 -0
- package/templates/.claude/skills/tauri-react-best-practices/SKILL.md +571 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-barrel-imports.md +140 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-cargo-profile.md +96 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-frontend-treeshake.md +242 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-lazy-components.md +255 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-remove-unused-commands.md +160 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/deploy-ci-pipeline.md +269 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/deploy-signing.md +207 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/deploy-updater.md +226 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-async-commands.md +172 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-batch-commands.md +133 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-binary-response.md +198 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-channel-streaming.md +186 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-error-handling.md +250 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-type-safe.md +227 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/perf-derived-state.md +231 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/perf-functional-setstate.md +191 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/perf-index-maps.md +276 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/perf-lazy-state-init.md +196 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/plugin-lifecycle.md +265 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/plugin-mobile-compat.md +199 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/plugin-permission-scope.md +193 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/react-error-boundary.md +239 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/react-event-listener.md +151 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/react-file-src.md +155 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/react-invoke-hook.md +139 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/react-optimistic-update.md +211 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/security-capability-split.md +205 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/security-csp.md +207 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/security-least-privilege.md +106 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/security-no-wildcard.md +253 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/security-scope-paths.md +160 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/state-async-mutex.md +270 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/state-mutex-pattern.md +265 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/state-react-sync.md +375 -0
- package/templates/.claude/skills/tauri-react-best-practices/rules/state-single-container.md +275 -0
- package/templates/tanstack-start/docs/architecture.md +238 -167
- package/templates/tanstack-start/docs/library/tanstack-router/error-handling.md +777 -38
- package/templates/tanstack-start/docs/library/tanstack-router/hooks.md +549 -37
- package/templates/tanstack-start/docs/library/tanstack-router/index.md +895 -111
- package/templates/tanstack-start/docs/library/tanstack-router/navigation.md +641 -43
- package/templates/tanstack-start/docs/library/tanstack-router/route-context.md +889 -38
- package/templates/tanstack-start/docs/library/tanstack-router/search-params.md +891 -29
- package/templates/tanstack-start/docs/library/tanstack-start/auth-patterns.md +972 -36
- package/templates/tanstack-start/docs/library/tanstack-start/index.md +1525 -881
- package/templates/tanstack-start/docs/library/tanstack-start/middleware.md +1099 -20
- package/templates/tanstack-start/docs/library/tanstack-start/routing.md +796 -30
- package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +953 -35
- package/templates/tanstack-start/docs/library/tanstack-start/setup.md +371 -15
- package/templates/tauri/CLAUDE.md +189 -0
- package/templates/tauri/docs/guides/distribution.md +261 -0
- package/templates/tauri/docs/guides/getting-started.md +302 -0
- package/templates/tauri/docs/guides/mobile.md +288 -0
- 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
|
|
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,
|
|
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
|
-
│
|
|
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:
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
<
|
|
725
|
+
<preloading>
|
|
726
|
+
|
|
727
|
+
## Preloading
|
|
728
|
+
|
|
729
|
+
**Link 프리로딩 모드:**
|
|
228
730
|
|
|
229
|
-
|
|
731
|
+
| 모드 | 트리거 | 사용 시점 |
|
|
732
|
+
|------|--------|---------|
|
|
733
|
+
| `'intent'` | hover/touch 50ms 후 | 일반적인 네비게이션 링크 (권장) |
|
|
734
|
+
| `'viewport'` | 뷰포트 진입 시 (IntersectionObserver) | 목록 페이지, 무한 스크롤 |
|
|
735
|
+
| `'render'` | 컴포넌트 마운트 시 | 확실히 이동할 링크 |
|
|
736
|
+
| `false` | 비활성 | 인증/외부 링크 |
|
|
737
|
+
|
|
738
|
+
**Router 레벨 기본값:**
|
|
230
739
|
|
|
231
740
|
```tsx
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
782
|
+
**커스텀 프리로드 지연:**
|
|
258
783
|
|
|
259
784
|
```tsx
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
785
|
+
<Link to="/posts" preload="intent" preloadDelay={100}>
|
|
786
|
+
Posts (100ms 지연)
|
|
787
|
+
</Link>
|
|
788
|
+
```
|
|
263
789
|
|
|
264
|
-
|
|
265
|
-
const Pagination = () => {
|
|
266
|
-
const navigate = useNavigate()
|
|
267
|
-
const { page } = Route.useSearch()
|
|
790
|
+
</preloading>
|
|
268
791
|
|
|
269
|
-
|
|
270
|
-
navigate({ to: '/products', search: prev => ({ ...prev, page: newPage }) })
|
|
271
|
-
}
|
|
792
|
+
---
|
|
272
793
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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
|
-
|
|
298
|
-
<
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
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()
|
|
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()
|
|
410
|
-
const { postId } = Route.useParams()
|
|
411
|
-
const { page, sort } = Route.useSearch()
|
|
412
|
-
const { user } = Route.useRouteContext()
|
|
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 })
|
|
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 })
|
|
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)
|
|
443
|
-
console.log(location.search)
|
|
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
|
-
|
|
1163
|
+
**Root Mode:** Root의 `notFoundComponent` 표시
|
|
1164
|
+
|
|
1165
|
+
```tsx
|
|
503
1166
|
export const Route = createRootRoute({
|
|
504
|
-
|
|
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,
|
|
521
|
-
pendingMinMs: 500,
|
|
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:
|
|
608
|
-
|
|
609
|
-
|
|
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 }) =>
|
|
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>
|