@kood/claude-code 0.2.0 → 0.2.2
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 +62 -18
- package/package.json +1 -1
- package/templates/.claude/agents/code-reviewer.md +31 -0
- package/templates/.claude/agents/debug-detective.md +37 -0
- package/templates/.claude/agents/refactor-advisor.md +44 -0
- package/templates/.claude/agents/test-writer.md +41 -0
- package/templates/.claude/skills/frontend-design/SKILL.md +310 -0
- package/templates/.claude/skills/frontend-design/references/animation-patterns.md +446 -0
- package/templates/.claude/skills/frontend-design/references/colors-2026.md +244 -0
- package/templates/.claude/skills/frontend-design/references/typography-2026.md +302 -0
- package/templates/.claude/skills/gemini-review/SKILL.md +1 -1
- package/templates/hono/docs/library/drizzle/cloudflare-d1.md +247 -0
- package/templates/hono/docs/library/drizzle/config.md +167 -0
- package/templates/hono/docs/library/drizzle/index.md +259 -0
- package/templates/tanstack-start/docs/library/drizzle/cloudflare-d1.md +146 -0
- package/templates/tanstack-start/docs/library/drizzle/config.md +118 -0
- package/templates/tanstack-start/docs/library/drizzle/crud.md +205 -0
- package/templates/tanstack-start/docs/library/drizzle/index.md +79 -0
- package/templates/tanstack-start/docs/library/drizzle/relations.md +202 -0
- package/templates/tanstack-start/docs/library/drizzle/schema.md +154 -0
- package/templates/tanstack-start/docs/library/drizzle/setup.md +95 -0
- package/templates/tanstack-start/docs/library/drizzle/transactions.md +127 -0
- package/templates/tanstack-start/docs/library/tanstack-router/error-handling.md +204 -0
- package/templates/tanstack-start/docs/library/tanstack-router/hooks.md +195 -0
- package/templates/tanstack-start/docs/library/tanstack-router/index.md +150 -0
- package/templates/tanstack-start/docs/library/tanstack-router/navigation.md +150 -0
- package/templates/tanstack-start/docs/library/tanstack-router/route-context.md +203 -0
- package/templates/tanstack-start/docs/library/tanstack-router/search-params.md +213 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# TanStack Router - Error Handling
|
|
2
|
+
|
|
3
|
+
errorComponent, notFoundComponent, 에러 경계.
|
|
4
|
+
|
|
5
|
+
## errorComponent
|
|
6
|
+
|
|
7
|
+
loader/beforeLoad에서 에러 발생 시 표시.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { createFileRoute, ErrorComponent } from '@tanstack/react-router'
|
|
11
|
+
import type { ErrorComponentProps } from '@tanstack/react-router'
|
|
12
|
+
|
|
13
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
14
|
+
loader: async ({ params }) => {
|
|
15
|
+
const post = await getPost(params.postId)
|
|
16
|
+
if (!post) throw new Error('Post not found')
|
|
17
|
+
return { post }
|
|
18
|
+
},
|
|
19
|
+
errorComponent: PostError,
|
|
20
|
+
component: PostPage,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
function PostError({ error, reset }: ErrorComponentProps) {
|
|
24
|
+
return (
|
|
25
|
+
<div>
|
|
26
|
+
<h2>Error loading post</h2>
|
|
27
|
+
<p>{error.message}</p>
|
|
28
|
+
<button onClick={reset}>Retry</button>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## notFoundComponent
|
|
35
|
+
|
|
36
|
+
`notFound()` 호출 시 표시.
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { createFileRoute, notFound } from '@tanstack/react-router'
|
|
40
|
+
|
|
41
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
42
|
+
loader: async ({ params }) => {
|
|
43
|
+
const post = await getPost(params.postId)
|
|
44
|
+
if (!post) {
|
|
45
|
+
throw notFound() // notFoundComponent 렌더링
|
|
46
|
+
}
|
|
47
|
+
return { post }
|
|
48
|
+
},
|
|
49
|
+
notFoundComponent: PostNotFound,
|
|
50
|
+
component: PostPage,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
function PostNotFound() {
|
|
54
|
+
const { postId } = Route.useParams()
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div>
|
|
58
|
+
<h2>Post not found</h2>
|
|
59
|
+
<p>Post with ID "{postId}" does not exist.</p>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### notFound에 데이터 전달
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
69
|
+
loader: async ({ params }) => {
|
|
70
|
+
const post = await getPost(params.postId)
|
|
71
|
+
if (!post) {
|
|
72
|
+
throw notFound({
|
|
73
|
+
data: { searchedId: params.postId },
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
return { post }
|
|
77
|
+
},
|
|
78
|
+
notFoundComponent: ({ data }) => {
|
|
79
|
+
// data.searchedId 접근 가능
|
|
80
|
+
return <p>Post {data?.searchedId} not found</p>
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Root 404 페이지
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
// routes/__root.tsx
|
|
89
|
+
import { createRootRoute, Outlet } from '@tanstack/react-router'
|
|
90
|
+
|
|
91
|
+
export const Route = createRootRoute({
|
|
92
|
+
component: RootLayout,
|
|
93
|
+
notFoundComponent: GlobalNotFound,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
function GlobalNotFound() {
|
|
97
|
+
return (
|
|
98
|
+
<div>
|
|
99
|
+
<h1>404</h1>
|
|
100
|
+
<p>Page not found</p>
|
|
101
|
+
<Link to="/">Go Home</Link>
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## pendingComponent
|
|
108
|
+
|
|
109
|
+
loader 실행 중 표시.
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
export const Route = createFileRoute('/posts')({
|
|
113
|
+
loader: async () => {
|
|
114
|
+
await new Promise(r => setTimeout(r, 1000)) // 느린 로딩
|
|
115
|
+
return { posts: await getPosts() }
|
|
116
|
+
},
|
|
117
|
+
pendingComponent: () => <div>Loading posts...</div>,
|
|
118
|
+
component: PostsPage,
|
|
119
|
+
})
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### pendingMs / pendingMinMs
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
export const Route = createFileRoute('/posts')({
|
|
126
|
+
loader: async () => fetchPosts(),
|
|
127
|
+
pendingComponent: () => <Spinner />,
|
|
128
|
+
pendingMs: 200, // 200ms 후에 pending 표시
|
|
129
|
+
pendingMinMs: 500, // 최소 500ms 동안 pending 유지 (깜빡임 방지)
|
|
130
|
+
component: PostsPage,
|
|
131
|
+
})
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Catch-all Route
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
// routes/$.tsx - 모든 매치되지 않는 경로
|
|
138
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
139
|
+
|
|
140
|
+
export const Route = createFileRoute('/$')({
|
|
141
|
+
component: CatchAllPage,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
function CatchAllPage() {
|
|
145
|
+
const params = Route.useParams()
|
|
146
|
+
// params._splat에 전체 경로
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div>
|
|
150
|
+
<h1>Page Not Found</h1>
|
|
151
|
+
<p>Path: /{params._splat}</p>
|
|
152
|
+
</div>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## 에러 타입 구분
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
function CustomError({ error, reset }: ErrorComponentProps) {
|
|
161
|
+
// 네트워크 에러
|
|
162
|
+
if (error instanceof TypeError && error.message.includes('fetch')) {
|
|
163
|
+
return (
|
|
164
|
+
<div>
|
|
165
|
+
<p>Network error. Check your connection.</p>
|
|
166
|
+
<button onClick={reset}>Retry</button>
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 인증 에러
|
|
172
|
+
if (error.message.includes('unauthorized')) {
|
|
173
|
+
return <Navigate to="/login" />
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 기본 에러
|
|
177
|
+
return (
|
|
178
|
+
<div>
|
|
179
|
+
<p>Something went wrong</p>
|
|
180
|
+
<button onClick={reset}>Retry</button>
|
|
181
|
+
</div>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## 컴포넌트 우선순위
|
|
187
|
+
|
|
188
|
+
| 우선순위 | 컴포넌트 | 조건 |
|
|
189
|
+
|---------|---------|------|
|
|
190
|
+
| 1 | `errorComponent` | loader/beforeLoad에서 Error throw |
|
|
191
|
+
| 2 | `notFoundComponent` | `notFound()` throw |
|
|
192
|
+
| 3 | `pendingComponent` | loader 실행 중 (pendingMs 이후) |
|
|
193
|
+
| 4 | `component` | 정상 렌더링 |
|
|
194
|
+
|
|
195
|
+
## 에러 전파
|
|
196
|
+
|
|
197
|
+
하위 라우트에서 처리 안 된 에러는 상위로 전파.
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
__root.tsx (errorComponent: GlobalError)
|
|
201
|
+
└── posts.tsx (errorComponent: PostsError)
|
|
202
|
+
└── $postId.tsx (errorComponent 없음)
|
|
203
|
+
→ 에러 발생 시 posts.tsx의 PostsError로 전파
|
|
204
|
+
```
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# TanStack Router - Hooks
|
|
2
|
+
|
|
3
|
+
라우터 Hooks 레퍼런스.
|
|
4
|
+
|
|
5
|
+
## Route-Scoped Hooks
|
|
6
|
+
|
|
7
|
+
라우트 컴포넌트 내에서 사용. Type-safe.
|
|
8
|
+
|
|
9
|
+
### Route.useLoaderData()
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
13
|
+
loader: async ({ params }) => {
|
|
14
|
+
const post = await getPost(params.postId)
|
|
15
|
+
return { post }
|
|
16
|
+
},
|
|
17
|
+
component: PostPage,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
function PostPage() {
|
|
21
|
+
const { post } = Route.useLoaderData()
|
|
22
|
+
// ^? { post: Post }
|
|
23
|
+
return <h1>{post.title}</h1>
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Route.useParams()
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
31
|
+
component: PostPage,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
function PostPage() {
|
|
35
|
+
const { postId } = Route.useParams()
|
|
36
|
+
// ^? string
|
|
37
|
+
return <div>Post ID: {postId}</div>
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Route.useSearch()
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
export const Route = createFileRoute('/products')({
|
|
45
|
+
validateSearch: z.object({
|
|
46
|
+
page: z.number().catch(1),
|
|
47
|
+
sort: z.string().catch('newest'),
|
|
48
|
+
}),
|
|
49
|
+
component: ProductsPage,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
function ProductsPage() {
|
|
53
|
+
const { page, sort } = Route.useSearch()
|
|
54
|
+
// ^? number, string
|
|
55
|
+
return <div>Page: {page}, Sort: {sort}</div>
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Route.useRouteContext()
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
// _authed.tsx에서 beforeLoad로 user 전달 가정
|
|
63
|
+
export const Route = createFileRoute('/_authed/dashboard')({
|
|
64
|
+
component: DashboardPage,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
function DashboardPage() {
|
|
68
|
+
const { user } = Route.useRouteContext()
|
|
69
|
+
return <h1>Welcome, {user.name}</h1>
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Global Hooks
|
|
74
|
+
|
|
75
|
+
어디서든 사용 가능. 타입 안전성 직접 지정.
|
|
76
|
+
|
|
77
|
+
### useNavigate()
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { useNavigate } from '@tanstack/react-router'
|
|
81
|
+
|
|
82
|
+
function Component() {
|
|
83
|
+
const navigate = useNavigate()
|
|
84
|
+
|
|
85
|
+
const handleClick = () => {
|
|
86
|
+
navigate({
|
|
87
|
+
to: '/posts/$postId',
|
|
88
|
+
params: { postId: '123' },
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### useMatch()
|
|
95
|
+
|
|
96
|
+
특정 라우트 매치 정보 접근.
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { useMatch } from '@tanstack/react-router'
|
|
100
|
+
|
|
101
|
+
function Breadcrumb() {
|
|
102
|
+
// 특정 라우트가 현재 매치되면 정보 반환
|
|
103
|
+
const postMatch = useMatch({
|
|
104
|
+
from: '/posts/$postId',
|
|
105
|
+
shouldThrow: false, // 매치 안 되면 undefined
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
if (postMatch) {
|
|
109
|
+
return <span>Post: {postMatch.params.postId}</span>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### useParams() (Global)
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
import { useParams } from '@tanstack/react-router'
|
|
120
|
+
|
|
121
|
+
// 타입 지정 필요
|
|
122
|
+
const { postId } = useParams({ from: '/posts/$postId' })
|
|
123
|
+
|
|
124
|
+
// strict: false로 모든 params
|
|
125
|
+
const params = useParams({ strict: false })
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### useSearch() (Global)
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { useSearch } from '@tanstack/react-router'
|
|
132
|
+
|
|
133
|
+
// 타입 지정 필요
|
|
134
|
+
const { page } = useSearch({ from: '/products' })
|
|
135
|
+
|
|
136
|
+
// strict: false로 현재 라우트 search
|
|
137
|
+
const search = useSearch({ strict: false })
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### useRouterState()
|
|
141
|
+
|
|
142
|
+
라우터 전체 상태 접근.
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
import { useRouterState } from '@tanstack/react-router'
|
|
146
|
+
|
|
147
|
+
function CurrentPath() {
|
|
148
|
+
const pathname = useRouterState({
|
|
149
|
+
select: state => state.location.pathname,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
return <span>Current: {pathname}</span>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 전체 location
|
|
156
|
+
const location = useRouterState({
|
|
157
|
+
select: state => state.location,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// pending 상태
|
|
161
|
+
const isLoading = useRouterState({
|
|
162
|
+
select: state => state.isLoading,
|
|
163
|
+
})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### useLocation()
|
|
167
|
+
|
|
168
|
+
현재 location 정보.
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
import { useLocation } from '@tanstack/react-router'
|
|
172
|
+
|
|
173
|
+
function Component() {
|
|
174
|
+
const location = useLocation()
|
|
175
|
+
|
|
176
|
+
console.log(location.pathname) // '/posts/123'
|
|
177
|
+
console.log(location.search) // { page: 1 }
|
|
178
|
+
console.log(location.hash) // '#section'
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Hook 비교
|
|
183
|
+
|
|
184
|
+
| Hook | Scope | Type Safety | 용도 |
|
|
185
|
+
|------|-------|-------------|------|
|
|
186
|
+
| `Route.useLoaderData()` | Route | Auto | Loader 데이터 |
|
|
187
|
+
| `Route.useParams()` | Route | Auto | Path params |
|
|
188
|
+
| `Route.useSearch()` | Route | Auto | Search params |
|
|
189
|
+
| `Route.useRouteContext()` | Route | Auto | Route context |
|
|
190
|
+
| `useParams({ from })` | Global | Manual | 다른 라우트 params |
|
|
191
|
+
| `useSearch({ from })` | Global | Manual | 다른 라우트 search |
|
|
192
|
+
| `useMatch({ from })` | Global | Manual | 라우트 매치 정보 |
|
|
193
|
+
| `useNavigate()` | Global | Auto | 네비게이션 |
|
|
194
|
+
| `useRouterState()` | Global | Manual | 라우터 상태 |
|
|
195
|
+
| `useLocation()` | Global | Auto | 현재 location |
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# TanStack Router
|
|
2
|
+
|
|
3
|
+
> **Version**: 1.x | Type-safe React Router
|
|
4
|
+
|
|
5
|
+
@navigation.md
|
|
6
|
+
@search-params.md
|
|
7
|
+
@route-context.md
|
|
8
|
+
@hooks.md
|
|
9
|
+
@error-handling.md
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Quick Reference
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { createFileRoute, Link, Outlet } from '@tanstack/react-router'
|
|
17
|
+
|
|
18
|
+
// 기본 라우트
|
|
19
|
+
export const Route = createFileRoute('/about')({
|
|
20
|
+
component: AboutPage,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// Loader + 동적 파라미터
|
|
24
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
25
|
+
loader: async ({ params }) => {
|
|
26
|
+
const post = await getPost(params.postId)
|
|
27
|
+
return { post }
|
|
28
|
+
},
|
|
29
|
+
component: PostPage,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
function PostPage() {
|
|
33
|
+
const { post } = Route.useLoaderData()
|
|
34
|
+
return <h1>{post.title}</h1>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Search Params (Zod)
|
|
38
|
+
export const Route = createFileRoute('/products')({
|
|
39
|
+
validateSearch: z.object({
|
|
40
|
+
page: z.number().catch(1),
|
|
41
|
+
sort: z.enum(['newest', 'price']).catch('newest'),
|
|
42
|
+
}),
|
|
43
|
+
component: ProductsPage,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function ProductsPage() {
|
|
47
|
+
const { page, sort } = Route.useSearch()
|
|
48
|
+
return <div>Page {page}, Sort: {sort}</div>
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 파일 구조
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
routes/
|
|
58
|
+
├── __root.tsx # Root layout
|
|
59
|
+
├── index.tsx # /
|
|
60
|
+
├── about.tsx # /about
|
|
61
|
+
├── posts/
|
|
62
|
+
│ ├── index.tsx # /posts
|
|
63
|
+
│ └── $postId.tsx # /posts/:postId
|
|
64
|
+
├── _authed/ # Protected routes (pathless)
|
|
65
|
+
│ ├── dashboard.tsx # /dashboard
|
|
66
|
+
│ └── settings.tsx # /settings
|
|
67
|
+
└── $.tsx # Catch-all (404)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
| 파일명 패턴 | 설명 |
|
|
71
|
+
|------------|------|
|
|
72
|
+
| `index.tsx` | 디렉토리 루트 |
|
|
73
|
+
| `$param.tsx` | 동적 세그먼트 |
|
|
74
|
+
| `_layout/` | Pathless layout |
|
|
75
|
+
| `$.tsx` | Catch-all |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Root Route
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
// routes/__root.tsx
|
|
83
|
+
import { createRootRoute, Outlet } from '@tanstack/react-router'
|
|
84
|
+
|
|
85
|
+
export const Route = createRootRoute({
|
|
86
|
+
component: RootLayout,
|
|
87
|
+
notFoundComponent: () => <div>404 Not Found</div>,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
function RootLayout() {
|
|
91
|
+
return (
|
|
92
|
+
<div>
|
|
93
|
+
<nav>{/* navigation */}</nav>
|
|
94
|
+
<main>
|
|
95
|
+
<Outlet />
|
|
96
|
+
</main>
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Route Options
|
|
105
|
+
|
|
106
|
+
| 옵션 | 설명 |
|
|
107
|
+
|------|------|
|
|
108
|
+
| `component` | 렌더링할 컴포넌트 |
|
|
109
|
+
| `loader` | 데이터 로드 함수 |
|
|
110
|
+
| `beforeLoad` | 로드 전 실행 (인증 체크 등) |
|
|
111
|
+
| `validateSearch` | Search params 스키마 |
|
|
112
|
+
| `pendingComponent` | 로딩 중 표시 |
|
|
113
|
+
| `errorComponent` | 에러 발생 시 표시 |
|
|
114
|
+
| `notFoundComponent` | Not found 시 표시 |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Navigation
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import { Link, useNavigate } from '@tanstack/react-router'
|
|
122
|
+
|
|
123
|
+
// Link
|
|
124
|
+
<Link to="/posts/$postId" params={{ postId: '123' }}>
|
|
125
|
+
Post
|
|
126
|
+
</Link>
|
|
127
|
+
|
|
128
|
+
// Search params
|
|
129
|
+
<Link to="/products" search={{ page: 1, sort: 'newest' }}>
|
|
130
|
+
Products
|
|
131
|
+
</Link>
|
|
132
|
+
|
|
133
|
+
// Programmatic
|
|
134
|
+
const navigate = useNavigate()
|
|
135
|
+
navigate({ to: '/posts/$postId', params: { postId: '123' } })
|
|
136
|
+
navigate({ to: '/products', search: prev => ({ ...prev, page: 2 }) })
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Hooks
|
|
142
|
+
|
|
143
|
+
| Hook | 용도 |
|
|
144
|
+
|------|------|
|
|
145
|
+
| `Route.useLoaderData()` | Loader 반환값 |
|
|
146
|
+
| `Route.useParams()` | Path 파라미터 |
|
|
147
|
+
| `Route.useSearch()` | Search params |
|
|
148
|
+
| `Route.useRouteContext()` | Route context |
|
|
149
|
+
| `useNavigate()` | 프로그래밍 네비게이션 |
|
|
150
|
+
| `useMatch({ from })` | 특정 라우트 매치 정보 |
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# TanStack Router - Navigation
|
|
2
|
+
|
|
3
|
+
Link 컴포넌트와 프로그래밍 네비게이션.
|
|
4
|
+
|
|
5
|
+
## Link 컴포넌트
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Link } from '@tanstack/react-router'
|
|
9
|
+
|
|
10
|
+
// 기본
|
|
11
|
+
<Link to="/about">About</Link>
|
|
12
|
+
|
|
13
|
+
// 동적 파라미터
|
|
14
|
+
<Link to="/posts/$postId" params={{ postId: '123' }}>
|
|
15
|
+
Post 123
|
|
16
|
+
</Link>
|
|
17
|
+
|
|
18
|
+
// Search params
|
|
19
|
+
<Link to="/products" search={{ page: 1, sort: 'newest' }}>
|
|
20
|
+
Products
|
|
21
|
+
</Link>
|
|
22
|
+
|
|
23
|
+
// Search params 병합
|
|
24
|
+
<Link to="/products" search={prev => ({ ...prev, page: 2 })}>
|
|
25
|
+
Next Page
|
|
26
|
+
</Link>
|
|
27
|
+
|
|
28
|
+
// Active 스타일
|
|
29
|
+
<Link
|
|
30
|
+
to="/about"
|
|
31
|
+
activeProps={{ className: 'text-blue-500 font-bold' }}
|
|
32
|
+
inactiveProps={{ className: 'text-gray-500' }}
|
|
33
|
+
>
|
|
34
|
+
About
|
|
35
|
+
</Link>
|
|
36
|
+
|
|
37
|
+
// 정확히 일치할 때만 active
|
|
38
|
+
<Link to="/" activeOptions={{ exact: true }}>
|
|
39
|
+
Home
|
|
40
|
+
</Link>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Link Props
|
|
44
|
+
|
|
45
|
+
| Prop | 타입 | 설명 |
|
|
46
|
+
|------|------|------|
|
|
47
|
+
| `to` | string | 목적지 경로 |
|
|
48
|
+
| `params` | object | Path 파라미터 |
|
|
49
|
+
| `search` | object \| function | Search params |
|
|
50
|
+
| `hash` | string | Hash |
|
|
51
|
+
| `replace` | boolean | history replace |
|
|
52
|
+
| `preload` | 'intent' \| 'render' \| 'viewport' | Preload 전략 |
|
|
53
|
+
| `activeProps` | object | Active 시 props |
|
|
54
|
+
| `inactiveProps` | object | Inactive 시 props |
|
|
55
|
+
|
|
56
|
+
## useNavigate
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { useNavigate } from '@tanstack/react-router'
|
|
60
|
+
|
|
61
|
+
function Component() {
|
|
62
|
+
const navigate = useNavigate()
|
|
63
|
+
|
|
64
|
+
// 기본
|
|
65
|
+
const goToAbout = () => {
|
|
66
|
+
navigate({ to: '/about' })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 동적 파라미터
|
|
70
|
+
const goToPost = (postId: string) => {
|
|
71
|
+
navigate({ to: '/posts/$postId', params: { postId } })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Search params
|
|
75
|
+
const updateSearch = () => {
|
|
76
|
+
navigate({
|
|
77
|
+
to: '/products',
|
|
78
|
+
search: prev => ({ ...prev, page: 2 }),
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Replace (뒤로가기 안 됨)
|
|
83
|
+
const replaceRoute = () => {
|
|
84
|
+
navigate({ to: '/login', replace: true })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 상대 경로
|
|
88
|
+
const goUp = () => {
|
|
89
|
+
navigate({ to: '..' })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<button onClick={goToAbout}>Go to About</button>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## navigate 옵션
|
|
99
|
+
|
|
100
|
+
| 옵션 | 타입 | 설명 |
|
|
101
|
+
|------|------|------|
|
|
102
|
+
| `to` | string | 목적지 경로 |
|
|
103
|
+
| `params` | object | Path 파라미터 |
|
|
104
|
+
| `search` | object \| function | Search params |
|
|
105
|
+
| `hash` | string | Hash |
|
|
106
|
+
| `replace` | boolean | history.replace 사용 |
|
|
107
|
+
| `resetScroll` | boolean | 스크롤 리셋 |
|
|
108
|
+
|
|
109
|
+
## Preloading
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
// hover 시 preload
|
|
113
|
+
<Link to="/posts" preload="intent">
|
|
114
|
+
Posts
|
|
115
|
+
</Link>
|
|
116
|
+
|
|
117
|
+
// 렌더링 시 preload
|
|
118
|
+
<Link to="/dashboard" preload="render">
|
|
119
|
+
Dashboard
|
|
120
|
+
</Link>
|
|
121
|
+
|
|
122
|
+
// viewport 진입 시 preload
|
|
123
|
+
<Link to="/products" preload="viewport">
|
|
124
|
+
Products
|
|
125
|
+
</Link>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## 조건부 네비게이션
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
function SubmitButton() {
|
|
132
|
+
const navigate = useNavigate()
|
|
133
|
+
const [isPending, startTransition] = useTransition()
|
|
134
|
+
|
|
135
|
+
const handleSubmit = async () => {
|
|
136
|
+
const result = await submitForm()
|
|
137
|
+
if (result.success) {
|
|
138
|
+
startTransition(() => {
|
|
139
|
+
navigate({ to: '/success' })
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<button onClick={handleSubmit} disabled={isPending}>
|
|
146
|
+
Submit
|
|
147
|
+
</button>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
```
|