@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,84 +1,935 @@
|
|
|
1
1
|
# TanStack Router - Route Context
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> TanStack Router v1.159.4
|
|
4
|
+
|
|
5
|
+
Route context로 라우트 간 상태 공유. 인증, 권한, 사용자 정보, 의존성 주입 등.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<context_fundamentals>
|
|
10
|
+
|
|
11
|
+
## Context 기본 개념
|
|
12
|
+
|
|
13
|
+
Context는 라우트를 통해 전달되는 공유 상태. 인증, 쿼리 클라이언트, 권한 등에 활용.
|
|
14
|
+
|
|
15
|
+
### 3가지 Context 레벨
|
|
16
|
+
|
|
17
|
+
| 레벨 | 정의 위치 | 사용 범위 | 타입 |
|
|
18
|
+
|------|----------|---------|------|
|
|
19
|
+
| Root Context | `createRootRouteWithContext<T>()` | 모든 라우트 | 필수 |
|
|
20
|
+
| Route Context | `beforeLoad` 반환값 | 해당 라우트 + 자식 | 선택 |
|
|
21
|
+
| Pathless Layout | `beforeLoad` + `_authed/` | 자식 라우트만 | 선택 |
|
|
22
|
+
|
|
23
|
+
### 주요 활용 사례
|
|
24
|
+
|
|
25
|
+
- **의존성 주입**: 데이터 페칭 함수, 쿼리 클라이언트, 뮤테이션 서비스 등
|
|
26
|
+
- **빵 부스러기 (Breadcrumbs)**: 각 라우트에 제목/메타 정보 첨부
|
|
27
|
+
- **동적 메타 태그**: 라우트별 title, description 관리
|
|
28
|
+
- **인증/권한**: 사용자 정보와 권한 전파
|
|
29
|
+
|
|
30
|
+
</context_fundamentals>
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
<root_context>
|
|
35
|
+
|
|
36
|
+
## Root Context: 전역 상태
|
|
37
|
+
|
|
38
|
+
Router 초기화 시 모든 라우트에서 접근 가능한 context.
|
|
39
|
+
|
|
40
|
+
### 타입 정의
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
// /src/lib/router-context.ts
|
|
44
|
+
import { QueryClient } from '@tanstack/react-query'
|
|
45
|
+
import { User } from '@/lib/auth'
|
|
46
|
+
|
|
47
|
+
export interface RouterContext {
|
|
48
|
+
queryClient: QueryClient
|
|
49
|
+
auth: {
|
|
50
|
+
isAuthenticated: boolean
|
|
51
|
+
user: User | null
|
|
52
|
+
permissions: string[]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
> RouterContext에는 `createRouter`에 직접 전달되는 내용만 포함. beforeLoad에서 추가되는 context는 자동 추론됨.
|
|
58
|
+
|
|
59
|
+
### Root Route 정의
|
|
4
60
|
|
|
5
61
|
```tsx
|
|
6
|
-
//
|
|
62
|
+
// /src/routes/__root.tsx
|
|
63
|
+
import { createRootRouteWithContext } from '@tanstack/react-router'
|
|
64
|
+
import { RouterContext } from '@/lib/router-context'
|
|
65
|
+
|
|
66
|
+
export const Route = createRootRouteWithContext<RouterContext>()({
|
|
67
|
+
component: RootLayout,
|
|
68
|
+
notFoundComponent: NotFound,
|
|
69
|
+
})
|
|
70
|
+
// 주의: createRootRouteWithContext는 팩토리 함수라 ()() 이중 호출 필요
|
|
71
|
+
|
|
72
|
+
const RootLayout = () => (
|
|
73
|
+
<div>
|
|
74
|
+
<nav>{/* ... */}</nav>
|
|
75
|
+
<main>
|
|
76
|
+
<Outlet />
|
|
77
|
+
</main>
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Router 초기화
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// /src/main.tsx
|
|
86
|
+
import { QueryClient } from '@tanstack/react-query'
|
|
87
|
+
import { createRouter } from '@tanstack/react-router'
|
|
88
|
+
import { routeTree } from './routeTree.gen'
|
|
89
|
+
import { RouterContext } from '@/lib/router-context'
|
|
90
|
+
import { getCurrentUser } from '@/lib/auth'
|
|
91
|
+
|
|
92
|
+
const queryClient = new QueryClient()
|
|
93
|
+
|
|
94
|
+
// 초기 context 설정
|
|
95
|
+
const initialContext: RouterContext = {
|
|
96
|
+
queryClient,
|
|
97
|
+
auth: {
|
|
98
|
+
isAuthenticated: false,
|
|
99
|
+
user: null,
|
|
100
|
+
permissions: [],
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const router = createRouter({
|
|
105
|
+
routeTree,
|
|
106
|
+
context: initialContext,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// 초기 인증 상태 로드
|
|
110
|
+
const user = await getCurrentUser()
|
|
111
|
+
if (user) {
|
|
112
|
+
router.context = {
|
|
113
|
+
...router.context,
|
|
114
|
+
auth: {
|
|
115
|
+
isAuthenticated: true,
|
|
116
|
+
user,
|
|
117
|
+
permissions: user.permissions,
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
createRoot(document.getElementById('root')).render(
|
|
123
|
+
<RouterProvider router={router} />
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### RouterProvider에서 context 주입 (React 훅 사용)
|
|
128
|
+
|
|
129
|
+
React 훅을 beforeLoad/loader에서 쓸 수 없으므로, RouterProvider의 `context` prop으로 주입.
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
// /src/router.tsx
|
|
133
|
+
const router = createRouter({
|
|
134
|
+
routeTree,
|
|
135
|
+
context: {
|
|
136
|
+
networkStrength: undefined!, // React에서 나중에 설정
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// /src/main.tsx
|
|
141
|
+
import { useNetworkStrength } from '@/hooks/useNetworkStrength'
|
|
142
|
+
|
|
143
|
+
function App() {
|
|
144
|
+
const networkStrength = useNetworkStrength()
|
|
145
|
+
// 훅 결과를 context로 주입
|
|
146
|
+
return <RouterProvider router={router} context={{ networkStrength }} />
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
// /src/routes/posts.tsx
|
|
152
|
+
export const Route = createFileRoute('/posts')({
|
|
153
|
+
loader: ({ context }) => {
|
|
154
|
+
if (context.networkStrength === 'STRONG') {
|
|
155
|
+
// 고해상도 데이터 로드
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Root Context 접근
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
// 모든 라우트에서 접근 가능
|
|
165
|
+
const Dashboard = () => {
|
|
166
|
+
const { user, permissions } = Route.useRouteContext()
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div>
|
|
170
|
+
<h1>Welcome, {user?.name}!</h1>
|
|
171
|
+
{permissions.includes('admin') && <AdminPanel />}
|
|
172
|
+
</div>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
</root_context>
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
<route_context>
|
|
182
|
+
|
|
183
|
+
## Route Context: 라우트 레벨 상태
|
|
184
|
+
|
|
185
|
+
특정 라우트에서 beforeLoad로 context 추가. 자식 라우트도 접근 가능.
|
|
186
|
+
|
|
187
|
+
### beforeLoad에서 Context 생성
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
// /src/routes/dashboard.tsx
|
|
7
191
|
export const Route = createFileRoute('/dashboard')({
|
|
8
192
|
beforeLoad: async ({ context, location }) => {
|
|
193
|
+
// 인증 확인
|
|
9
194
|
if (!context.auth.isAuthenticated) {
|
|
10
195
|
throw redirect({ to: '/login', search: { redirect: location.href } })
|
|
11
196
|
}
|
|
12
|
-
|
|
197
|
+
|
|
198
|
+
// 라우트 특화 context 추가
|
|
199
|
+
const userPermissions = await fetchPermissions(context.auth.user!.id)
|
|
200
|
+
const dashboardConfig = await fetchConfig()
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
userPermissions,
|
|
204
|
+
dashboardConfig,
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
loader: async ({ context }) => {
|
|
208
|
+
// beforeLoad의 context 사용
|
|
209
|
+
const data = await fetchDashboardData(context.userPermissions)
|
|
210
|
+
return { data }
|
|
13
211
|
},
|
|
14
|
-
loader: async ({ context }) => fetchDashboardData(context.userPermissions),
|
|
15
212
|
component: DashboardPage,
|
|
16
213
|
})
|
|
17
214
|
|
|
18
|
-
|
|
215
|
+
const DashboardPage = () => {
|
|
216
|
+
// context 접근
|
|
217
|
+
const { userPermissions, dashboardConfig } = Route.useRouteContext()
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<div>
|
|
221
|
+
{userPermissions.includes('edit') && <EditButton />}
|
|
222
|
+
</div>
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### 부모 Context 확장 (누적)
|
|
228
|
+
|
|
229
|
+
context는 라우트 트리를 따라 merge됨. 각 라우트에서 추가한 context가 자식에 전파.
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
// /src/routes/_authed.tsx (parent)
|
|
19
233
|
export const Route = createFileRoute('/_authed')({
|
|
20
|
-
beforeLoad: async ({
|
|
234
|
+
beforeLoad: async ({ context }) => ({
|
|
235
|
+
user: await getCurrentUser(),
|
|
236
|
+
}),
|
|
237
|
+
component: () => <Outlet />,
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// /src/routes/_authed/dashboard.tsx (child)
|
|
241
|
+
export const Route = createFileRoute('/_authed/dashboard')({
|
|
242
|
+
beforeLoad: async ({ context }) => ({
|
|
243
|
+
// 부모의 context + 추가 context
|
|
244
|
+
dashboardData: await fetchDashboard(context.user.id),
|
|
245
|
+
}),
|
|
246
|
+
component: Dashboard,
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
const Dashboard = () => {
|
|
250
|
+
// 부모 + 자신의 context 모두 접근
|
|
251
|
+
const { user, dashboardData } = Route.useRouteContext()
|
|
252
|
+
|
|
253
|
+
return <div>User: {user.name}, Data: {dashboardData}</div>
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### 의존성 주입 패턴
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
// /src/routes/__root.tsx
|
|
261
|
+
export const Route = createRootRouteWithContext<{
|
|
262
|
+
fetchPosts: typeof fetchPosts
|
|
263
|
+
queryClient: QueryClient
|
|
264
|
+
}>()({
|
|
265
|
+
component: App,
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
// /src/routes/posts.tsx
|
|
269
|
+
export const Route = createFileRoute('/posts')({
|
|
270
|
+
loader: ({ context: { fetchPosts } }) => fetchPosts(),
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// /src/routes/todos.tsx (TanStack Query와 함께)
|
|
274
|
+
export const Route = createFileRoute('/todos')({
|
|
275
|
+
loader: async ({ context }) => {
|
|
276
|
+
await context.queryClient.ensureQueryData({
|
|
277
|
+
queryKey: ['todos'],
|
|
278
|
+
queryFn: fetchTodos,
|
|
279
|
+
})
|
|
280
|
+
},
|
|
281
|
+
})
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
</route_context>
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
<pathless_layout>
|
|
289
|
+
|
|
290
|
+
## Pathless Layout (_authed/): 인증 보호 그룹
|
|
291
|
+
|
|
292
|
+
_로 시작하는 레이아웃은 URL에 반영되지 않음. 인증, 권한 보호에 활용.
|
|
293
|
+
|
|
294
|
+
### 구조
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
routes/
|
|
298
|
+
├── __root.tsx # Root layout
|
|
299
|
+
├── _authed.tsx # Protected layout (경로 없음)
|
|
300
|
+
├── _authed/
|
|
301
|
+
│ ├── dashboard.tsx # /dashboard (protected)
|
|
302
|
+
│ ├── settings.tsx # /settings (protected)
|
|
303
|
+
│ └── profile.tsx # /profile (protected)
|
|
304
|
+
├── index.tsx # / (public)
|
|
305
|
+
├── login.tsx # /login (public)
|
|
306
|
+
└── $.tsx # Catch-all
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### 구현
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
// /src/routes/_authed.tsx
|
|
313
|
+
export const Route = createFileRoute('/_authed')({
|
|
314
|
+
beforeLoad: async ({ context, location }) => {
|
|
315
|
+
// 인증 확인
|
|
21
316
|
const user = await getCurrentUser()
|
|
22
|
-
|
|
317
|
+
|
|
318
|
+
if (!user) {
|
|
319
|
+
throw redirect({
|
|
320
|
+
to: '/login',
|
|
321
|
+
search: { redirect: location.href }, // 로그인 후 돌아올 경로
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Context에 user 추가
|
|
23
326
|
return { user }
|
|
24
327
|
},
|
|
328
|
+
// Outlet으로 자식 라우트 렌더링
|
|
25
329
|
component: () => <Outlet />,
|
|
26
330
|
})
|
|
27
331
|
|
|
28
|
-
// _authed/dashboard.tsx
|
|
332
|
+
// /src/routes/_authed/dashboard.tsx
|
|
29
333
|
export const Route = createFileRoute('/_authed/dashboard')({
|
|
30
|
-
component:
|
|
334
|
+
component: Dashboard,
|
|
31
335
|
})
|
|
32
|
-
|
|
33
|
-
|
|
336
|
+
|
|
337
|
+
const Dashboard = () => {
|
|
338
|
+
// 부모 _authed에서 전달된 user 접근
|
|
339
|
+
const { user } = Route.useRouteContext()
|
|
340
|
+
|
|
34
341
|
return <h1>Welcome, {user.name}!</h1>
|
|
35
342
|
}
|
|
36
343
|
|
|
37
|
-
//
|
|
38
|
-
|
|
344
|
+
// /src/routes/_authed/settings.tsx
|
|
345
|
+
export const Route = createFileRoute('/_authed/settings')({
|
|
346
|
+
component: Settings,
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
const Settings = () => {
|
|
350
|
+
const { user } = Route.useRouteContext()
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<div>
|
|
354
|
+
<h1>Settings for {user.email}</h1>
|
|
355
|
+
</div>
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### 공개 라우트
|
|
361
|
+
|
|
362
|
+
```tsx
|
|
363
|
+
// /src/routes/index.tsx (public, 인증 불필요)
|
|
364
|
+
export const Route = createFileRoute('/')({
|
|
365
|
+
component: HomePage,
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
// /src/routes/login.tsx (public)
|
|
369
|
+
export const Route = createFileRoute('/login')({
|
|
370
|
+
component: LoginPage,
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
const LoginPage = () => {
|
|
374
|
+
const navigate = useNavigate()
|
|
375
|
+
const searchParams = useSearch({ strict: false })
|
|
376
|
+
|
|
377
|
+
const handleLogin = async (credentials) => {
|
|
378
|
+
await authenticate(credentials)
|
|
379
|
+
|
|
380
|
+
// 로그인 후 리다이렉트 (redirect 파라미터 사용)
|
|
381
|
+
navigate({
|
|
382
|
+
to: searchParams.redirect ?? '/dashboard',
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return <LoginForm onSubmit={handleLogin} />
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
</pathless_layout>
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
<before_load>
|
|
395
|
+
|
|
396
|
+
## beforeLoad: Context 추가 시점
|
|
397
|
+
|
|
398
|
+
beforeLoad는 loader 전에 실행. Context 추가에 최적.
|
|
399
|
+
|
|
400
|
+
### 실행 순서
|
|
401
|
+
|
|
402
|
+
```
|
|
403
|
+
1. beforeLoad() 실행 (Serial, Top-Down)
|
|
404
|
+
|- context 접근 (Root + 부모 beforeLoad context)
|
|
405
|
+
|- location 접근
|
|
406
|
+
|- search 접근
|
|
407
|
+
+- context 추가 반환
|
|
408
|
+
|
|
|
409
|
+
2. loader() 실행 (Parallel)
|
|
410
|
+
|- 추가된 context 사용
|
|
411
|
+
+- 데이터 반환
|
|
412
|
+
|
|
|
413
|
+
3. component 렌더링
|
|
414
|
+
|- Route.useRouteContext() 사용
|
|
415
|
+
+- loader 데이터 사용
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### beforeLoad 파라미터
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
interface BeforeLoadParams {
|
|
422
|
+
context: RouterContext // Root + 부모 context
|
|
423
|
+
location: Location // 현재 위치 정보
|
|
424
|
+
search: SearchParams // 검증된 search params
|
|
425
|
+
params: PathParams // 경로 파라미터
|
|
426
|
+
cause: 'enter' | 'stay' // 진입 원인
|
|
427
|
+
abortController: AbortController
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### beforeLoad에서 인증 체크
|
|
432
|
+
|
|
433
|
+
```tsx
|
|
434
|
+
export const Route = createFileRoute('/admin')({
|
|
435
|
+
beforeLoad: async ({ context, location }) => {
|
|
436
|
+
if (!context.auth.isAuthenticated) {
|
|
437
|
+
throw redirect({
|
|
438
|
+
to: '/login',
|
|
439
|
+
search: { redirect: location.href },
|
|
440
|
+
})
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (!context.auth.permissions.includes('admin')) {
|
|
444
|
+
throw new Error('Access denied')
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 추가 context
|
|
448
|
+
const adminData = await fetchAdminData()
|
|
449
|
+
|
|
450
|
+
return { adminData }
|
|
451
|
+
},
|
|
452
|
+
loader: async ({ context }) => {
|
|
453
|
+
// beforeLoad에서 추가된 adminData 사용
|
|
454
|
+
return fetchFullAdminPanel(context.adminData)
|
|
455
|
+
},
|
|
456
|
+
component: AdminPanel,
|
|
457
|
+
})
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### 검색 쿼리 기반 Context
|
|
461
|
+
|
|
462
|
+
```tsx
|
|
463
|
+
export const Route = createFileRoute('/posts')({
|
|
464
|
+
beforeLoad: async ({ search, context }) => {
|
|
465
|
+
// Search params 기반 context
|
|
466
|
+
const filters = {
|
|
467
|
+
category: search.category ?? 'all',
|
|
468
|
+
sort: search.sort ?? 'newest',
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return { filters }
|
|
472
|
+
},
|
|
473
|
+
loader: async ({ context }) => {
|
|
474
|
+
// context.filters로 필터링
|
|
475
|
+
return fetchPosts(context.filters)
|
|
476
|
+
},
|
|
477
|
+
component: Posts,
|
|
478
|
+
})
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
</before_load>
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
<context_access>
|
|
486
|
+
|
|
487
|
+
## Context 접근 방법
|
|
488
|
+
|
|
489
|
+
| 위치 | 접근 방법 | 접근 가능 |
|
|
490
|
+
|------|----------|---------|
|
|
491
|
+
| `beforeLoad()` | `{ context }` 파라미터 | Root context + 부모 beforeLoad context |
|
|
492
|
+
| `loader()` | `{ context }` 파라미터 | Root + Route context (beforeLoad 포함) |
|
|
493
|
+
| `component` | `Route.useRouteContext()` | Root + Route context |
|
|
494
|
+
| 다른 라우트 | `useRouterState()` | Root context만 |
|
|
495
|
+
|
|
496
|
+
### Component에서 접근
|
|
497
|
+
|
|
498
|
+
```tsx
|
|
499
|
+
const Page = () => {
|
|
500
|
+
// 현재 라우트 context (Root + Route-specific)
|
|
501
|
+
const context = Route.useRouteContext()
|
|
502
|
+
|
|
503
|
+
return <div>{context.user?.name}</div>
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### Loader에서 접근
|
|
508
|
+
|
|
509
|
+
```tsx
|
|
510
|
+
export const Route = createFileRoute('/posts')({
|
|
511
|
+
loader: async ({ context }) => {
|
|
512
|
+
// Root context + 부모 beforeLoad context
|
|
513
|
+
console.log(context.queryClient) // Root
|
|
514
|
+
console.log(context.user) // 부모 beforeLoad에서 추가됨
|
|
515
|
+
return fetchPosts()
|
|
516
|
+
},
|
|
517
|
+
component: Posts,
|
|
518
|
+
})
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### 누적 Context로 Breadcrumb 생성
|
|
522
|
+
|
|
523
|
+
```tsx
|
|
524
|
+
// /src/routes/__root.tsx
|
|
525
|
+
export const Route = createRootRoute({
|
|
526
|
+
component: () => {
|
|
527
|
+
const matches = useRouterState({ select: s => s.matches })
|
|
528
|
+
|
|
529
|
+
const breadcrumbs = matches
|
|
530
|
+
.filter(match => match.context.getTitle)
|
|
531
|
+
.map(({ pathname, context }) => ({
|
|
532
|
+
title: context.getTitle(),
|
|
533
|
+
path: pathname,
|
|
534
|
+
}))
|
|
535
|
+
|
|
536
|
+
return (
|
|
537
|
+
<div>
|
|
538
|
+
<nav>{breadcrumbs.map(b => (
|
|
539
|
+
<Link key={b.path} to={b.path}>{b.title}</Link>
|
|
540
|
+
))}</nav>
|
|
541
|
+
<Outlet />
|
|
542
|
+
</div>
|
|
543
|
+
)
|
|
544
|
+
},
|
|
545
|
+
})
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### 동적 타이틀 관리
|
|
549
|
+
|
|
550
|
+
```tsx
|
|
551
|
+
export const Route = createRootRoute({
|
|
552
|
+
component: () => {
|
|
553
|
+
const matches = useRouterState({ select: s => s.matches })
|
|
554
|
+
|
|
555
|
+
const matchWithTitle = [...matches]
|
|
556
|
+
.reverse()
|
|
557
|
+
.find(d => d.context.getTitle)
|
|
558
|
+
|
|
559
|
+
const title = matchWithTitle?.context.getTitle() || 'My App'
|
|
560
|
+
|
|
561
|
+
return (
|
|
562
|
+
<html>
|
|
563
|
+
<head>
|
|
564
|
+
<title>{title}</title>
|
|
565
|
+
</head>
|
|
566
|
+
<body>
|
|
567
|
+
<Outlet />
|
|
568
|
+
</body>
|
|
569
|
+
</html>
|
|
570
|
+
)
|
|
571
|
+
},
|
|
572
|
+
})
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
</context_access>
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
<redirect>
|
|
580
|
+
|
|
581
|
+
## redirect(): 조건부 리다이렉트
|
|
582
|
+
|
|
583
|
+
beforeLoad에서 throw하여 즉시 리다이렉트.
|
|
584
|
+
|
|
585
|
+
### 기본 사용
|
|
586
|
+
|
|
587
|
+
```tsx
|
|
588
|
+
import { redirect } from '@tanstack/react-router'
|
|
589
|
+
|
|
590
|
+
export const Route = createFileRoute('/dashboard')({
|
|
591
|
+
beforeLoad: async ({ context }) => {
|
|
592
|
+
if (!context.auth.isAuthenticated) {
|
|
593
|
+
throw redirect({ to: '/login' })
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
component: Dashboard,
|
|
597
|
+
})
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### 리다이렉트 후 돌아오기
|
|
601
|
+
|
|
602
|
+
```tsx
|
|
603
|
+
export const Route = createFileRoute('/_authed')({
|
|
604
|
+
beforeLoad: async ({ context, location }) => {
|
|
605
|
+
if (!context.auth.isAuthenticated) {
|
|
606
|
+
throw redirect({
|
|
607
|
+
to: '/login',
|
|
608
|
+
// Search params에 현재 경로 저장
|
|
609
|
+
search: { redirect: location.href },
|
|
610
|
+
})
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
component: () => <Outlet />,
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
// /src/routes/login.tsx
|
|
617
|
+
export const Route = createFileRoute('/login')({
|
|
618
|
+
component: LoginPage,
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
const LoginPage = () => {
|
|
622
|
+
const navigate = useNavigate()
|
|
623
|
+
const { redirect: redirectPath } = useSearch()
|
|
624
|
+
|
|
625
|
+
const handleLogin = async (credentials) => {
|
|
626
|
+
await authenticate(credentials)
|
|
627
|
+
|
|
628
|
+
// 원래 경로로 리다이렉트
|
|
629
|
+
navigate({ to: redirectPath ?? '/dashboard' })
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return <LoginForm onSubmit={handleLogin} />
|
|
633
|
+
}
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### Params와 Search 함께 사용
|
|
637
|
+
|
|
638
|
+
```tsx
|
|
639
|
+
throw redirect({
|
|
640
|
+
to: '/posts/$postId',
|
|
641
|
+
params: { postId: '123' },
|
|
642
|
+
search: { tab: 'comments' },
|
|
643
|
+
})
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### History Replace
|
|
647
|
+
|
|
648
|
+
```tsx
|
|
649
|
+
// 브라우저 히스토리에 남지 않음 (뒤로 가기 시 이전 페이지로)
|
|
650
|
+
throw redirect({
|
|
651
|
+
to: '/login',
|
|
652
|
+
replace: true,
|
|
653
|
+
})
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### 조건부 리다이렉트
|
|
657
|
+
|
|
658
|
+
```tsx
|
|
659
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
660
|
+
beforeLoad: async ({ params, context }) => {
|
|
661
|
+
// 삭제된 포스트: 목록으로
|
|
662
|
+
const post = await getPost(params.postId)
|
|
663
|
+
if (post.isDeleted) {
|
|
664
|
+
throw redirect({ to: '/posts' })
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// 비공개 포스트 + 소유자 아님: 홈으로
|
|
668
|
+
if (!post.isPublished && post.ownerId !== context.auth.user?.id) {
|
|
669
|
+
throw redirect({ to: '/' })
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return { post }
|
|
673
|
+
},
|
|
674
|
+
component: PostDetail,
|
|
675
|
+
})
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
</redirect>
|
|
679
|
+
|
|
680
|
+
---
|
|
681
|
+
|
|
682
|
+
<auth_example>
|
|
683
|
+
|
|
684
|
+
## 실전 예시: Better Auth 통합
|
|
685
|
+
|
|
686
|
+
Better Auth와 Route Context 통합.
|
|
687
|
+
|
|
688
|
+
### Context 타입
|
|
689
|
+
|
|
690
|
+
```tsx
|
|
691
|
+
// /src/lib/router-context.ts
|
|
692
|
+
import { QueryClient } from '@tanstack/react-query'
|
|
693
|
+
import { User, Session } from 'better-auth'
|
|
694
|
+
|
|
695
|
+
export interface RouterContext {
|
|
39
696
|
queryClient: QueryClient
|
|
40
|
-
auth: {
|
|
697
|
+
auth: {
|
|
698
|
+
isAuthenticated: boolean
|
|
699
|
+
user: User | null
|
|
700
|
+
session: Session | null
|
|
701
|
+
}
|
|
41
702
|
}
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### Root Route
|
|
706
|
+
|
|
707
|
+
```tsx
|
|
708
|
+
// /src/routes/__root.tsx
|
|
709
|
+
import { createRootRouteWithContext } from '@tanstack/react-router'
|
|
710
|
+
import { RouterContext } from '@/lib/router-context'
|
|
42
711
|
|
|
43
712
|
export const Route = createRootRouteWithContext<RouterContext>()({
|
|
44
713
|
component: RootLayout,
|
|
45
714
|
})
|
|
46
715
|
|
|
716
|
+
const RootLayout = () => (
|
|
717
|
+
<div>
|
|
718
|
+
<Header />
|
|
719
|
+
<Outlet />
|
|
720
|
+
</div>
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
const Header = () => {
|
|
724
|
+
const { user } = Route.useRouteContext()
|
|
725
|
+
|
|
726
|
+
return (
|
|
727
|
+
<header>
|
|
728
|
+
{user && <span>Logged in as {user.email}</span>}
|
|
729
|
+
</header>
|
|
730
|
+
)
|
|
731
|
+
}
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
### Protected Layout
|
|
735
|
+
|
|
736
|
+
```tsx
|
|
737
|
+
// /src/routes/_authed.tsx
|
|
738
|
+
import { redirect } from '@tanstack/react-router'
|
|
739
|
+
import { authClient } from '@/lib/auth-client'
|
|
740
|
+
|
|
741
|
+
export const Route = createFileRoute('/_authed')({
|
|
742
|
+
beforeLoad: async ({ context, location }) => {
|
|
743
|
+
// Better Auth로 세션 확인
|
|
744
|
+
const session = await authClient.getSession()
|
|
745
|
+
|
|
746
|
+
if (!session) {
|
|
747
|
+
throw redirect({
|
|
748
|
+
to: '/login',
|
|
749
|
+
search: { redirect: location.href },
|
|
750
|
+
})
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Context 업데이트
|
|
754
|
+
context.auth = {
|
|
755
|
+
isAuthenticated: true,
|
|
756
|
+
user: session.user,
|
|
757
|
+
session,
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return {}
|
|
761
|
+
},
|
|
762
|
+
component: () => <Outlet />,
|
|
763
|
+
})
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
### Protected Route
|
|
767
|
+
|
|
768
|
+
```tsx
|
|
769
|
+
// /src/routes/_authed/dashboard.tsx
|
|
770
|
+
export const Route = createFileRoute('/_authed/dashboard')({
|
|
771
|
+
component: Dashboard,
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
const Dashboard = () => {
|
|
775
|
+
const { user } = Route.useRouteContext()
|
|
776
|
+
|
|
777
|
+
return (
|
|
778
|
+
<div>
|
|
779
|
+
<h1>Welcome, {user.name || user.email}!</h1>
|
|
780
|
+
<Link to="/logout">Logout</Link>
|
|
781
|
+
</div>
|
|
782
|
+
)
|
|
783
|
+
}
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
### Router 초기화
|
|
787
|
+
|
|
788
|
+
```tsx
|
|
789
|
+
// /src/main.tsx
|
|
790
|
+
import { QueryClient } from '@tanstack/react-query'
|
|
791
|
+
import { createRouter } from '@tanstack/react-router'
|
|
792
|
+
import { authClient } from '@/lib/auth-client'
|
|
793
|
+
import { routeTree } from './routeTree.gen'
|
|
794
|
+
import { RouterContext } from '@/lib/router-context'
|
|
795
|
+
|
|
796
|
+
const queryClient = new QueryClient()
|
|
797
|
+
|
|
798
|
+
// 초기 컨텍스트
|
|
799
|
+
const initialContext: RouterContext = {
|
|
800
|
+
queryClient,
|
|
801
|
+
auth: {
|
|
802
|
+
isAuthenticated: false,
|
|
803
|
+
user: null,
|
|
804
|
+
session: null,
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
47
808
|
const router = createRouter({
|
|
48
809
|
routeTree,
|
|
49
|
-
context:
|
|
810
|
+
context: initialContext,
|
|
50
811
|
})
|
|
51
812
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
813
|
+
// 세션 로드
|
|
814
|
+
const session = await authClient.getSession()
|
|
815
|
+
if (session) {
|
|
816
|
+
router.context = {
|
|
817
|
+
...router.context,
|
|
818
|
+
auth: {
|
|
819
|
+
isAuthenticated: true,
|
|
820
|
+
user: session.user,
|
|
821
|
+
session,
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
createRoot(document.getElementById('root')).render(
|
|
827
|
+
<RouterProvider router={router} />
|
|
828
|
+
)
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
</auth_example>
|
|
832
|
+
|
|
833
|
+
---
|
|
834
|
+
|
|
835
|
+
<router_invalidate>
|
|
836
|
+
|
|
837
|
+
## router.invalidate(): Context 재설정
|
|
838
|
+
|
|
839
|
+
인증 상태 변경 시 라우터 상태 갱신. 모든 활성 라우트의 loader를 즉시 재실행하고 캐시를 stale로 표시.
|
|
840
|
+
|
|
841
|
+
### 로그인 후
|
|
842
|
+
|
|
843
|
+
```tsx
|
|
844
|
+
const LoginPage = () => {
|
|
845
|
+
const router = useRouter()
|
|
846
|
+
const navigate = useNavigate()
|
|
847
|
+
|
|
848
|
+
const handleLogin = async (credentials) => {
|
|
849
|
+
await authenticate(credentials)
|
|
850
|
+
|
|
851
|
+
// 라우터 context 갱신 (loader와 beforeLoad 재실행)
|
|
852
|
+
await router.invalidate()
|
|
853
|
+
|
|
854
|
+
// 대시보드로 이동
|
|
855
|
+
navigate({ to: '/dashboard' })
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return <LoginForm onSubmit={handleLogin} />
|
|
859
|
+
}
|
|
57
860
|
```
|
|
58
861
|
|
|
59
|
-
|
|
862
|
+
### 로그아웃 후
|
|
863
|
+
|
|
864
|
+
```tsx
|
|
865
|
+
const LogoutButton = () => {
|
|
866
|
+
const router = useRouter()
|
|
867
|
+
const navigate = useNavigate()
|
|
60
868
|
|
|
61
|
-
|
|
869
|
+
const handleLogout = async () => {
|
|
870
|
+
await logout()
|
|
62
871
|
|
|
872
|
+
// Context 갱신
|
|
873
|
+
router.context = {
|
|
874
|
+
...router.context,
|
|
875
|
+
auth: {
|
|
876
|
+
isAuthenticated: false,
|
|
877
|
+
user: null,
|
|
878
|
+
session: null,
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// 라우터 상태 갱신
|
|
883
|
+
await router.invalidate()
|
|
884
|
+
|
|
885
|
+
// 홈으로 이동
|
|
886
|
+
navigate({ to: '/' })
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
return <button onClick={handleLogout}>Logout</button>
|
|
890
|
+
}
|
|
63
891
|
```
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
892
|
+
|
|
893
|
+
### 인증 상태 변경 감지
|
|
894
|
+
|
|
895
|
+
```tsx
|
|
896
|
+
function useAuth() {
|
|
897
|
+
const router = useRouter()
|
|
898
|
+
const [user, setUser] = useState<User | null>(null)
|
|
899
|
+
|
|
900
|
+
useEffect(() => {
|
|
901
|
+
const unsubscribe = auth.onAuthStateChanged(user => {
|
|
902
|
+
setUser(user)
|
|
903
|
+
router.invalidate() // 인증 변경 시 라우터 갱신
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
return unsubscribe
|
|
907
|
+
}, [])
|
|
908
|
+
|
|
909
|
+
return user
|
|
910
|
+
}
|
|
72
911
|
```
|
|
73
912
|
|
|
74
|
-
</
|
|
913
|
+
</router_invalidate>
|
|
914
|
+
|
|
915
|
+
---
|
|
916
|
+
|
|
917
|
+
<dos_donts>
|
|
75
918
|
|
|
76
|
-
|
|
919
|
+
## Do's & Don'ts
|
|
77
920
|
|
|
78
|
-
|
|
|
79
|
-
|
|
80
|
-
|
|
|
81
|
-
|
|
|
82
|
-
|
|
|
921
|
+
| Do | Don't |
|
|
922
|
+
|-------|---------|
|
|
923
|
+
| `createRootRouteWithContext<T>()` | context 없이 root 생성 |
|
|
924
|
+
| `beforeLoad`에서 인증 체크 | `component`에서 try-catch |
|
|
925
|
+
| `_authed/` layout으로 그룹 보호 | 모든 라우트에 인증 로직 중복 |
|
|
926
|
+
| `redirect()` throw | `navigate()`로 리다이렉트 |
|
|
927
|
+
| `beforeLoad`에서 context 추가 | `loader`에서 context 추가 |
|
|
928
|
+
| `Route.useRouteContext()` | 글로벌 상태로 context 관리 |
|
|
929
|
+
| Root context 최소화 | Root에 모든 상태 넣기 |
|
|
930
|
+
| Route-specific context 사용 | 모든 context Root에 정의 |
|
|
931
|
+
| RouterProvider context로 React 훅 주입 | beforeLoad에서 React 훅 호출 |
|
|
932
|
+
| `router.invalidate()` 인증 변경 시 | 수동으로 context 동기화 |
|
|
933
|
+
| `()()` 이중 호출 (팩토리 패턴) | `createRootRouteWithContext<T>()` 단일 호출 |
|
|
83
934
|
|
|
84
|
-
</
|
|
935
|
+
</dos_donts>
|