@kood/claude-code 0.6.5 → 0.6.7
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 +255 -149
- package/package.json +1 -1
- package/templates/.claude/agents/researcher.md +8 -1
- package/templates/.claude/instructions/sourcing/reliable-search.md +49 -2
- package/templates/.claude/scripts/deploy/build-run.sh +36 -0
- package/templates/.claude/scripts/deploy/deploy-check.sh +38 -0
- package/templates/.claude/scripts/git/git-all.sh +57 -0
- package/templates/.claude/scripts/git/git-clean-check.sh +31 -0
- package/templates/.claude/scripts/git/git-commit.sh +51 -0
- package/templates/.claude/scripts/git/git-info.sh +34 -0
- package/templates/.claude/scripts/git/git-push.sh +50 -0
- package/templates/.claude/scripts/lint/lint-check.sh +56 -0
- package/templates/.claude/scripts/lint/lint-file.sh +41 -0
- package/templates/.claude/scripts/pm/pm-detect.sh +25 -0
- package/templates/.claude/scripts/pm/pm-run.sh +41 -0
- package/templates/.claude/scripts/version/version-bump.sh +54 -0
- package/templates/.claude/scripts/version/version-find.sh +49 -0
- package/templates/.claude/skills/docs-fetch/SKILL.md +5 -4
- package/templates/.claude/skills/project-optimizer/AGENTS.md +275 -0
- package/templates/.claude/skills/project-optimizer/SKILL.md +374 -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/sql-optimizer/SKILL.md +437 -0
- package/templates/.claude/skills/sql-optimizer/orm-patterns.md +218 -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 +93 -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 +570 -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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use useOptimistic for Instant UI Feedback
|
|
3
|
+
impact: MEDIUM-HIGH
|
|
4
|
+
impactDescription: eliminates perceived latency on mutations
|
|
5
|
+
tags: client, react-19, useOptimistic, mutations, optimistic-ui
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## useOptimistic으로 즉각적인 UI 피드백
|
|
9
|
+
|
|
10
|
+
React 19의 `useOptimistic`으로 서버 응답 대기 없이 UI를 즉시 업데이트하고, 실패 시 자동 롤백합니다.
|
|
11
|
+
|
|
12
|
+
**❌ 잘못된 예시 (서버 응답까지 대기):**
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
function TodoList({ todos }: { todos: Todo[] }) {
|
|
16
|
+
const mutation = useMutation({
|
|
17
|
+
mutationFn: createTodo,
|
|
18
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<form onSubmit={(e) => {
|
|
23
|
+
e.preventDefault()
|
|
24
|
+
mutation.mutate({ title: inputValue })
|
|
25
|
+
// 사용자는 서버 응답까지 기다려야 아이템을 볼 수 있음
|
|
26
|
+
}}>
|
|
27
|
+
{mutation.isPending && <Spinner />}
|
|
28
|
+
{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
|
|
29
|
+
</form>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**✅ 올바른 예시 (useOptimistic으로 즉시 반영):**
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { useOptimistic, startTransition } from 'react'
|
|
38
|
+
|
|
39
|
+
function TodoList({ todos }: { todos: Todo[] }) {
|
|
40
|
+
const [optimisticTodos, addOptimistic] = useOptimistic(
|
|
41
|
+
todos,
|
|
42
|
+
(current, newTodo: Todo) => [...current, { ...newTodo, pending: true }]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const handleAdd = async (formData: FormData) => {
|
|
46
|
+
const newTodo = {
|
|
47
|
+
id: crypto.randomUUID(),
|
|
48
|
+
title: formData.get('title') as string,
|
|
49
|
+
completed: false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
startTransition(async () => {
|
|
53
|
+
addOptimistic(newTodo) // 즉시 UI에 반영
|
|
54
|
+
await createTodo({ data: newTodo }) // 서버에 저장
|
|
55
|
+
// 실패 시 자동으로 이전 상태로 롤백
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<form action={handleAdd}>
|
|
61
|
+
<input name="title" />
|
|
62
|
+
<button type="submit">추가</button>
|
|
63
|
+
{optimisticTodos.map(todo => (
|
|
64
|
+
<TodoItem
|
|
65
|
+
key={todo.id}
|
|
66
|
+
todo={todo}
|
|
67
|
+
style={{ opacity: todo.pending ? 0.5 : 1 }}
|
|
68
|
+
/>
|
|
69
|
+
))}
|
|
70
|
+
</form>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**TanStack Query와 조합:**
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
function LikeButton({ postId, liked, count }: Props) {
|
|
79
|
+
const [optimistic, setOptimistic] = useOptimistic(
|
|
80
|
+
{ liked, count },
|
|
81
|
+
(curr) => ({
|
|
82
|
+
liked: !curr.liked,
|
|
83
|
+
count: curr.liked ? curr.count - 1 : curr.count + 1
|
|
84
|
+
})
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const mutation = useMutation({
|
|
88
|
+
mutationFn: () => toggleLike({ data: { postId } }),
|
|
89
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['post', postId] })
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<button onClick={() => {
|
|
94
|
+
startTransition(async () => {
|
|
95
|
+
setOptimistic(null)
|
|
96
|
+
await mutation.mutateAsync()
|
|
97
|
+
})
|
|
98
|
+
}}>
|
|
99
|
+
{optimistic.liked ? '❤️' : '🤍'} {optimistic.count}
|
|
100
|
+
</button>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**사용 시점:** 좋아요/투표, 댓글 추가, 장바구니 아이템 추가/삭제, 토글 스위치 등 사용자가 즉각적인 피드백을 기대하는 모든 액션.
|
|
106
|
+
|
|
107
|
+
**주의:** `startTransition` 내에서 사용해야 자동 롤백이 작동합니다.
|
|
108
|
+
|
|
109
|
+
참고: [React 19 useOptimistic](https://react.dev/reference/react/useOptimistic)
|
package/templates/.claude/skills/tanstack-start-react-best-practices/rules/client-suspense-query.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use useSuspenseQuery for Declarative Data Loading
|
|
3
|
+
impact: MEDIUM-HIGH
|
|
4
|
+
impactDescription: eliminates loading state boilerplate
|
|
5
|
+
tags: client, tanstack-query, suspense, data-fetching
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## useSuspenseQuery로 선언적 데이터 로딩
|
|
9
|
+
|
|
10
|
+
TanStack Query v5의 `useSuspenseQuery`는 Suspense를 기본 지원하여 로딩/에러 상태 보일러플레이트를 제거합니다.
|
|
11
|
+
|
|
12
|
+
**❌ 잘못된 예시 (수동 로딩/에러 처리):**
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { useQuery } from '@tanstack/react-query'
|
|
16
|
+
|
|
17
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
18
|
+
const { data: user, isLoading, error } = useQuery({
|
|
19
|
+
queryKey: ['user', userId],
|
|
20
|
+
queryFn: () => getUser({ data: { id: userId } })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
if (isLoading) return <UserSkeleton />
|
|
24
|
+
if (error) return <ErrorMessage error={error} />
|
|
25
|
+
if (!user) return null
|
|
26
|
+
|
|
27
|
+
return <div>{user.name}</div>
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**✅ 올바른 예시 (Suspense + ErrorBoundary):**
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { useSuspenseQuery } from '@tanstack/react-query'
|
|
35
|
+
import { Suspense } from 'react'
|
|
36
|
+
import { ErrorBoundary } from 'react-error-boundary'
|
|
37
|
+
|
|
38
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
39
|
+
// data는 항상 존재 (undefined 아님), 타입 안전
|
|
40
|
+
const { data: user } = useSuspenseQuery({
|
|
41
|
+
queryKey: ['user', userId],
|
|
42
|
+
queryFn: () => getUser({ data: { id: userId } })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return <div>{user.name}</div>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 부모에서 Suspense + ErrorBoundary로 감싸기
|
|
49
|
+
function UserPage({ userId }: { userId: string }) {
|
|
50
|
+
return (
|
|
51
|
+
<ErrorBoundary fallback={<ErrorMessage />}>
|
|
52
|
+
<Suspense fallback={<UserSkeleton />}>
|
|
53
|
+
<UserProfile userId={userId} />
|
|
54
|
+
</Suspense>
|
|
55
|
+
</ErrorBoundary>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**장점:**
|
|
61
|
+
- `data`가 `undefined`가 아닌 확정 타입 (타입 가드 불필요)
|
|
62
|
+
- 로딩/에러 처리가 컴포넌트 트리 상위로 위임 (관심사 분리)
|
|
63
|
+
- 여러 쿼리의 로딩 상태를 하나의 Suspense로 통합 가능
|
|
64
|
+
|
|
65
|
+
**TanStack Query v5 주요 변경 (v4 대비):**
|
|
66
|
+
|
|
67
|
+
| v4 | v5 |
|
|
68
|
+
|----|-----|
|
|
69
|
+
| `cacheTime` | `gcTime` |
|
|
70
|
+
| `keepPreviousData` | `placeholderData` |
|
|
71
|
+
| `isLoading` | `isPending` |
|
|
72
|
+
| `useSuspenseQuery` (실험적) | `useSuspenseQuery` (안정적) |
|
|
73
|
+
|
|
74
|
+
참고: [TanStack Query](https://tanstack.com/query/latest/docs/framework/react/overview)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use React 19 use() Hook for Promise Handling
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: replaces useEffect fetch patterns
|
|
5
|
+
tags: client, react-19, use-hook, promises, suspense
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## React 19 use() Hook으로 Promise 처리
|
|
9
|
+
|
|
10
|
+
React 19의 `use()` 훅은 렌더링 중 Promise를 읽어 Suspense와 자동 통합됩니다. `useEffect` + `useState` 조합을 대체합니다.
|
|
11
|
+
|
|
12
|
+
**❌ 잘못된 예시 (useEffect + useState 패턴):**
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
function Comments({ postId }: { postId: string }) {
|
|
16
|
+
const [comments, setComments] = useState<Comment[] | null>(null)
|
|
17
|
+
const [loading, setLoading] = useState(true)
|
|
18
|
+
const [error, setError] = useState<Error | null>(null)
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
setLoading(true)
|
|
22
|
+
getComments({ data: { postId } })
|
|
23
|
+
.then(setComments)
|
|
24
|
+
.catch(setError)
|
|
25
|
+
.finally(() => setLoading(false))
|
|
26
|
+
}, [postId])
|
|
27
|
+
|
|
28
|
+
if (loading) return <CommentsSkeleton />
|
|
29
|
+
if (error) return <ErrorMessage error={error} />
|
|
30
|
+
return <CommentList comments={comments!} />
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**✅ 올바른 예시 (use() + Suspense):**
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { use, Suspense } from 'react'
|
|
38
|
+
import { ErrorBoundary } from 'react-error-boundary'
|
|
39
|
+
|
|
40
|
+
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
|
|
41
|
+
const comments = use(commentsPromise)
|
|
42
|
+
return <CommentList comments={comments} />
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 부모에서 Promise 전달
|
|
46
|
+
function PostPage() {
|
|
47
|
+
const { post, deferredComments } = Route.useLoaderData()
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div>
|
|
51
|
+
<PostContent post={post} />
|
|
52
|
+
<ErrorBoundary fallback={<ErrorMessage />}>
|
|
53
|
+
<Suspense fallback={<CommentsSkeleton />}>
|
|
54
|
+
<Comments commentsPromise={deferredComments} />
|
|
55
|
+
</Suspense>
|
|
56
|
+
</ErrorBoundary>
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**use()로 Context 읽기:**
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
// 조건부 Context 읽기 (useContext와 달리 조건부 호출 가능)
|
|
66
|
+
function ThemeButton({ override }: { override?: Theme }) {
|
|
67
|
+
if (override) return <Button theme={override} />
|
|
68
|
+
|
|
69
|
+
const theme = use(ThemeContext)
|
|
70
|
+
return <Button theme={theme} />
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**use() 사용 규칙:**
|
|
75
|
+
- ✅ 조건문/루프 내에서 호출 가능 (일반 Hook과 다름)
|
|
76
|
+
- ❌ 컴포넌트 내에서 `new Promise()` 생성 후 전달 금지 (매 렌더링마다 재생성)
|
|
77
|
+
- ✅ loader, server function, 부모 컴포넌트에서 생성된 Promise 전달
|
|
78
|
+
|
|
79
|
+
**TanStack Start와 조합:** loader에서 deferred Promise를 반환하고, 컴포넌트에서 `use()` 또는 `<Await>`로 처리합니다. 두 패턴 모두 유효하며, `<Await>`가 TanStack Router의 공식 패턴입니다.
|
|
80
|
+
|
|
81
|
+
참고: [React use() API](https://react.dev/reference/react/use)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Leverage React Compiler for Automatic Memoization
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: eliminates manual memo/useMemo/useCallback
|
|
5
|
+
tags: rerender, react-compiler, memoization, react-19
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## React Compiler로 자동 메모이제이션
|
|
9
|
+
|
|
10
|
+
React Compiler가 활성화되어 있다면 `memo()`, `useMemo()`, `useCallback()`을 수동으로 작성할 필요가 없습니다. 컴파일러가 빌드 타임에 자동으로 최적화합니다.
|
|
11
|
+
|
|
12
|
+
**React Compiler 없을 때 (수동 메모이제이션):**
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { memo, useMemo, useCallback } from 'react'
|
|
16
|
+
|
|
17
|
+
const UserList = memo(function UserList({ users, onSelect }: Props) {
|
|
18
|
+
const sorted = useMemo(
|
|
19
|
+
() => users.toSorted((a, b) => a.name.localeCompare(b.name)),
|
|
20
|
+
[users]
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const handleSelect = useCallback((id: string) => {
|
|
24
|
+
onSelect(id)
|
|
25
|
+
}, [onSelect])
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<ul>
|
|
29
|
+
{sorted.map(user => (
|
|
30
|
+
<UserItem key={user.id} user={user} onSelect={handleSelect} />
|
|
31
|
+
))}
|
|
32
|
+
</ul>
|
|
33
|
+
)
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**React Compiler 있을 때 (자동 최적화):**
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
function UserList({ users, onSelect }: Props) {
|
|
41
|
+
const sorted = users.toSorted((a, b) => a.name.localeCompare(b.name))
|
|
42
|
+
|
|
43
|
+
const handleSelect = (id: string) => {
|
|
44
|
+
onSelect(id)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<ul>
|
|
49
|
+
{sorted.map(user => (
|
|
50
|
+
<UserItem key={user.id} user={user} onSelect={handleSelect} />
|
|
51
|
+
))}
|
|
52
|
+
</ul>
|
|
53
|
+
)
|
|
54
|
+
// Compiler가 자동으로 memo, useMemo, useCallback 삽입
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Compiler 활성화 확인:**
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// vite.config.ts (TanStack Start)
|
|
62
|
+
import { defineConfig } from '@tanstack/react-start/config'
|
|
63
|
+
|
|
64
|
+
export default defineConfig({
|
|
65
|
+
react: {
|
|
66
|
+
babel: {
|
|
67
|
+
plugins: [['babel-plugin-react-compiler', {}]]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**수동 메모이제이션이 여전히 필요한 경우:**
|
|
74
|
+
- React Compiler가 비활성화된 프로젝트
|
|
75
|
+
- 써드파티 라이브러리가 `===` 참조 비교를 요구하는 경우
|
|
76
|
+
- Effect 의존성에 대한 정확한 제어가 필요한 경우
|
|
77
|
+
- 함수형 setState는 정확성(stale closure 방지)을 위해 Compiler와 무관하게 권장
|
|
78
|
+
|
|
79
|
+
**성능:** Meta 프로덕션에서 평균 12% 더 빠른 로드, 2.5배 빠른 상호작용 측정.
|
|
80
|
+
|
|
81
|
+
참고: [React Compiler](https://react.dev/learn/react-compiler)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use beforeLoad for Authentication Guards
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: prevents unauthorized access before data loading
|
|
5
|
+
tags: routing, beforeLoad, authentication, redirect, guards
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## beforeLoad로 인증 가드 구현
|
|
9
|
+
|
|
10
|
+
`beforeLoad`는 loader보다 먼저 순차 실행되며, 인증 체크/리다이렉트에 최적입니다. Pathless layout(`_prefix`)과 조합하면 하위 라우트 전체를 보호합니다.
|
|
11
|
+
|
|
12
|
+
**❌ 잘못된 예시 (loader에서 인증 체크):**
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
export const Route = createFileRoute('/dashboard')({
|
|
16
|
+
loader: async () => {
|
|
17
|
+
const user = await getSession()
|
|
18
|
+
if (!user) throw redirect({ to: '/login' }) // 데이터 로딩 후 체크 (늦음)
|
|
19
|
+
const data = await fetchDashboard(user.id)
|
|
20
|
+
return { data }
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**✅ 올바른 예시 (beforeLoad + pathless layout):**
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
// src/routes/_authenticated.tsx - 인증 가드 레이아웃
|
|
29
|
+
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'
|
|
30
|
+
|
|
31
|
+
export const Route = createFileRoute('/_authenticated')({
|
|
32
|
+
beforeLoad: async ({ location }) => {
|
|
33
|
+
const user = await verifySession()
|
|
34
|
+
|
|
35
|
+
if (!user) {
|
|
36
|
+
throw redirect({
|
|
37
|
+
to: '/login',
|
|
38
|
+
search: { redirect: location.href }, // 원래 위치 보존
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { user } // context로 하위 라우트에 전달
|
|
43
|
+
},
|
|
44
|
+
component: () => <Outlet />,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// src/routes/_authenticated/dashboard.tsx - 보호된 라우트
|
|
48
|
+
export const Route = createFileRoute('/_authenticated/dashboard')({
|
|
49
|
+
loader: async ({ context }) => {
|
|
50
|
+
// context.user는 beforeLoad에서 타입 안전하게 전달됨
|
|
51
|
+
return { stats: await fetchDashboard(context.user.id) }
|
|
52
|
+
},
|
|
53
|
+
component: Dashboard,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
function Dashboard() {
|
|
57
|
+
const { stats } = Route.useLoaderData()
|
|
58
|
+
return <DashboardView stats={stats} />
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**실행 순서:**
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
1. Root.beforeLoad() ← 순차 실행
|
|
66
|
+
2. _authenticated.beforeLoad() ← 인증 실패 시 여기서 redirect
|
|
67
|
+
3. Dashboard.beforeLoad() ← 인증 통과 후 실행
|
|
68
|
+
4. Root.loader() ← 모든 loader 병렬 실행
|
|
69
|
+
5. _authenticated.loader()
|
|
70
|
+
6. Dashboard.loader()
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**beforeLoad vs loader:**
|
|
74
|
+
|
|
75
|
+
| 항목 | beforeLoad | loader |
|
|
76
|
+
|------|-----------|--------|
|
|
77
|
+
| 실행 | 순차 (부모→자식) | 병렬 (모든 라우트 동시) |
|
|
78
|
+
| 시점 | loader 전 | beforeLoad 후 |
|
|
79
|
+
| 용도 | 인증, 리다이렉트, context 설정 | 데이터 페칭 |
|
|
80
|
+
| 반환값 | context 객체 (하위 라우트 접근 가능) | 라우트 데이터 |
|
|
81
|
+
| 성능 | 가볍게 유지 (모든 네비게이션에서 실행) | 무거운 작업 가능 |
|
|
82
|
+
|
|
83
|
+
**선택적 인증 (인증 없어도 페이지 렌더링):**
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
export const Route = createFileRoute('/profile')({
|
|
87
|
+
beforeLoad: async () => {
|
|
88
|
+
try {
|
|
89
|
+
const user = await verifySession()
|
|
90
|
+
return { user }
|
|
91
|
+
} catch {
|
|
92
|
+
return { user: null } // 리다이렉트 없이 null 반환
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
component: Profile,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
function Profile() {
|
|
99
|
+
const { user } = Route.useRouteContext()
|
|
100
|
+
return user ? <UserProfile user={user} /> : <LoginPrompt />
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**redirect에서 isRedirect 사용:**
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { redirect, isRedirect } from '@tanstack/react-router'
|
|
108
|
+
|
|
109
|
+
beforeLoad: async ({ location }) => {
|
|
110
|
+
try {
|
|
111
|
+
const user = await verifySession()
|
|
112
|
+
if (!user) throw redirect({ to: '/login' })
|
|
113
|
+
return { user }
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (isRedirect(error)) throw error // redirect는 그대로 전파
|
|
116
|
+
throw redirect({ to: '/login' }) // 다른 에러도 로그인으로
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
참고: [Authenticated Routes](https://tanstack.com/router/latest/docs/framework/react/guide/authenticated-routes)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Follow File-Based Routing Conventions
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: ensures correct route generation and code splitting
|
|
5
|
+
tags: routing, file-conventions, file-based-routing, code-splitting
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 파일 기반 라우팅 컨벤션
|
|
9
|
+
|
|
10
|
+
TanStack Router의 파일 이름 컨벤션을 따르면 라우트 트리가 자동 생성됩니다.
|
|
11
|
+
|
|
12
|
+
**파일 이름 규칙:**
|
|
13
|
+
|
|
14
|
+
| 패턴 | 파일명 | URL | 설명 |
|
|
15
|
+
|------|--------|-----|------|
|
|
16
|
+
| **Root** | `__root.tsx` | - | 앱 최상위 레이아웃 (필수) |
|
|
17
|
+
| **Index** | `index.tsx` | `/` | 디렉토리 인덱스 |
|
|
18
|
+
| **Static** | `about.tsx` | `/about` | 정적 경로 |
|
|
19
|
+
| **Dynamic** | `$postId.tsx` | `/posts/:postId` | 동적 파라미터 |
|
|
20
|
+
| **Pathless Layout** | `_layout.tsx` | - | URL 없는 레이아웃 래퍼 |
|
|
21
|
+
| **Route Group** | `(group)/` | - | 파일 정리용 (URL 영향 없음) |
|
|
22
|
+
| **Excluded** | `-helpers.tsx` | - | 라우트 생성 제외 |
|
|
23
|
+
| **Lazy** | `route.lazy.tsx` | - | 코드 스플릿 컴포넌트 |
|
|
24
|
+
|
|
25
|
+
**디렉토리 구조 예시:**
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
src/routes/
|
|
29
|
+
├── __root.tsx # 루트 레이아웃 (필수)
|
|
30
|
+
├── index.tsx # / (홈)
|
|
31
|
+
├── about.tsx # /about
|
|
32
|
+
├── posts.tsx # /posts (레이아웃)
|
|
33
|
+
├── posts/
|
|
34
|
+
│ ├── index.tsx # /posts (목록)
|
|
35
|
+
│ └── $postId.tsx # /posts/:postId
|
|
36
|
+
├── _authenticated.tsx # pathless 인증 가드
|
|
37
|
+
├── _authenticated/
|
|
38
|
+
│ ├── dashboard.tsx # /dashboard
|
|
39
|
+
│ └── settings.tsx # /settings
|
|
40
|
+
├── (marketing)/ # route group (정리용)
|
|
41
|
+
│ ├── pricing.tsx # /pricing
|
|
42
|
+
│ └── features.tsx # /features
|
|
43
|
+
└── -components/ # 제외 (라우트 아님)
|
|
44
|
+
└── Sidebar.tsx
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**코드 스플리팅 (lazy route):**
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// src/routes/posts.$postId.tsx - 크리티컬 설정만
|
|
51
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
52
|
+
|
|
53
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
54
|
+
loader: async ({ params }) => {
|
|
55
|
+
return { post: await fetchPost(params.postId) }
|
|
56
|
+
},
|
|
57
|
+
validateSearch: zodValidator(searchSchema),
|
|
58
|
+
beforeLoad: async ({ context }) => { /* auth */ },
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// src/routes/posts.$postId.lazy.tsx - 컴포넌트만 (코드 스플릿)
|
|
62
|
+
import { createLazyFileRoute } from '@tanstack/react-router'
|
|
63
|
+
|
|
64
|
+
export const Route = createLazyFileRoute('/posts/$postId')({
|
|
65
|
+
component: PostDetail,
|
|
66
|
+
pendingComponent: PostSkeleton,
|
|
67
|
+
errorComponent: PostError,
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Flat vs Directory 스타일:**
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
# Flat (점 구분자)
|
|
75
|
+
posts.tsx → /posts
|
|
76
|
+
posts.index.tsx → /posts (index)
|
|
77
|
+
posts.$postId.tsx → /posts/:postId
|
|
78
|
+
|
|
79
|
+
# Directory (폴더)
|
|
80
|
+
posts/
|
|
81
|
+
route.tsx → /posts
|
|
82
|
+
index.tsx → /posts (index)
|
|
83
|
+
$postId.tsx → /posts/:postId
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**tsr.config.json 설정:**
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"routesDirectory": "./src/routes",
|
|
91
|
+
"generatedRouteTree": "./src/routeTree.gen.ts",
|
|
92
|
+
"routeFileIgnorePrefix": "-",
|
|
93
|
+
"indexToken": "index",
|
|
94
|
+
"routeToken": "route"
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**주의사항:**
|
|
99
|
+
- `routeTree.gen.ts`는 자동 생성 파일 → 직접 수정 금지
|
|
100
|
+
- `__root.tsx`는 반드시 존재해야 함
|
|
101
|
+
- `_prefix`는 pathless layout, `(group)`은 정리 전용 (레이아웃 불가)
|
|
102
|
+
- lazy 파일에는 `loader`, `beforeLoad`, `validateSearch` 넣지 않음
|
|
103
|
+
|
|
104
|
+
참고: [File-Based Routing](https://tanstack.com/router/latest/docs/framework/react/routing/file-based-routing), [File Naming Conventions](https://tanstack.com/router/latest/docs/framework/react/routing/file-naming-conventions)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Link Component and useNavigate for Type-Safe Navigation
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: type-safe navigation prevents runtime route errors
|
|
5
|
+
tags: routing, navigation, link, useNavigate, type-safety
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Link와 useNavigate로 타입 안전한 네비게이션
|
|
9
|
+
|
|
10
|
+
TanStack Router는 `<Link>`(선언적)와 `useNavigate()`(명령적) 두 가지 네비게이션 방식을 제공합니다. 모든 라우트 경로, params, search가 자동 타입 추론됩니다.
|
|
11
|
+
|
|
12
|
+
**❌ 잘못된 예시 (타입 안전성 없는 네비게이션):**
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
// 문자열 하드코딩 - 오타 감지 불가
|
|
16
|
+
<a href="/posts/123">Post</a>
|
|
17
|
+
|
|
18
|
+
// useNavigate에서 from 없이 사용
|
|
19
|
+
const navigate = useNavigate()
|
|
20
|
+
navigate({ to: '/posts/$postId', params: { postId: '123' } })
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**✅ 올바른 예시 (Link 컴포넌트):**
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { Link } from '@tanstack/react-router'
|
|
27
|
+
|
|
28
|
+
// 기본 네비게이션 - to, params 모두 타입 추론
|
|
29
|
+
<Link to="/posts/$postId" params={{ postId: '123' }}>
|
|
30
|
+
Post Detail
|
|
31
|
+
</Link>
|
|
32
|
+
|
|
33
|
+
// search params 병합 (함수형 - 기존 params 유지)
|
|
34
|
+
<Link to="/posts" search={(prev) => ({ ...prev, page: 2 })}>
|
|
35
|
+
Next Page
|
|
36
|
+
</Link>
|
|
37
|
+
|
|
38
|
+
// 활성 링크 스타일링
|
|
39
|
+
<Link
|
|
40
|
+
to="/dashboard"
|
|
41
|
+
activeProps={{ className: 'font-bold text-blue-600' }}
|
|
42
|
+
activeOptions={{ exact: true }}
|
|
43
|
+
>
|
|
44
|
+
Dashboard
|
|
45
|
+
</Link>
|
|
46
|
+
|
|
47
|
+
// 프리로딩 - hover 시 미리 로드
|
|
48
|
+
<Link to="/posts/$postId" params={{ postId: '123' }} preload="intent">
|
|
49
|
+
View Post
|
|
50
|
+
</Link>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**✅ 올바른 예시 (useNavigate):**
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { useNavigate } from '@tanstack/react-router'
|
|
57
|
+
|
|
58
|
+
function PostActions({ postId }: { postId: string }) {
|
|
59
|
+
// from을 지정하면 상대 경로 + 타입 추론 향상
|
|
60
|
+
const navigate = useNavigate({ from: '/posts/$postId' })
|
|
61
|
+
|
|
62
|
+
const handleDelete = async () => {
|
|
63
|
+
await deletePost(postId)
|
|
64
|
+
navigate({ to: '/posts', replace: true })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const handleEdit = () => {
|
|
68
|
+
navigate({
|
|
69
|
+
to: '/posts/$postId/edit',
|
|
70
|
+
params: { postId },
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div>
|
|
76
|
+
<button onClick={handleEdit}>Edit</button>
|
|
77
|
+
<button onClick={handleDelete}>Delete</button>
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**search params 주의사항:**
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
// ❌ 기존 search params 덮어쓰기
|
|
87
|
+
<Link to="/posts" search={{ page: 2 }}>Next</Link>
|
|
88
|
+
|
|
89
|
+
// ✅ 기존 search params 유지 + 병합
|
|
90
|
+
<Link to="/posts" search={(prev) => ({ ...prev, page: 2 })}>Next</Link>
|
|
91
|
+
|
|
92
|
+
// ✅ 모든 search params 유지
|
|
93
|
+
<Link to="/about" search={true}>About</Link>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**activeOptions 설정:**
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
<Link
|
|
100
|
+
to="/products"
|
|
101
|
+
search={{ category: 'electronics' }}
|
|
102
|
+
activeOptions={{
|
|
103
|
+
exact: false, // 하위 경로도 활성 처리
|
|
104
|
+
includeSearch: true, // search params도 매칭 조건에 포함
|
|
105
|
+
}}
|
|
106
|
+
activeProps={{ className: 'active' }}
|
|
107
|
+
>
|
|
108
|
+
Electronics
|
|
109
|
+
</Link>
|
|
110
|
+
|
|
111
|
+
// children render function으로 동적 콘텐츠
|
|
112
|
+
<Link to="/profile">
|
|
113
|
+
{({ isActive }) => (
|
|
114
|
+
<span className={isActive ? 'font-bold' : ''}>Profile</span>
|
|
115
|
+
)}
|
|
116
|
+
</Link>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
참고: [Navigation Guide](https://tanstack.com/router/latest/docs/framework/react/guide/navigation), [Link Options](https://tanstack.com/router/latest/docs/framework/react/guide/link-options)
|