@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
|
@@ -1,89 +1,828 @@
|
|
|
1
1
|
# TanStack Router - Error Handling
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> TanStack Router v1.159.4
|
|
4
|
+
|
|
5
|
+
에러 경계, 404 처리, 로딩 상태를 다룬다.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<error_component>
|
|
10
|
+
|
|
11
|
+
## errorComponent: 일반 에러 처리
|
|
12
|
+
|
|
13
|
+
Loader나 beforeLoad에서 throw한 Error를 잡는다.
|
|
14
|
+
|
|
15
|
+
### 기본 사용
|
|
4
16
|
|
|
5
17
|
```tsx
|
|
6
|
-
|
|
18
|
+
import { ErrorComponentProps } from '@tanstack/react-router'
|
|
19
|
+
|
|
7
20
|
export const Route = createFileRoute('/posts/$postId')({
|
|
8
21
|
loader: async ({ params }) => {
|
|
9
22
|
const post = await getPost(params.postId)
|
|
10
|
-
if (!post)
|
|
23
|
+
if (!post) {
|
|
24
|
+
// 임의의 Error throw
|
|
25
|
+
throw new Error(`Post ${params.postId} not found`)
|
|
26
|
+
}
|
|
11
27
|
return { post }
|
|
12
28
|
},
|
|
13
29
|
errorComponent: PostError,
|
|
14
|
-
component:
|
|
30
|
+
component: PostDetail,
|
|
15
31
|
})
|
|
16
32
|
|
|
17
33
|
const PostError = ({ error, reset }: ErrorComponentProps) => (
|
|
18
|
-
<div>
|
|
34
|
+
<div className="error-container">
|
|
19
35
|
<h2>Error loading post</h2>
|
|
20
36
|
<p>{error.message}</p>
|
|
21
37
|
<button onClick={reset}>Retry</button>
|
|
22
38
|
</div>
|
|
23
39
|
)
|
|
24
40
|
|
|
25
|
-
|
|
41
|
+
const PostDetail = () => {
|
|
42
|
+
const { post } = Route.useLoaderData()
|
|
43
|
+
return <h1>{post.title}</h1>
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### ErrorComponentProps
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
interface ErrorComponentProps {
|
|
51
|
+
error: Error
|
|
52
|
+
reset: () => void // Retry 버튼: loader와 beforeLoad 재실행
|
|
53
|
+
info?: string
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 비동기 에러
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
export const Route = createFileRoute('/users')({
|
|
61
|
+
loader: async () => {
|
|
62
|
+
try {
|
|
63
|
+
return await fetchUsers()
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// 에러를 명시적으로 throw
|
|
66
|
+
throw new Error(`Failed to fetch users: ${error.message}`)
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
errorComponent: ({ error, reset }) => (
|
|
70
|
+
<div>
|
|
71
|
+
<p>{error.message}</p>
|
|
72
|
+
<button onClick={reset}>Try again</button>
|
|
73
|
+
</div>
|
|
74
|
+
),
|
|
75
|
+
component: UsersList,
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 에러 타입 구분
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
const CustomError = ({ error, reset }: ErrorComponentProps) => {
|
|
83
|
+
// Network error
|
|
84
|
+
if (error instanceof TypeError && error.message.includes('fetch')) {
|
|
85
|
+
return (
|
|
86
|
+
<div>
|
|
87
|
+
<p>Network connection error. Please check your internet.</p>
|
|
88
|
+
<button onClick={reset}>Retry</button>
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 인증 에러
|
|
94
|
+
if (error.message.includes('unauthorized') || error.message.includes('401')) {
|
|
95
|
+
return <Navigate to="/login" />
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Validation 에러
|
|
99
|
+
if (error.message.includes('validation')) {
|
|
100
|
+
return <div>Invalid data provided. Please try again.</div>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 기타 에러
|
|
104
|
+
return (
|
|
105
|
+
<div>
|
|
106
|
+
<p>Something went wrong</p>
|
|
107
|
+
<details>
|
|
108
|
+
<summary>Details</summary>
|
|
109
|
+
<pre>{error.message}</pre>
|
|
110
|
+
</details>
|
|
111
|
+
<button onClick={reset}>Retry</button>
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const Route = createFileRoute('/dashboard')({
|
|
117
|
+
loader: async () => {
|
|
118
|
+
// ...
|
|
119
|
+
},
|
|
120
|
+
errorComponent: CustomError,
|
|
121
|
+
component: Dashboard,
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### DefaultErrorComponent로 폴백
|
|
126
|
+
|
|
127
|
+
커스텀 에러에서 처리 못하는 에러는 기본 컴포넌트로 위임.
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
import { ErrorComponent } from '@tanstack/react-router'
|
|
131
|
+
|
|
132
|
+
export const Route = createFileRoute('/posts')({
|
|
133
|
+
loader: () => fetchPosts(),
|
|
134
|
+
errorComponent: ({ error }) => {
|
|
135
|
+
if (error instanceof MyCustomError) {
|
|
136
|
+
return <div>{error.message}</div>
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 기본 ErrorComponent로 폴백 (항상 추천)
|
|
140
|
+
return <ErrorComponent error={error} />
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 상위로 에러 전파
|
|
146
|
+
|
|
147
|
+
errorComponent가 없으면 부모 라우트로 전파.
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
// /src/routes/posts/$postId.tsx (errorComponent 없음)
|
|
151
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
152
|
+
loader: async ({ params }) => {
|
|
153
|
+
throw new Error('Failed to load post')
|
|
154
|
+
},
|
|
155
|
+
component: PostDetail,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// /src/routes/posts.tsx (errorComponent 있음)
|
|
159
|
+
export const Route = createFileRoute('/posts')({
|
|
160
|
+
errorComponent: PostsError, // /posts/$postId의 에러도 여기서 잡힘
|
|
161
|
+
component: () => <Outlet />,
|
|
162
|
+
})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### onError / onCatch 콜백
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
export const Route = createFileRoute('/posts')({
|
|
169
|
+
loader: () => fetchPosts(),
|
|
170
|
+
// 에러 로깅
|
|
171
|
+
onError: ({ error }) => {
|
|
172
|
+
console.error('Loader error:', error)
|
|
173
|
+
},
|
|
174
|
+
// CatchBoundary에서 잡힌 에러
|
|
175
|
+
onCatch: ({ error, errorInfo }) => {
|
|
176
|
+
console.error('Caught error:', error)
|
|
177
|
+
},
|
|
178
|
+
errorComponent: PostsError,
|
|
179
|
+
component: PostsList,
|
|
180
|
+
})
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
</error_component>
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
<not_found_component>
|
|
188
|
+
|
|
189
|
+
## notFoundComponent: 404 처리
|
|
190
|
+
|
|
191
|
+
notFound() 함수로 throw하면 notFoundComponent 표시.
|
|
192
|
+
|
|
193
|
+
### notFoundMode 옵션
|
|
194
|
+
|
|
195
|
+
| 모드 | 설명 |
|
|
196
|
+
|------|------|
|
|
197
|
+
| `fuzzy` (기본) | 가장 가까운 부모 라우트의 notFoundComponent 사용. 부모 레이아웃 최대 보존. |
|
|
198
|
+
| `root` | 모든 404를 root의 notFoundComponent에서 처리 |
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
const router = createRouter({
|
|
202
|
+
routeTree,
|
|
203
|
+
notFoundMode: 'fuzzy', // 기본값
|
|
204
|
+
})
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### fuzzy 모드 동작 원리
|
|
208
|
+
|
|
209
|
+
`/posts/1/edit` 접근 시 (edit 라우트가 없는 경우):
|
|
210
|
+
|
|
211
|
+
- `__root__` (notFoundComponent 있음)
|
|
212
|
+
- `posts` (notFoundComponent 있음) -> 여기서 처리 (가장 가까운 부모)
|
|
213
|
+
- `$postId`
|
|
214
|
+
|
|
215
|
+
가장 가까운 적합한 부모 조건:
|
|
216
|
+
1. 자식 라우트를 가짐 (Outlet이 있음)
|
|
217
|
+
2. notFoundComponent가 설정됨
|
|
218
|
+
|
|
219
|
+
### 기본 사용
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
import { notFound } from '@tanstack/react-router'
|
|
223
|
+
|
|
26
224
|
export const Route = createFileRoute('/posts/$postId')({
|
|
27
225
|
loader: async ({ params }) => {
|
|
28
226
|
const post = await getPost(params.postId)
|
|
29
|
-
|
|
227
|
+
|
|
228
|
+
// Post 없으면 404
|
|
229
|
+
if (!post) {
|
|
230
|
+
throw notFound({
|
|
231
|
+
data: { searchedId: params.postId }
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
30
235
|
return { post }
|
|
31
236
|
},
|
|
32
|
-
notFoundComponent:
|
|
33
|
-
component:
|
|
237
|
+
notFoundComponent: PostNotFound,
|
|
238
|
+
component: PostDetail,
|
|
34
239
|
})
|
|
35
240
|
|
|
36
|
-
|
|
241
|
+
const PostNotFound = ({ data }: { data?: { searchedId: string } }) => (
|
|
242
|
+
<div>
|
|
243
|
+
<h2>Post not found</h2>
|
|
244
|
+
<p>Could not find post {data?.searchedId}</p>
|
|
245
|
+
<Link to="/posts">Back to posts</Link>
|
|
246
|
+
</div>
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
const PostDetail = () => {
|
|
250
|
+
const { post } = Route.useLoaderData()
|
|
251
|
+
return <h1>{post.title}</h1>
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Root 404 (전역 핸들링)
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
// /src/routes/__root.tsx
|
|
37
259
|
export const Route = createRootRoute({
|
|
38
260
|
component: RootLayout,
|
|
39
|
-
notFoundComponent:
|
|
261
|
+
notFoundComponent: NotFound, // 모든 404 처리
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
const NotFound = () => (
|
|
265
|
+
<div>
|
|
266
|
+
<h1>404 - Page Not Found</h1>
|
|
267
|
+
<Link to="/">Go Home</Link>
|
|
268
|
+
</div>
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
const RootLayout = () => (
|
|
272
|
+
<div>
|
|
273
|
+
<nav>{/* ... */}</nav>
|
|
274
|
+
<Outlet />
|
|
275
|
+
</div>
|
|
276
|
+
)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### defaultNotFoundComponent (라우터 수준)
|
|
280
|
+
|
|
281
|
+
모든 라우트에 기본 404 컴포넌트 적용.
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
const router = createRouter({
|
|
285
|
+
routeTree,
|
|
286
|
+
defaultNotFoundComponent: () => (
|
|
40
287
|
<div>
|
|
41
|
-
<
|
|
42
|
-
<Link to="/">Go
|
|
288
|
+
<p>Not found!</p>
|
|
289
|
+
<Link to="/">Go home</Link>
|
|
43
290
|
</div>
|
|
44
291
|
),
|
|
45
292
|
})
|
|
293
|
+
```
|
|
46
294
|
|
|
47
|
-
|
|
295
|
+
### 특정 라우트로 404 전파 (routeId)
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
import { rootRouteId } from '@tanstack/react-router'
|
|
299
|
+
|
|
300
|
+
// 특정 부모 라우트의 notFoundComponent 사용
|
|
301
|
+
export const Route = createFileRoute('/_pathless/route-a')({
|
|
302
|
+
loader: async () => {
|
|
303
|
+
throw notFound({ routeId: '/_pathlessLayout' })
|
|
304
|
+
// ^^^^^^^^^ 자동완성 지원
|
|
305
|
+
},
|
|
306
|
+
// 이 notFoundComponent는 렌더링되지 않음
|
|
307
|
+
notFoundComponent: () => <p>여기는 안 보임</p>,
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
// root 라우트로 직접 전파
|
|
311
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
312
|
+
loader: async ({ params: { postId } }) => {
|
|
313
|
+
const post = await getPost(postId)
|
|
314
|
+
if (!post) throw notFound({ routeId: rootRouteId })
|
|
315
|
+
return { post }
|
|
316
|
+
},
|
|
317
|
+
})
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### notFound() 사용 패턴
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
// 단순 throw
|
|
324
|
+
throw notFound()
|
|
325
|
+
|
|
326
|
+
// 데이터 전달
|
|
327
|
+
throw notFound({
|
|
328
|
+
data: {
|
|
329
|
+
resourceType: 'Post',
|
|
330
|
+
searchedId: params.postId,
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
// 특정 라우트 지정
|
|
335
|
+
throw notFound({ routeId: '/_pathlessLayout' })
|
|
336
|
+
|
|
337
|
+
// throw 옵션
|
|
338
|
+
notFound({ throw: true }) // throw 대신 함수 내에서 throw
|
|
339
|
+
|
|
340
|
+
// beforeLoad에서도 사용 가능 (항상 __root notFoundComponent 사용)
|
|
341
|
+
export const Route = createFileRoute('/admin')({
|
|
342
|
+
beforeLoad: async ({ context }) => {
|
|
343
|
+
if (!context.user?.isAdmin) {
|
|
344
|
+
throw notFound()
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
component: AdminPanel,
|
|
348
|
+
})
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
> 주의: beforeLoad에서 notFound()를 throw하면 항상 `__root`의 notFoundComponent가 사용됨. 이는 beforeLoad가 loader 전에 실행되므로 레이아웃 데이터 로딩이 보장되지 않기 때문.
|
|
352
|
+
|
|
353
|
+
### notFound() vs Error throw
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
357
|
+
loader: async ({ params }) => {
|
|
358
|
+
const post = await getPost(params.postId)
|
|
359
|
+
|
|
360
|
+
if (!post) {
|
|
361
|
+
// 리소스 없음: notFound() 사용
|
|
362
|
+
throw notFound()
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// API 에러로 실패
|
|
366
|
+
if (!post.data) {
|
|
367
|
+
// 예상치 못한 에러: Error throw
|
|
368
|
+
throw new Error('Invalid post data')
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return { post }
|
|
372
|
+
},
|
|
373
|
+
notFoundComponent: () => <div>Post not found</div>,
|
|
374
|
+
errorComponent: ({ error }) => <div>{error.message}</div>,
|
|
375
|
+
component: PostDetail,
|
|
376
|
+
})
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
</not_found_component>
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
<catch_not_found>
|
|
384
|
+
|
|
385
|
+
## CatchNotFound: 컴포넌트 레벨 404
|
|
386
|
+
|
|
387
|
+
컴포넌트 내부에서 notFound() throw 가능.
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
import { CatchNotFound } from '@tanstack/react-router'
|
|
391
|
+
|
|
392
|
+
export const Route = createFileRoute('/products/$productId')({
|
|
393
|
+
component: () => (
|
|
394
|
+
<CatchNotFound fallback={<div>Product not found</div>}>
|
|
395
|
+
<ProductDetail />
|
|
396
|
+
</CatchNotFound>
|
|
397
|
+
),
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
const ProductDetail = () => {
|
|
401
|
+
const { productId } = Route.useParams()
|
|
402
|
+
const product = useQuery({
|
|
403
|
+
queryKey: ['products', productId],
|
|
404
|
+
queryFn: async () => {
|
|
405
|
+
const res = await getProduct(productId)
|
|
406
|
+
if (!res) throw notFound() // 컴포넌트에서 throw
|
|
407
|
+
return res
|
|
408
|
+
},
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
if (product.isPending) return <Spinner />
|
|
412
|
+
|
|
413
|
+
return <h1>{product.data.name}</h1>
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
> 권장: 가능하면 loader에서 notFound()를 throw. 컴포넌트에서 throw하면 loader 데이터 타입이 불안정하고 UI 깜빡임이 발생할 수 있음.
|
|
418
|
+
|
|
419
|
+
</catch_not_found>
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
<pending_component>
|
|
424
|
+
|
|
425
|
+
## pendingComponent: 로딩 상태
|
|
426
|
+
|
|
427
|
+
Loader 실행 중일 때 표시. **기본 1000ms** 이후에 노출.
|
|
428
|
+
|
|
429
|
+
### 기본 사용
|
|
430
|
+
|
|
431
|
+
```tsx
|
|
48
432
|
export const Route = createFileRoute('/posts')({
|
|
49
|
-
loader: async () =>
|
|
433
|
+
loader: async () => {
|
|
434
|
+
// 로딩 시뮬레이션 (1초)
|
|
435
|
+
await new Promise(r => setTimeout(r, 1000))
|
|
436
|
+
return fetchPosts()
|
|
437
|
+
},
|
|
438
|
+
// 1000ms 후 표시
|
|
50
439
|
pendingComponent: () => <Spinner />,
|
|
51
|
-
|
|
52
|
-
pendingMinMs: 500, // 최소 500ms 유지 (깜빡임 방지)
|
|
53
|
-
component: PostsPage,
|
|
440
|
+
component: PostsList,
|
|
54
441
|
})
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### pendingMs & pendingMinMs
|
|
445
|
+
|
|
446
|
+
```tsx
|
|
447
|
+
export const Route = createFileRoute('/dashboard')({
|
|
448
|
+
loader: async () => fetchDashboard(),
|
|
449
|
+
|
|
450
|
+
// 로딩이 200ms 이상 걸리면 표시
|
|
451
|
+
pendingMs: 200,
|
|
452
|
+
|
|
453
|
+
// 최소 500ms 표시 (깜빡임 방지)
|
|
454
|
+
pendingMinMs: 500,
|
|
455
|
+
|
|
456
|
+
pendingComponent: () => <LoadingScreen />,
|
|
457
|
+
component: Dashboard,
|
|
458
|
+
})
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### 라우터 수준 기본값
|
|
462
|
+
|
|
463
|
+
```tsx
|
|
464
|
+
const router = createRouter({
|
|
465
|
+
routeTree,
|
|
466
|
+
defaultPendingMs: 200, // 기본 대기 시간
|
|
467
|
+
defaultPendingMinMs: 300, // 기본 최소 표시 시간
|
|
468
|
+
})
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### 커스텀 로딩 UI
|
|
472
|
+
|
|
473
|
+
```tsx
|
|
474
|
+
const LoadingSpinner = () => (
|
|
475
|
+
<div className="flex items-center justify-center">
|
|
476
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900" />
|
|
477
|
+
<span>Loading...</span>
|
|
478
|
+
</div>
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
export const Route = createFileRoute('/blog')({
|
|
482
|
+
loader: async () => fetchBlogPosts(),
|
|
483
|
+
pendingComponent: LoadingSpinner,
|
|
484
|
+
pendingMs: 100,
|
|
485
|
+
pendingMinMs: 300,
|
|
486
|
+
component: Blog,
|
|
487
|
+
})
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### 스켈레톤 화면
|
|
491
|
+
|
|
492
|
+
```tsx
|
|
493
|
+
const PostSkeleton = () => (
|
|
494
|
+
<div className="space-y-4">
|
|
495
|
+
{[1, 2, 3].map(i => (
|
|
496
|
+
<div key={i} className="h-20 bg-gray-200 rounded animate-pulse" />
|
|
497
|
+
))}
|
|
498
|
+
</div>
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
export const Route = createFileRoute('/posts')({
|
|
502
|
+
loader: async () => fetchPosts(),
|
|
503
|
+
pendingComponent: PostSkeleton,
|
|
504
|
+
component: PostsList,
|
|
505
|
+
})
|
|
506
|
+
```
|
|
55
507
|
|
|
56
|
-
|
|
508
|
+
</pending_component>
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
<catch_all>
|
|
513
|
+
|
|
514
|
+
## Catch-all Route: 경로 매칭 실패
|
|
515
|
+
|
|
516
|
+
routes/$.tsx로 존재하지 않는 모든 경로 캐치.
|
|
517
|
+
|
|
518
|
+
### 기본 설정
|
|
519
|
+
|
|
520
|
+
```tsx
|
|
521
|
+
// /src/routes/$.tsx
|
|
522
|
+
export const Route = createFileRoute('/$')({
|
|
523
|
+
component: NotFoundPage,
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
const NotFoundPage = () => {
|
|
527
|
+
const { _splat } = Route.useParams()
|
|
528
|
+
|
|
529
|
+
return (
|
|
530
|
+
<div className="text-center">
|
|
531
|
+
<h1>404 - Not Found</h1>
|
|
532
|
+
<p>The page /{_splat} does not exist.</p>
|
|
533
|
+
<Link to="/">Go Home</Link>
|
|
534
|
+
</div>
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### _splat 파라미터
|
|
540
|
+
|
|
541
|
+
_splat은 매칭되지 않은 나머지 경로 문자열.
|
|
542
|
+
|
|
543
|
+
```tsx
|
|
544
|
+
// /users/123/settings 요청
|
|
545
|
+
// _splat = 'users/123/settings'
|
|
546
|
+
|
|
547
|
+
const DetailedNotFound = () => {
|
|
548
|
+
const { _splat } = Route.useParams()
|
|
549
|
+
const segments = _splat?.split('/') ?? []
|
|
550
|
+
|
|
551
|
+
return (
|
|
552
|
+
<div>
|
|
553
|
+
<h1>Page Not Found</h1>
|
|
554
|
+
<p>Requested path: /{_splat}</p>
|
|
555
|
+
<p>Segments: {segments.join(', ')}</p>
|
|
556
|
+
<Link to="/">Home</Link>
|
|
557
|
+
</div>
|
|
558
|
+
)
|
|
559
|
+
}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### errorComponent와 함께 사용
|
|
563
|
+
|
|
564
|
+
```tsx
|
|
57
565
|
export const Route = createFileRoute('/$')({
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
566
|
+
errorComponent: CatchAllError,
|
|
567
|
+
component: CatchAllNotFound,
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
const CatchAllError = ({ error }: ErrorComponentProps) => (
|
|
571
|
+
<div>
|
|
572
|
+
<h1>Error</h1>
|
|
573
|
+
<p>{error.message}</p>
|
|
574
|
+
</div>
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
const CatchAllNotFound = () => {
|
|
578
|
+
const { _splat } = Route.useParams()
|
|
579
|
+
return <div>Page /{_splat} not found</div>
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
</catch_all>
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
<priority>
|
|
588
|
+
|
|
589
|
+
## 에러 처리 우선순위
|
|
590
|
+
|
|
591
|
+
여러 상태가 동시에 발생할 때 표시 순서.
|
|
592
|
+
|
|
593
|
+
| 우선순위 | 컴포넌트 | 조건 | 표시 시기 |
|
|
594
|
+
|---------|---------|------|----------|
|
|
595
|
+
| 1 | `errorComponent` | loader/beforeLoad에서 Error throw | 에러 즉시 |
|
|
596
|
+
| 2 | `notFoundComponent` | `notFound()` throw | 404 즉시 |
|
|
597
|
+
| 3 | `pendingComponent` | loader 실행 중 (pendingMs 이후) | 로딩 진행 중 |
|
|
598
|
+
| 4 | `component` | 정상 데이터 | 완료 |
|
|
599
|
+
|
|
600
|
+
### 라우트 로딩 라이프사이클
|
|
601
|
+
|
|
602
|
+
```
|
|
603
|
+
Route Matching (Top-Down)
|
|
604
|
+
route.params.parse
|
|
605
|
+
route.validateSearch
|
|
606
|
+
|
|
|
607
|
+
Route Pre-Loading (Serial)
|
|
608
|
+
route.beforeLoad
|
|
609
|
+
route.onError -> errorComponent
|
|
610
|
+
|
|
|
611
|
+
Route Loading (Parallel)
|
|
612
|
+
route.component.preload?
|
|
613
|
+
route.loader
|
|
614
|
+
-> pendingComponent (pendingMs 초과 시)
|
|
615
|
+
-> component (완료)
|
|
616
|
+
route.onError -> errorComponent
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### 부모로 전파
|
|
620
|
+
|
|
621
|
+
```tsx
|
|
622
|
+
// 자식에 errorComponent 없으면 부모로 전파
|
|
623
|
+
// /src/routes/posts/$postId.tsx (errorComponent 없음)
|
|
624
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
625
|
+
loader: async () => {
|
|
626
|
+
throw new Error('Failed!')
|
|
61
627
|
},
|
|
628
|
+
component: PostDetail,
|
|
62
629
|
})
|
|
63
630
|
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
631
|
+
// /src/routes/posts.tsx (errorComponent 있음)
|
|
632
|
+
export const Route = createFileRoute('/posts')({
|
|
633
|
+
errorComponent: PostsError, // <- 여기서 잡힘
|
|
634
|
+
component: () => <Outlet />,
|
|
635
|
+
})
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
</priority>
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
<error_recovery>
|
|
643
|
+
|
|
644
|
+
## 에러 복구 (router.invalidate)
|
|
645
|
+
|
|
646
|
+
에러 후 상태 재설정. 인증 갱신, 데이터 재로드 등에 활용.
|
|
647
|
+
|
|
648
|
+
### reset() vs router.invalidate()
|
|
649
|
+
|
|
650
|
+
| 방법 | 용도 |
|
|
651
|
+
|------|------|
|
|
652
|
+
| `reset()` | 에러 바운더리 UI만 리셋 (컴포넌트 렌더링 에러) |
|
|
653
|
+
| `router.invalidate()` | loader 재실행 + 에러 바운더리 리셋 (데이터 로딩 에러) |
|
|
654
|
+
|
|
655
|
+
```tsx
|
|
656
|
+
// 데이터 로딩 에러: router.invalidate() 사용 (권장)
|
|
657
|
+
const ErrorWithRecovery = ({ error, reset }: ErrorComponentProps) => {
|
|
658
|
+
const router = useRouter()
|
|
659
|
+
|
|
660
|
+
return (
|
|
661
|
+
<div>
|
|
662
|
+
<p>{error.message}</p>
|
|
663
|
+
<button onClick={() => router.invalidate()}>
|
|
664
|
+
Retry & Refresh
|
|
665
|
+
</button>
|
|
666
|
+
</div>
|
|
667
|
+
)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// React Query와 함께 사용
|
|
671
|
+
const ErrorWithQueryRecovery = ({ error, reset }: ErrorComponentProps) => {
|
|
672
|
+
const router = useRouter()
|
|
673
|
+
const queryClient = useQueryClient()
|
|
674
|
+
|
|
675
|
+
const handleRetry = () => {
|
|
676
|
+
// 1. React Query 캐시 정리
|
|
677
|
+
queryClient.clear()
|
|
678
|
+
|
|
679
|
+
// 2. 라우터 상태 갱신 (loader 재실행 + 에러 바운더리 리셋)
|
|
680
|
+
router.invalidate()
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return (
|
|
684
|
+
<div>
|
|
685
|
+
<p>{error.message}</p>
|
|
686
|
+
<button onClick={handleRetry}>Retry & Refresh</button>
|
|
687
|
+
</div>
|
|
688
|
+
)
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### 인증 에러 후 로그인으로 리다이렉트
|
|
693
|
+
|
|
694
|
+
```tsx
|
|
695
|
+
const AuthError = ({ error, reset }: ErrorComponentProps) => {
|
|
696
|
+
const router = useRouter()
|
|
697
|
+
const navigate = useNavigate()
|
|
698
|
+
|
|
699
|
+
const handleLogin = () => {
|
|
700
|
+
// 인증 상태 갱신
|
|
701
|
+
router.invalidate()
|
|
702
|
+
|
|
703
|
+
// 로그인 페이지로 리다이렉트
|
|
704
|
+
navigate({
|
|
705
|
+
to: '/login',
|
|
706
|
+
search: { redirect: location.href },
|
|
707
|
+
})
|
|
68
708
|
}
|
|
709
|
+
|
|
69
710
|
if (error.message.includes('unauthorized')) {
|
|
70
|
-
return
|
|
711
|
+
return (
|
|
712
|
+
<div>
|
|
713
|
+
<p>Session expired. Please log in again.</p>
|
|
714
|
+
<button onClick={handleLogin}>Go to Login</button>
|
|
715
|
+
</div>
|
|
716
|
+
)
|
|
71
717
|
}
|
|
72
|
-
|
|
718
|
+
|
|
719
|
+
return <div>{error.message}</div>
|
|
73
720
|
}
|
|
74
721
|
```
|
|
75
722
|
|
|
76
|
-
</
|
|
723
|
+
</error_recovery>
|
|
77
724
|
|
|
78
|
-
|
|
725
|
+
---
|
|
79
726
|
|
|
80
|
-
|
|
81
|
-
|---------|---------|------|
|
|
82
|
-
| 1 | `errorComponent` | loader/beforeLoad에서 Error throw |
|
|
83
|
-
| 2 | `notFoundComponent` | `notFound()` throw |
|
|
84
|
-
| 3 | `pendingComponent` | loader 실행 중 (pendingMs 이후) |
|
|
85
|
-
| 4 | `component` | 정상 렌더링 |
|
|
727
|
+
<default_error>
|
|
86
728
|
|
|
87
|
-
|
|
729
|
+
## Default ErrorComponent
|
|
88
730
|
|
|
89
|
-
|
|
731
|
+
기본 에러 컴포넌트 import. 빠른 프로토타이핑용.
|
|
732
|
+
|
|
733
|
+
```tsx
|
|
734
|
+
import { DefaultErrorComponent } from '@tanstack/react-router'
|
|
735
|
+
|
|
736
|
+
export const Route = createFileRoute('/dashboard')({
|
|
737
|
+
loader: async () => fetchDashboard(),
|
|
738
|
+
errorComponent: DefaultErrorComponent,
|
|
739
|
+
component: Dashboard,
|
|
740
|
+
})
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
</default_error>
|
|
744
|
+
|
|
745
|
+
---
|
|
746
|
+
|
|
747
|
+
<data_access_in_error>
|
|
748
|
+
|
|
749
|
+
## 에러 상황에서 데이터 접근
|
|
750
|
+
|
|
751
|
+
각 에러 컴포넌트에서 접근 가능한 데이터.
|
|
752
|
+
|
|
753
|
+
### errorComponent
|
|
754
|
+
|
|
755
|
+
Loader 데이터 접근 불가 (에러 발생했으므로).
|
|
756
|
+
|
|
757
|
+
```tsx
|
|
758
|
+
const ErrorHandler = ({ error, reset }: ErrorComponentProps) => {
|
|
759
|
+
// loader 데이터 없음
|
|
760
|
+
// const data = Route.useLoaderData() // 에러 발생
|
|
761
|
+
|
|
762
|
+
// params, search, context는 접근 가능
|
|
763
|
+
const params = Route.useParams()
|
|
764
|
+
const search = Route.useSearch()
|
|
765
|
+
const context = Route.useRouteContext()
|
|
766
|
+
|
|
767
|
+
return (
|
|
768
|
+
<div>
|
|
769
|
+
<p>Error on route: {params}</p>
|
|
770
|
+
<button onClick={reset}>Retry</button>
|
|
771
|
+
</div>
|
|
772
|
+
)
|
|
773
|
+
}
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
### notFoundComponent
|
|
777
|
+
|
|
778
|
+
Loader 데이터 없음. params/search/context만 접근. data 옵션으로 불완전한 데이터 전달 가능.
|
|
779
|
+
|
|
780
|
+
```tsx
|
|
781
|
+
const NotFoundHandler = ({ data }: { data?: any }) => {
|
|
782
|
+
// Loader 데이터 없음
|
|
783
|
+
// const { item } = Route.useLoaderData()
|
|
784
|
+
|
|
785
|
+
// params, search, context는 접근 가능
|
|
786
|
+
const { itemId } = Route.useParams()
|
|
787
|
+
const search = Route.useSearch()
|
|
788
|
+
const context = Route.useRouteContext()
|
|
789
|
+
|
|
790
|
+
return (
|
|
791
|
+
<div>
|
|
792
|
+
<p>Item {itemId} not found</p>
|
|
793
|
+
<p>You searched for: {search}</p>
|
|
794
|
+
</div>
|
|
795
|
+
)
|
|
796
|
+
}
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
| 컴포넌트 | useLoaderData | useParams | useSearch | useRouteContext | data 옵션 |
|
|
800
|
+
|----------|---------------|-----------|-----------|----------------|-----------|
|
|
801
|
+
| errorComponent | 불가 | 가능 | 가능 | 가능 | - |
|
|
802
|
+
| notFoundComponent | 불가 | 가능 | 가능 | 가능 | 가능 |
|
|
803
|
+
| pendingComponent | 불가 | 가능 | 가능 | 가능 | - |
|
|
804
|
+
| component | 가능 | 가능 | 가능 | 가능 | - |
|
|
805
|
+
|
|
806
|
+
</data_access_in_error>
|
|
807
|
+
|
|
808
|
+
---
|
|
809
|
+
|
|
810
|
+
<dos_donts>
|
|
811
|
+
|
|
812
|
+
## Do's & Don'ts
|
|
813
|
+
|
|
814
|
+
| Do | Don't |
|
|
815
|
+
|-------|---------|
|
|
816
|
+
| `throw new Error()` for server errors | Error throw 하지 말고 데이터 반환 |
|
|
817
|
+
| `throw notFound()` for missing resources | 404를 Error로 throw |
|
|
818
|
+
| `errorComponent`로 에러 처리 | try-catch in component |
|
|
819
|
+
| `pendingComponent`로 로딩 표시 | useQuery isLoading |
|
|
820
|
+
| `router.invalidate()`로 loader 에러 복구 | `reset()`만 호출 (loader 재실행 안 됨) |
|
|
821
|
+
| `ErrorComponent`로 폴백 처리 | 모든 에러를 커스텀 처리 |
|
|
822
|
+
| Catch-all `$` 라우트로 404 | 모든 라우트에 notFoundComponent |
|
|
823
|
+
| `notFoundMode: 'fuzzy'`로 레이아웃 보존 | root에서만 404 처리 |
|
|
824
|
+
| Root notFoundComponent 정의 | 기본 not found 그대로 사용 |
|
|
825
|
+
| `onError`로 에러 로깅 | 에러 무시 |
|
|
826
|
+
| `defaultNotFoundComponent` 라우터 설정 | 각 라우트별로 다른 404 UI |
|
|
827
|
+
|
|
828
|
+
</dos_donts>
|