@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,10 +1,138 @@
|
|
|
1
1
|
# TanStack Start - Middleware
|
|
2
2
|
|
|
3
|
-
Server
|
|
3
|
+
> Server Functions 및 라우트에 공통 로직 적용 (v1.159.4)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<overview>
|
|
8
|
+
|
|
9
|
+
## Middleware란?
|
|
10
|
+
|
|
11
|
+
Middleware는 Server Function 또는 라우트 핸들러 실행 전에 공통 로직을 처리하는 함수입니다.
|
|
12
|
+
|
|
13
|
+
| 용도 | 예시 |
|
|
14
|
+
|------|------|
|
|
15
|
+
| **인증** | 세션/토큰 검증 |
|
|
16
|
+
| **권한** | 역할 기반 접근 제어 |
|
|
17
|
+
| **검증** | 입력값 Zod 검증 |
|
|
18
|
+
| **로깅** | 요청/응답 로깅 |
|
|
19
|
+
| **CORS** | 교차 출처 요청 처리 |
|
|
20
|
+
| **CSP** | Content Security Policy 설정 |
|
|
21
|
+
| **Observability** | 메트릭, 트레이스, 로그 |
|
|
22
|
+
| **Context 제공** | 요청 객체에 데이터 첨부 |
|
|
23
|
+
| **에러 처리** | 일관된 에러 핸들링 |
|
|
24
|
+
|
|
25
|
+
</overview>
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<middleware_types>
|
|
30
|
+
|
|
31
|
+
## 미들웨어 타입
|
|
32
|
+
|
|
33
|
+
TanStack Start는 두 가지 미들웨어 타입을 제공합니다:
|
|
34
|
+
|
|
35
|
+
| 타입 | 범위 | 메서드 | 입력 검증 | 클라이언트 로직 |
|
|
36
|
+
|------|------|--------|----------|----------------|
|
|
37
|
+
| **Request Middleware** | 모든 서버 요청 (SSR, Server Routes, Server Functions) | `.server()` | 없음 | 없음 |
|
|
38
|
+
| **Server Function Middleware** | Server Functions만 | `.client()`, `.server()` | `.inputValidator()` | 있음 |
|
|
39
|
+
|
|
40
|
+
> **참고:** Request Middleware는 Server Function Middleware에 의존할 수 없지만, Server Function Middleware는 Request Middleware에 의존할 수 있습니다.
|
|
41
|
+
|
|
42
|
+
### Request Middleware
|
|
43
|
+
|
|
44
|
+
모든 서버 요청 (SSR, Server Routes, Server Functions)에 적용됩니다.
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { createMiddleware } from '@tanstack/react-start'
|
|
48
|
+
|
|
49
|
+
// 기본 (type 생략 시 'request'가 기본값)
|
|
50
|
+
const loggingMiddleware = createMiddleware().server(({ next }) => {
|
|
51
|
+
console.log('Processing request')
|
|
52
|
+
return next()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// 명시적 type 지정
|
|
56
|
+
const explicitRequestMiddleware = createMiddleware({ type: 'request' }).server(
|
|
57
|
+
({ next, context, request }) => {
|
|
58
|
+
return next()
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**사용 가능한 메서드:**
|
|
64
|
+
- `middleware`: 체인에 다른 미들웨어 추가
|
|
65
|
+
- `server`: 서버 사이드 로직 정의
|
|
66
|
+
|
|
67
|
+
### Server Function Middleware
|
|
68
|
+
|
|
69
|
+
Server Functions 전용으로, 클라이언트/서버 양쪽에서 로직 실행이 가능합니다.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
const functionMiddleware = createMiddleware({ type: 'function' })
|
|
73
|
+
.client(async ({ next, context }) => {
|
|
74
|
+
console.log('Client: before RPC')
|
|
75
|
+
const result = await next()
|
|
76
|
+
console.log('Client: after RPC')
|
|
77
|
+
return result
|
|
78
|
+
})
|
|
79
|
+
.server(async ({ next }) => {
|
|
80
|
+
console.log('Server: executing')
|
|
81
|
+
return next()
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**사용 가능한 메서드 (순서 중요 - TypeScript가 강제):**
|
|
86
|
+
1. `middleware`: 체인에 다른 미들웨어 추가
|
|
87
|
+
2. `inputValidator`: 데이터 검증
|
|
88
|
+
3. `client`: 클라이언트 사이드 로직 (RPC 호출 전후)
|
|
89
|
+
4. `server`: 서버 사이드 로직
|
|
90
|
+
|
|
91
|
+
</middleware_types>
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
<execution_flow>
|
|
96
|
+
|
|
97
|
+
## 실행 흐름
|
|
98
|
+
|
|
99
|
+
### Request Middleware 흐름
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
HTTP Request
|
|
103
|
+
-> Middleware.server (next() 호출)
|
|
104
|
+
-> ServerFn (payload 처리)
|
|
105
|
+
<- result 반환
|
|
106
|
+
<- return
|
|
107
|
+
HTTP Response
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Server Function Middleware 흐름
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
ServerFn (client) -> Middleware.client (payload)
|
|
114
|
+
-> Middleware.client: next()
|
|
115
|
+
-> Middleware.server (Request)
|
|
116
|
+
-> Middleware.server: next()
|
|
117
|
+
-> ServerFn (payload 처리)
|
|
118
|
+
<- result 반환
|
|
119
|
+
<- return
|
|
120
|
+
<- Response
|
|
121
|
+
<- return
|
|
122
|
+
ServerFn (client) <- result
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
</execution_flow>
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
<basic_pattern>
|
|
130
|
+
|
|
131
|
+
## 기본 미들웨어 패턴
|
|
6
132
|
|
|
7
133
|
```typescript
|
|
134
|
+
import { createMiddleware, createServerFn } from '@tanstack/react-start'
|
|
135
|
+
|
|
8
136
|
// 미들웨어 정의
|
|
9
137
|
const loggingMiddleware = createMiddleware({ type: 'function' })
|
|
10
138
|
.server(({ next }) => {
|
|
@@ -12,53 +140,1004 @@ const loggingMiddleware = createMiddleware({ type: 'function' })
|
|
|
12
140
|
return next()
|
|
13
141
|
})
|
|
14
142
|
|
|
15
|
-
// 적용
|
|
16
|
-
const
|
|
143
|
+
// Server Function에 적용
|
|
144
|
+
export const sayHello = createServerFn({ method: 'GET' })
|
|
17
145
|
.middleware([loggingMiddleware])
|
|
18
|
-
.handler(async ()
|
|
146
|
+
.handler(async (): Promise<{ message: string }> => {
|
|
147
|
+
return { message: 'Hello' }
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 미들웨어 조합 (Composition)
|
|
152
|
+
|
|
153
|
+
미들웨어는 다른 미들웨어에 의존할 수 있습니다.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const loggingMiddleware = createMiddleware().server(() => {
|
|
157
|
+
//...
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const authMiddleware = createMiddleware()
|
|
161
|
+
.middleware([loggingMiddleware]) // loggingMiddleware에 의존
|
|
162
|
+
.server(() => {
|
|
163
|
+
//...
|
|
164
|
+
})
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### next()로 체인 진행
|
|
168
|
+
|
|
169
|
+
모든 미들웨어는 `next()` 함수를 호출해야 다음 미들웨어가 실행됩니다:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
const loggingMiddleware = createMiddleware().server(async ({ next }) => {
|
|
173
|
+
const result = await next() // 다음 미들웨어 실행
|
|
174
|
+
return result
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
</basic_pattern>
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
<context_management>
|
|
183
|
+
|
|
184
|
+
## Context 관리
|
|
185
|
+
|
|
186
|
+
### Context 전달 (next를 통해)
|
|
187
|
+
|
|
188
|
+
`next()` 호출 시 `context` 객체를 전달하면 하위 미들웨어에 데이터를 제공합니다.
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
const awesomeMiddleware = createMiddleware({ type: 'function' }).server(
|
|
192
|
+
({ next }) => {
|
|
193
|
+
return next({
|
|
194
|
+
context: {
|
|
195
|
+
isAwesome: Math.random() > 0.5,
|
|
196
|
+
},
|
|
197
|
+
})
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
const loggingMiddleware = createMiddleware({ type: 'function' })
|
|
202
|
+
.middleware([awesomeMiddleware])
|
|
203
|
+
.server(async ({ next, context }) => {
|
|
204
|
+
console.log('Is awesome?', context.isAwesome) // 타입 안전
|
|
205
|
+
return next()
|
|
206
|
+
})
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Context 누적
|
|
210
|
+
|
|
211
|
+
미들웨어가 순차적으로 context를 누적합니다.
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
const middleware1 = createMiddleware({ type: 'function' })
|
|
215
|
+
.server(({ next }) => next({ context: { data1: 'value1' } }))
|
|
216
|
+
|
|
217
|
+
const middleware2 = createMiddleware({ type: 'function' })
|
|
218
|
+
.server(({ next, context }) => next({
|
|
219
|
+
context: { ...context, data2: 'value2' }
|
|
220
|
+
}))
|
|
221
|
+
|
|
222
|
+
export const fn = createServerFn({ method: 'GET' })
|
|
223
|
+
.middleware([middleware1, middleware2])
|
|
224
|
+
.handler(async ({ context }): Promise<unknown> => {
|
|
225
|
+
// context = { data1: 'value1', data2: 'value2' }
|
|
226
|
+
return context
|
|
227
|
+
})
|
|
19
228
|
```
|
|
20
229
|
|
|
230
|
+
</context_management>
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
<send_context>
|
|
235
|
+
|
|
236
|
+
## sendContext: 클라이언트-서버 간 컨텍스트 전송
|
|
237
|
+
|
|
238
|
+
### 클라이언트에서 서버로 (Client -> Server)
|
|
239
|
+
|
|
240
|
+
**클라이언트 컨텍스트는 기본적으로 서버로 전송되지 않습니다.** 대규모 페이로드가 의도치 않게 서버로 전송되는 것을 방지하기 위해서입니다. `sendContext`를 사용하면 명시적으로 데이터를 서버에 전송할 수 있습니다.
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { createMiddleware } from '@tanstack/react-start'
|
|
244
|
+
|
|
245
|
+
const requestLogger = createMiddleware({ type: 'function' })
|
|
246
|
+
.client(async ({ next, context }) => {
|
|
247
|
+
return next({
|
|
248
|
+
sendContext: {
|
|
249
|
+
// 클라이언트의 workspaceId를 서버로 전송
|
|
250
|
+
workspaceId: context.workspaceId,
|
|
251
|
+
},
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
.server(async ({ next, data, context }) => {
|
|
255
|
+
// 서버에서 클라이언트가 보낸 workspaceId 접근 가능
|
|
256
|
+
console.log('Workspace ID:', context.workspaceId)
|
|
257
|
+
return next()
|
|
258
|
+
})
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
#### sendContext 보안 주의사항
|
|
262
|
+
|
|
263
|
+
`sendContext`로 전달된 데이터는 타입 안전하지만, 런타임 검증이 자동으로 이루어지지 않습니다. **동적 사용자 데이터를 전송할 때는 반드시 서버에서 검증해야 합니다.**
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
import { createMiddleware } from '@tanstack/react-start'
|
|
267
|
+
import { zodValidator } from '@tanstack/zod-adapter'
|
|
268
|
+
import { z } from 'zod'
|
|
269
|
+
|
|
270
|
+
const requestLogger = createMiddleware({ type: 'function' })
|
|
271
|
+
.client(async ({ next, context }) => {
|
|
272
|
+
return next({
|
|
273
|
+
sendContext: {
|
|
274
|
+
workspaceId: context.workspaceId,
|
|
275
|
+
},
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
.server(async ({ next, data, context }) => {
|
|
279
|
+
// 사용 전 반드시 검증
|
|
280
|
+
const workspaceId = zodValidator(z.number()).parse(context.workspaceId)
|
|
281
|
+
console.log('Workspace ID:', workspaceId)
|
|
282
|
+
return next()
|
|
283
|
+
})
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### 서버에서 클라이언트로 (Server -> Client)
|
|
287
|
+
|
|
288
|
+
서버에서도 `sendContext`를 통해 클라이언트로 데이터를 전송할 수 있습니다. 전송된 데이터는 클라이언트 미들웨어의 `next()` 반환값의 `context`에서 접근 가능합니다.
|
|
289
|
+
|
|
290
|
+
> **주의:** `next()` 반환값의 타입은 현재 미들웨어 체인에서 알려진 미들웨어에서만 추론됩니다. 따라서 체인 마지막 미들웨어에서 가장 정확한 타입을 얻을 수 있습니다.
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { createMiddleware } from '@tanstack/react-start'
|
|
294
|
+
|
|
295
|
+
// 서버에서 시간 정보를 클라이언트로 전송
|
|
296
|
+
const serverTimer = createMiddleware({ type: 'function' }).server(
|
|
297
|
+
async ({ next }) => {
|
|
298
|
+
return next({
|
|
299
|
+
sendContext: {
|
|
300
|
+
timeFromServer: new Date(),
|
|
301
|
+
},
|
|
302
|
+
})
|
|
303
|
+
},
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
const requestLogger = createMiddleware({ type: 'function' })
|
|
307
|
+
.middleware([serverTimer])
|
|
308
|
+
.client(async ({ next }) => {
|
|
309
|
+
const result = await next()
|
|
310
|
+
// 서버에서 보낸 시간 정보에 접근 가능
|
|
311
|
+
console.log('Time from the server:', result.context.timeFromServer)
|
|
312
|
+
return result
|
|
313
|
+
})
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
</send_context>
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
<custom_headers>
|
|
321
|
+
|
|
322
|
+
## 커스텀 헤더 설정
|
|
323
|
+
|
|
324
|
+
### 클라이언트 미들웨어에서 헤더 추가
|
|
325
|
+
|
|
326
|
+
`.client()` 메서드에서 `next()`에 `headers` 객체를 전달하여 요청 헤더를 설정할 수 있습니다.
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
import { createMiddleware } from '@tanstack/react-start'
|
|
330
|
+
import { getToken } from 'my-auth-library'
|
|
331
|
+
|
|
332
|
+
const authMiddleware = createMiddleware({ type: 'function' }).client(
|
|
333
|
+
async ({ next }) => {
|
|
334
|
+
return next({
|
|
335
|
+
headers: {
|
|
336
|
+
Authorization: `Bearer ${getToken()}`,
|
|
337
|
+
},
|
|
338
|
+
})
|
|
339
|
+
},
|
|
340
|
+
)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### 헤더 머징 (다중 미들웨어)
|
|
344
|
+
|
|
345
|
+
여러 미들웨어가 헤더를 설정하면, 모두 **병합**됩니다. 후속 미들웨어가 동일 키의 헤더를 덮어씁니다.
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
const firstMiddleware = createMiddleware({ type: 'function' }).client(
|
|
349
|
+
async ({ next }) => {
|
|
350
|
+
return next({
|
|
351
|
+
headers: {
|
|
352
|
+
'X-Request-ID': '12345',
|
|
353
|
+
'X-Source': 'first-middleware',
|
|
354
|
+
},
|
|
355
|
+
})
|
|
356
|
+
},
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
const secondMiddleware = createMiddleware({ type: 'function' }).client(
|
|
360
|
+
async ({ next }) => {
|
|
361
|
+
return next({
|
|
362
|
+
headers: {
|
|
363
|
+
'X-Timestamp': Date.now().toString(),
|
|
364
|
+
'X-Source': 'second-middleware', // first를 덮어씀
|
|
365
|
+
},
|
|
366
|
+
})
|
|
367
|
+
},
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
// 최종 헤더:
|
|
371
|
+
// - X-Request-ID: '12345' (first에서)
|
|
372
|
+
// - X-Timestamp: '<timestamp>' (second에서)
|
|
373
|
+
// - X-Source: 'second-middleware' (second가 first를 덮어씀)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### 호출 시점에서 헤더 설정
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
await myServerFn({
|
|
380
|
+
data: { name: 'John' },
|
|
381
|
+
headers: {
|
|
382
|
+
'X-Custom-Header': 'call-site-value',
|
|
383
|
+
},
|
|
384
|
+
})
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
**헤더 우선순위 (모두 병합, 후순위가 덮어씀):**
|
|
388
|
+
|
|
389
|
+
| 우선순위 | 출처 | 설명 |
|
|
390
|
+
|---------|------|------|
|
|
391
|
+
| 1 (낮음) | 앞쪽 미들웨어 | 먼저 설정된 헤더 |
|
|
392
|
+
| 2 | 뒤쪽 미들웨어 | 앞쪽 미들웨어를 덮어씀 |
|
|
393
|
+
| 3 (높음) | 호출 시점 | 모든 미들웨어 헤더를 덮어씀 |
|
|
394
|
+
|
|
395
|
+
</custom_headers>
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
<custom_fetch>
|
|
400
|
+
|
|
401
|
+
## 커스텀 fetch 구현
|
|
402
|
+
|
|
403
|
+
고급 사용 사례에서 커스텀 `fetch` 구현을 제공하여 서버 함수 요청 방식을 제어할 수 있습니다:
|
|
404
|
+
|
|
405
|
+
- 요청 인터셉터 또는 재시도 로직
|
|
406
|
+
- 커스텀 HTTP 클라이언트 사용
|
|
407
|
+
- 테스트 및 모킹
|
|
408
|
+
- 텔레메트리/모니터링 추가
|
|
409
|
+
|
|
410
|
+
### 클라이언트 미들웨어를 통한 커스텀 fetch
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
import { createMiddleware } from '@tanstack/react-start'
|
|
414
|
+
import type { CustomFetch } from '@tanstack/react-start'
|
|
415
|
+
|
|
416
|
+
const customFetchMiddleware = createMiddleware({ type: 'function' }).client(
|
|
417
|
+
async ({ next }) => {
|
|
418
|
+
const customFetch: CustomFetch = async (url, init) => {
|
|
419
|
+
console.log('Request starting:', url)
|
|
420
|
+
const start = Date.now()
|
|
421
|
+
|
|
422
|
+
const response = await fetch(url, init)
|
|
423
|
+
|
|
424
|
+
console.log('Request completed in', Date.now() - start, 'ms')
|
|
425
|
+
return response
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return next({ fetch: customFetch })
|
|
429
|
+
},
|
|
430
|
+
)
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### 호출 시점에서 커스텀 fetch
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
import type { CustomFetch } from '@tanstack/react-start'
|
|
437
|
+
|
|
438
|
+
const myFetch: CustomFetch = async (url, init) => {
|
|
439
|
+
// 커스텀 로직 추가
|
|
440
|
+
return fetch(url, init)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
await myServerFn({
|
|
444
|
+
data: { name: 'John' },
|
|
445
|
+
fetch: myFetch,
|
|
446
|
+
})
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### fetch 우선순위
|
|
450
|
+
|
|
451
|
+
| 우선순위 | 출처 | 설명 |
|
|
452
|
+
|---------|------|------|
|
|
453
|
+
| 1 (최고) | 호출 시점 | `serverFn({ fetch: customFetch })` |
|
|
454
|
+
| 2 | 뒤쪽 미들웨어 | 체인 마지막 미들웨어의 fetch |
|
|
455
|
+
| 3 | 앞쪽 미들웨어 | 체인 첫 미들웨어의 fetch |
|
|
456
|
+
| 4 | createStart | `createStart({ serverFns: { fetch: customFetch } })` |
|
|
457
|
+
| 5 (최저) | 기본값 | 전역 `fetch` 함수 |
|
|
458
|
+
|
|
459
|
+
**핵심 원칙:** 호출 시점이 항상 우선입니다. 특정 호출에서 미들웨어 동작을 재정의할 수 있습니다.
|
|
460
|
+
|
|
461
|
+
### 전역 fetch (createStart)
|
|
462
|
+
|
|
463
|
+
모든 서버 함수에 대한 기본 커스텀 fetch 설정:
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
// src/start.ts
|
|
467
|
+
import { createStart } from '@tanstack/react-start'
|
|
468
|
+
import type { CustomFetch } from '@tanstack/react-start'
|
|
469
|
+
|
|
470
|
+
const globalFetch: CustomFetch = async (url, init) => {
|
|
471
|
+
console.log('Global fetch:', url)
|
|
472
|
+
return fetch(url, init)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export const startInstance = createStart(() => {
|
|
476
|
+
return {
|
|
477
|
+
serverFns: {
|
|
478
|
+
fetch: globalFetch,
|
|
479
|
+
},
|
|
480
|
+
}
|
|
481
|
+
})
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
> **참고:** 커스텀 fetch는 클라이언트 사이드에서만 적용됩니다. SSR 중에는 서버 함수가 fetch 없이 직접 호출됩니다.
|
|
485
|
+
|
|
486
|
+
</custom_fetch>
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
<auth_middleware>
|
|
491
|
+
|
|
21
492
|
## 인증 미들웨어
|
|
22
493
|
|
|
494
|
+
### 세션 기반 인증
|
|
495
|
+
|
|
23
496
|
```typescript
|
|
497
|
+
import { createMiddleware } from '@tanstack/react-start'
|
|
498
|
+
import { redirect } from '@tanstack/react-router'
|
|
499
|
+
|
|
24
500
|
const authMiddleware = createMiddleware({ type: 'function' })
|
|
25
|
-
.server(async ({ next }) => {
|
|
26
|
-
const session = await getSession()
|
|
27
|
-
|
|
501
|
+
.server(async ({ next, request }) => {
|
|
502
|
+
const session = await getSession(request)
|
|
503
|
+
|
|
504
|
+
if (!session?.user) {
|
|
505
|
+
throw redirect({ to: '/login' })
|
|
506
|
+
}
|
|
507
|
+
|
|
28
508
|
return next({ context: { user: session.user } })
|
|
29
509
|
})
|
|
30
510
|
|
|
31
511
|
// 사용
|
|
32
|
-
export const
|
|
512
|
+
export const getMyProfile = createServerFn({ method: 'GET' })
|
|
33
513
|
.middleware([authMiddleware])
|
|
34
|
-
.handler(async ({ context }) =>
|
|
514
|
+
.handler(async ({ context }): Promise<User> => {
|
|
515
|
+
return prisma.user.findUnique({
|
|
516
|
+
where: { id: context.user.id },
|
|
517
|
+
})
|
|
518
|
+
})
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### 선택적 인증
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
const optionalAuthMiddleware = createMiddleware({ type: 'function' })
|
|
525
|
+
.server(async ({ next, request }) => {
|
|
526
|
+
const session = await getSession(request)
|
|
527
|
+
return next({ context: { user: session?.user ?? null } })
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
export const getPublicPosts = createServerFn({ method: 'GET' })
|
|
531
|
+
.middleware([optionalAuthMiddleware])
|
|
532
|
+
.handler(async ({ context }): Promise<Post[]> => {
|
|
533
|
+
return prisma.post.findMany({
|
|
534
|
+
where: {
|
|
535
|
+
published: true,
|
|
536
|
+
...(context.user && { authorId: context.user.id }),
|
|
537
|
+
},
|
|
538
|
+
})
|
|
539
|
+
})
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
</auth_middleware>
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
<role_middleware>
|
|
547
|
+
|
|
548
|
+
## 역할 기반 미들웨어
|
|
549
|
+
|
|
550
|
+
### 고정 역할 (Admin)
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
const adminMiddleware = createMiddleware({ type: 'function' })
|
|
554
|
+
.server(async ({ next, request }) => {
|
|
555
|
+
const session = await getSession(request)
|
|
556
|
+
|
|
557
|
+
if (!session?.user || session.user.role !== 'ADMIN') {
|
|
558
|
+
throw new Error('Forbidden: Admin access only')
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return next({ context: { user: session.user } })
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
export const deleteAnyUser = createServerFn({ method: 'DELETE' })
|
|
565
|
+
.middleware([adminMiddleware])
|
|
566
|
+
.inputValidator(z.object({ id: z.string() }))
|
|
567
|
+
.handler(async ({ data }): Promise<{ success: true }> => {
|
|
568
|
+
await prisma.user.delete({ where: { id: data.id } })
|
|
569
|
+
return { success: true }
|
|
570
|
+
})
|
|
35
571
|
```
|
|
36
572
|
|
|
37
|
-
|
|
573
|
+
### 동적 역할
|
|
38
574
|
|
|
39
575
|
```typescript
|
|
576
|
+
const roleMiddleware = (allowedRoles: string[]) =>
|
|
577
|
+
createMiddleware({ type: 'function' })
|
|
578
|
+
.server(async ({ next, request }) => {
|
|
579
|
+
const session = await getSession(request)
|
|
580
|
+
|
|
581
|
+
if (!session?.user) {
|
|
582
|
+
throw redirect({ to: '/login' })
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (!allowedRoles.includes(session.user.role)) {
|
|
586
|
+
throw new Error(`Forbidden: Required roles: ${allowedRoles.join(', ')}`)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return next({ context: { user: session.user } })
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
export const approvePost = createServerFn({ method: 'POST' })
|
|
593
|
+
.middleware([roleMiddleware(['ADMIN', 'MODERATOR'])])
|
|
594
|
+
.inputValidator(z.object({ id: z.string() }))
|
|
595
|
+
.handler(async ({ data }): Promise<Post> => {
|
|
596
|
+
return prisma.post.update({
|
|
597
|
+
where: { id: data.id },
|
|
598
|
+
data: { published: true },
|
|
599
|
+
})
|
|
600
|
+
})
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
</role_middleware>
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
607
|
+
<validation_middleware>
|
|
608
|
+
|
|
609
|
+
## 검증 미들웨어
|
|
610
|
+
|
|
611
|
+
### Zod 검증 미들웨어
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
import { zodValidator } from '@tanstack/zod-adapter'
|
|
615
|
+
import { z } from 'zod'
|
|
616
|
+
|
|
617
|
+
const mySchema = z.object({
|
|
618
|
+
workspaceId: z.string(),
|
|
619
|
+
})
|
|
620
|
+
|
|
40
621
|
const workspaceMiddleware = createMiddleware({ type: 'function' })
|
|
41
|
-
.inputValidator(zodValidator(
|
|
42
|
-
.server(({ next, data }) =>
|
|
622
|
+
.inputValidator(zodValidator(mySchema))
|
|
623
|
+
.server(async ({ next, data }) => {
|
|
624
|
+
const workspace = await prisma.workspace.findUnique({
|
|
625
|
+
where: { id: data.workspaceId },
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
if (!workspace) {
|
|
629
|
+
throw new Error('Workspace not found')
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return next({ context: { workspace } })
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
export const getWorkspaceData = createServerFn({ method: 'GET' })
|
|
636
|
+
.middleware([authMiddleware, workspaceMiddleware])
|
|
637
|
+
.handler(async ({ context }): Promise<WorkspaceData> => {
|
|
638
|
+
return {
|
|
639
|
+
workspace: context.workspace,
|
|
640
|
+
user: context.user,
|
|
641
|
+
projects: await prisma.project.findMany({
|
|
642
|
+
where: { workspaceId: context.workspace.id },
|
|
643
|
+
}),
|
|
644
|
+
}
|
|
645
|
+
})
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### 비즈니스 로직 검증
|
|
649
|
+
|
|
650
|
+
```typescript
|
|
651
|
+
const projectAccessMiddleware = createMiddleware({ type: 'function' })
|
|
652
|
+
.inputValidator(z.object({ projectId: z.string() }))
|
|
653
|
+
.server(async ({ next, data, request }) => {
|
|
654
|
+
const session = await getSession(request)
|
|
655
|
+
if (!session?.user) throw redirect({ to: '/login' })
|
|
656
|
+
|
|
657
|
+
const project = await prisma.project.findUnique({
|
|
658
|
+
where: { id: data.projectId },
|
|
659
|
+
include: { members: true },
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
if (!project) {
|
|
663
|
+
throw new Error('Project not found')
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const isMember = project.members.some((m) => m.userId === session.user.id)
|
|
667
|
+
const isOwner = project.ownerId === session.user.id
|
|
668
|
+
|
|
669
|
+
if (!isMember && !isOwner) {
|
|
670
|
+
throw new Error('Access denied')
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return next({
|
|
674
|
+
context: {
|
|
675
|
+
user: session.user,
|
|
676
|
+
project,
|
|
677
|
+
isOwner,
|
|
678
|
+
},
|
|
679
|
+
})
|
|
680
|
+
})
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
</validation_middleware>
|
|
684
|
+
|
|
685
|
+
---
|
|
686
|
+
|
|
687
|
+
<middleware_chaining>
|
|
688
|
+
|
|
689
|
+
## 미들웨어 체이닝
|
|
690
|
+
|
|
691
|
+
### 순서가 중요
|
|
692
|
+
|
|
693
|
+
```typescript
|
|
694
|
+
export const protectedWorkspaceFn = createServerFn({ method: 'POST' })
|
|
695
|
+
.middleware([
|
|
696
|
+
loggingMiddleware, // 1. 로깅 (모든 요청)
|
|
697
|
+
authMiddleware, // 2. 인증 (세션 검증)
|
|
698
|
+
roleMiddleware(['ADMIN']), // 3. 권한 (역할 확인)
|
|
699
|
+
workspaceMiddleware, // 4. 비즈니스 로직 (workspace 검증)
|
|
700
|
+
])
|
|
701
|
+
.inputValidator(taskSchema)
|
|
702
|
+
.handler(async ({ data, context }): Promise<Task> => {
|
|
703
|
+
return prisma.task.create({
|
|
704
|
+
data: {
|
|
705
|
+
...data,
|
|
706
|
+
workspaceId: context.workspace.id,
|
|
707
|
+
createdById: context.user.id,
|
|
708
|
+
},
|
|
709
|
+
})
|
|
710
|
+
})
|
|
43
711
|
```
|
|
44
712
|
|
|
45
|
-
|
|
713
|
+
### 미들웨어 실행 순서
|
|
714
|
+
|
|
715
|
+
미들웨어는 의존성 우선으로 실행됩니다. 전역 미들웨어가 먼저, 그 다음 서버 함수 미들웨어가 실행됩니다.
|
|
716
|
+
|
|
717
|
+
```typescript
|
|
718
|
+
// 실행 순서: globalMiddleware1 -> globalMiddleware2 -> a -> b -> c -> d -> fn
|
|
719
|
+
const a = createMiddleware({ type: 'function' }).server(async ({ next }) => {
|
|
720
|
+
console.log('a')
|
|
721
|
+
return next()
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
const b = createMiddleware({ type: 'function' })
|
|
725
|
+
.middleware([a])
|
|
726
|
+
.server(async ({ next }) => {
|
|
727
|
+
console.log('b')
|
|
728
|
+
return next()
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
const c = createMiddleware({ type: 'function' }).server(async ({ next }) => {
|
|
732
|
+
console.log('c')
|
|
733
|
+
return next()
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
const d = createMiddleware({ type: 'function' })
|
|
737
|
+
.middleware([b, c])
|
|
738
|
+
.server(async ({ next }) => {
|
|
739
|
+
console.log('d')
|
|
740
|
+
return next()
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
const fn = createServerFn()
|
|
744
|
+
.middleware([d])
|
|
745
|
+
.handler(async () => {
|
|
746
|
+
console.log('fn')
|
|
747
|
+
})
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
</middleware_chaining>
|
|
751
|
+
|
|
752
|
+
---
|
|
753
|
+
|
|
754
|
+
<global_middleware>
|
|
755
|
+
|
|
756
|
+
## 전역 미들웨어
|
|
757
|
+
|
|
758
|
+
> **참고:** `src/start.ts` 파일은 기본 TanStack Start 템플릿에 포함되어 있지 않습니다. 전역 미들웨어를 구성하려면 이 파일을 직접 생성해야 합니다.
|
|
759
|
+
|
|
760
|
+
### 전역 Request Middleware
|
|
761
|
+
|
|
762
|
+
모든 요청 (Server Routes, SSR, Server Functions)에 적용:
|
|
46
763
|
|
|
47
764
|
```typescript
|
|
48
765
|
// src/start.ts
|
|
766
|
+
import { createStart, createMiddleware } from '@tanstack/react-start'
|
|
767
|
+
|
|
768
|
+
const myGlobalMiddleware = createMiddleware().server(({ next }) => {
|
|
769
|
+
console.log('모든 요청에 실행')
|
|
770
|
+
return next()
|
|
771
|
+
})
|
|
772
|
+
|
|
49
773
|
export const startInstance = createStart(() => ({
|
|
50
|
-
requestMiddleware: [
|
|
51
|
-
functionMiddleware: [loggingMiddleware], // 모든 Server Function
|
|
774
|
+
requestMiddleware: [myGlobalMiddleware],
|
|
52
775
|
}))
|
|
53
776
|
```
|
|
54
777
|
|
|
55
|
-
|
|
778
|
+
### 전역 Server Function Middleware
|
|
779
|
+
|
|
780
|
+
모든 Server Function에 적용:
|
|
781
|
+
|
|
782
|
+
```typescript
|
|
783
|
+
// src/start.ts
|
|
784
|
+
import { createStart, createMiddleware } from '@tanstack/react-start'
|
|
785
|
+
|
|
786
|
+
const loggingMiddleware = createMiddleware({ type: 'function' })
|
|
787
|
+
.server(({ next, request }) => {
|
|
788
|
+
console.log(`[${new Date().toISOString()}] ${request.method}`)
|
|
789
|
+
return next()
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
export const startInstance = createStart(() => ({
|
|
793
|
+
functionMiddleware: [loggingMiddleware],
|
|
794
|
+
}))
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
### 전역 에러 처리
|
|
798
|
+
|
|
799
|
+
```typescript
|
|
800
|
+
const errorHandlingMiddleware = createMiddleware({ type: 'function' })
|
|
801
|
+
.server(async ({ next }) => {
|
|
802
|
+
try {
|
|
803
|
+
return await next()
|
|
804
|
+
} catch (error) {
|
|
805
|
+
if (error instanceof Error) {
|
|
806
|
+
console.error('Error:', error.message)
|
|
807
|
+
}
|
|
808
|
+
throw error
|
|
809
|
+
}
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
export const startInstance = createStart(() => ({
|
|
813
|
+
functionMiddleware: [errorHandlingMiddleware],
|
|
814
|
+
}))
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
</global_middleware>
|
|
818
|
+
|
|
819
|
+
---
|
|
820
|
+
|
|
821
|
+
<route_middleware>
|
|
822
|
+
|
|
823
|
+
## 라우트 레벨 미들웨어
|
|
824
|
+
|
|
825
|
+
### 모든 Server Route 메서드에 적용
|
|
826
|
+
|
|
827
|
+
```typescript
|
|
828
|
+
export const Route = createFileRoute('/api/protected')({
|
|
829
|
+
server: {
|
|
830
|
+
middleware: [authMiddleware, adminMiddleware],
|
|
831
|
+
handlers: {
|
|
832
|
+
GET: async ({ request }): Promise<Response> => {
|
|
833
|
+
return new Response('Admin data')
|
|
834
|
+
},
|
|
835
|
+
POST: async ({ request }): Promise<Response> => {
|
|
836
|
+
const body = await request.json()
|
|
837
|
+
return Response.json({ created: true })
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
},
|
|
841
|
+
})
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
### 특정 메서드에만 미들웨어 적용
|
|
845
|
+
|
|
846
|
+
`createHandlers` 유틸리티를 사용하여 개별 메서드에 미들웨어를 적용합니다.
|
|
847
|
+
|
|
848
|
+
```typescript
|
|
849
|
+
export const Route = createFileRoute('/foo')({
|
|
850
|
+
server: {
|
|
851
|
+
handlers: ({ createHandlers }) =>
|
|
852
|
+
createHandlers({
|
|
853
|
+
GET: {
|
|
854
|
+
middleware: [loggingMiddleware],
|
|
855
|
+
handler: () => {
|
|
856
|
+
//...
|
|
857
|
+
},
|
|
858
|
+
},
|
|
859
|
+
}),
|
|
860
|
+
},
|
|
861
|
+
})
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
### beforeLoad와 미들웨어
|
|
56
865
|
|
|
57
866
|
```typescript
|
|
58
|
-
export const Route = createFileRoute('/
|
|
867
|
+
export const Route = createFileRoute('/dashboard')({
|
|
868
|
+
// 라우트 진입 전 검증
|
|
869
|
+
beforeLoad: async () => {
|
|
870
|
+
const user = await getCurrentUser()
|
|
871
|
+
if (!user) {
|
|
872
|
+
throw redirect({ to: '/login' })
|
|
873
|
+
}
|
|
874
|
+
return { user }
|
|
875
|
+
},
|
|
876
|
+
|
|
59
877
|
server: {
|
|
60
|
-
middleware: [
|
|
61
|
-
handlers: {
|
|
878
|
+
middleware: [loggingMiddleware],
|
|
879
|
+
handlers: {
|
|
880
|
+
GET: async (): Promise<Response> => {
|
|
881
|
+
return new Response('Dashboard data')
|
|
882
|
+
},
|
|
883
|
+
},
|
|
62
884
|
},
|
|
885
|
+
|
|
886
|
+
component: DashboardPage,
|
|
63
887
|
})
|
|
64
888
|
```
|
|
889
|
+
|
|
890
|
+
</route_middleware>
|
|
891
|
+
|
|
892
|
+
---
|
|
893
|
+
|
|
894
|
+
<request_response>
|
|
895
|
+
|
|
896
|
+
## 요청/응답 수정
|
|
897
|
+
|
|
898
|
+
### 서버 사이드: 요청/응답 유틸리티
|
|
899
|
+
|
|
900
|
+
`.server()` 메서드 내에서 Server Function Context 유틸리티를 사용할 수 있습니다:
|
|
901
|
+
|
|
902
|
+
```typescript
|
|
903
|
+
import {
|
|
904
|
+
getRequest,
|
|
905
|
+
getRequestHeader,
|
|
906
|
+
setResponseHeaders,
|
|
907
|
+
setResponseStatus,
|
|
908
|
+
} from '@tanstack/react-start/server'
|
|
909
|
+
|
|
910
|
+
const cachingMiddleware = createMiddleware({ type: 'function' })
|
|
911
|
+
.server(async ({ next }) => {
|
|
912
|
+
const request = getRequest()
|
|
913
|
+
const authHeader = getRequestHeader('Authorization')
|
|
914
|
+
|
|
915
|
+
setResponseHeaders(
|
|
916
|
+
new Headers({
|
|
917
|
+
'Cache-Control': 'public, max-age=300',
|
|
918
|
+
}),
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
setResponseStatus(200)
|
|
922
|
+
|
|
923
|
+
return next()
|
|
924
|
+
})
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
**사용 가능한 유틸리티:**
|
|
928
|
+
- `getRequest()` - 전체 Request 객체 접근
|
|
929
|
+
- `getRequestHeader(name)` - 특정 요청 헤더 읽기
|
|
930
|
+
- `setResponseHeader(name, value)` - 단일 응답 헤더 설정
|
|
931
|
+
- `setResponseHeaders(headers)` - Headers 객체로 다중 응답 헤더 설정
|
|
932
|
+
- `setResponseStatus(code)` - HTTP 상태 코드 설정
|
|
933
|
+
|
|
934
|
+
</request_response>
|
|
935
|
+
|
|
936
|
+
---
|
|
937
|
+
|
|
938
|
+
<error_handling>
|
|
939
|
+
|
|
940
|
+
## 미들웨어 에러 처리
|
|
941
|
+
|
|
942
|
+
### 리다이렉트
|
|
943
|
+
|
|
944
|
+
```typescript
|
|
945
|
+
const authMiddleware = createMiddleware({ type: 'function' })
|
|
946
|
+
.server(async ({ next, request }) => {
|
|
947
|
+
const session = await getSession(request)
|
|
948
|
+
|
|
949
|
+
if (!session?.user) {
|
|
950
|
+
throw redirect({
|
|
951
|
+
to: '/login',
|
|
952
|
+
search: { returnUrl: '/dashboard' },
|
|
953
|
+
})
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return next({ context: { user: session.user } })
|
|
957
|
+
})
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
### 커스텀 에러
|
|
961
|
+
|
|
962
|
+
```typescript
|
|
963
|
+
class ForbiddenError extends Error {
|
|
964
|
+
constructor(message: string) {
|
|
965
|
+
super(message)
|
|
966
|
+
this.name = 'ForbiddenError'
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const permissionMiddleware = createMiddleware({ type: 'function' })
|
|
971
|
+
.server(async ({ next, request }) => {
|
|
972
|
+
const session = await getSession(request)
|
|
973
|
+
|
|
974
|
+
if (!session?.user) {
|
|
975
|
+
throw new ForbiddenError('Unauthorized')
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return next({ context: { user: session.user } })
|
|
979
|
+
})
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
### 에러 변환
|
|
983
|
+
|
|
984
|
+
```typescript
|
|
985
|
+
const errorTransformMiddleware = createMiddleware({ type: 'function' })
|
|
986
|
+
.server(async ({ next }) => {
|
|
987
|
+
try {
|
|
988
|
+
return await next()
|
|
989
|
+
} catch (error) {
|
|
990
|
+
if (error instanceof ForbiddenError) {
|
|
991
|
+
throw new Error(`Access Denied: ${error.message}`)
|
|
992
|
+
}
|
|
993
|
+
throw error
|
|
994
|
+
}
|
|
995
|
+
})
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
</error_handling>
|
|
999
|
+
|
|
1000
|
+
---
|
|
1001
|
+
|
|
1002
|
+
<context_typing>
|
|
1003
|
+
|
|
1004
|
+
## Context 타입 정의
|
|
1005
|
+
|
|
1006
|
+
```typescript
|
|
1007
|
+
// lib/middleware-context.ts
|
|
1008
|
+
|
|
1009
|
+
export interface MiddlewareContext {
|
|
1010
|
+
user?: {
|
|
1011
|
+
id: string
|
|
1012
|
+
email: string
|
|
1013
|
+
role: 'USER' | 'ADMIN' | 'MODERATOR'
|
|
1014
|
+
}
|
|
1015
|
+
workspace?: {
|
|
1016
|
+
id: string
|
|
1017
|
+
name: string
|
|
1018
|
+
}
|
|
1019
|
+
permissions?: string[]
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// middleware/auth.ts
|
|
1023
|
+
const authMiddleware = createMiddleware({ type: 'function' })
|
|
1024
|
+
.server(async ({ next, request }) => {
|
|
1025
|
+
const session = await getSession(request)
|
|
1026
|
+
|
|
1027
|
+
return next({
|
|
1028
|
+
context: {
|
|
1029
|
+
user: session?.user ? {
|
|
1030
|
+
id: session.user.id,
|
|
1031
|
+
email: session.user.email,
|
|
1032
|
+
role: session.user.role as 'USER' | 'ADMIN',
|
|
1033
|
+
} : undefined,
|
|
1034
|
+
},
|
|
1035
|
+
})
|
|
1036
|
+
})
|
|
1037
|
+
|
|
1038
|
+
// Server Function
|
|
1039
|
+
export const fn = createServerFn({ method: 'GET' })
|
|
1040
|
+
.middleware([authMiddleware])
|
|
1041
|
+
.handler(async ({ context }): Promise<unknown> => {
|
|
1042
|
+
// context.user는 타입 안전
|
|
1043
|
+
return context.user?.id
|
|
1044
|
+
})
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
</context_typing>
|
|
1048
|
+
|
|
1049
|
+
---
|
|
1050
|
+
|
|
1051
|
+
<environment_tree_shaking>
|
|
1052
|
+
|
|
1053
|
+
## 환경별 트리 셰이킹
|
|
1054
|
+
|
|
1055
|
+
미들웨어 기능은 각 번들의 환경에 따라 트리 셰이킹됩니다:
|
|
1056
|
+
|
|
1057
|
+
| 환경 | 동작 |
|
|
1058
|
+
|------|------|
|
|
1059
|
+
| **서버** | 모든 코드 포함 (트리 셰이킹 없음) |
|
|
1060
|
+
| **클라이언트** | `.server()` 코드 제거, `data` 검증 코드도 제거 |
|
|
1061
|
+
|
|
1062
|
+
</environment_tree_shaking>
|
|
1063
|
+
|
|
1064
|
+
---
|
|
1065
|
+
|
|
1066
|
+
<best_practices>
|
|
1067
|
+
|
|
1068
|
+
## 모범 사례
|
|
1069
|
+
|
|
1070
|
+
| 원칙 | 설명 |
|
|
1071
|
+
|------|------|
|
|
1072
|
+
| **단일 책임** | 각 미들웨어는 하나의 기능만 담당 |
|
|
1073
|
+
| **재사용성** | 공통 미들웨어는 `@/middleware/`에 |
|
|
1074
|
+
| **순서 명시** | 미들웨어 순서를 주석으로 표기 |
|
|
1075
|
+
| **에러 처리** | 미들웨어에서 throw로 명확하게 에러 처리 |
|
|
1076
|
+
| **Context 명확성** | context 구조를 타입으로 정의 |
|
|
1077
|
+
| **이름** | 미들웨어는 `*Middleware` 이름으로 통일 |
|
|
1078
|
+
| **sendContext 검증** | 클라이언트에서 보낸 동적 데이터는 서버에서 반드시 검증 |
|
|
1079
|
+
| **메서드 순서** | TypeScript 강제 순서 준수: middleware -> inputValidator -> client -> server |
|
|
1080
|
+
|
|
1081
|
+
### 나쁜 예
|
|
1082
|
+
|
|
1083
|
+
```typescript
|
|
1084
|
+
// 여러 책임을 가진 미들웨어
|
|
1085
|
+
const megaMiddleware = createMiddleware({ type: 'function' })
|
|
1086
|
+
.server(async ({ next }) => {
|
|
1087
|
+
console.log('Request') // 로깅
|
|
1088
|
+
const session = await getSession() // 인증
|
|
1089
|
+
if (session?.user.role !== 'ADMIN') throw new Error('Forbidden') // 권한
|
|
1090
|
+
return next()
|
|
1091
|
+
})
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
### 좋은 예
|
|
1095
|
+
|
|
1096
|
+
```typescript
|
|
1097
|
+
// 책임 분리
|
|
1098
|
+
const loggingMiddleware = createMiddleware({ type: 'function' })
|
|
1099
|
+
.server(({ next }) => {
|
|
1100
|
+
console.log('Request')
|
|
1101
|
+
return next()
|
|
1102
|
+
})
|
|
1103
|
+
|
|
1104
|
+
const authMiddleware = createMiddleware({ type: 'function' })
|
|
1105
|
+
.server(async ({ next }) => {
|
|
1106
|
+
const session = await getSession()
|
|
1107
|
+
if (!session) throw redirect({ to: '/login' })
|
|
1108
|
+
return next({ context: { user: session.user } })
|
|
1109
|
+
})
|
|
1110
|
+
|
|
1111
|
+
const adminMiddleware = createMiddleware({ type: 'function' })
|
|
1112
|
+
.server(async ({ next, context }) => {
|
|
1113
|
+
if (context.user.role !== 'ADMIN') {
|
|
1114
|
+
throw new Error('Forbidden')
|
|
1115
|
+
}
|
|
1116
|
+
return next()
|
|
1117
|
+
})
|
|
1118
|
+
|
|
1119
|
+
// 조합 사용
|
|
1120
|
+
export const adminFn = createServerFn({ method: 'POST' })
|
|
1121
|
+
.middleware([loggingMiddleware, authMiddleware, adminMiddleware])
|
|
1122
|
+
.handler(async () => ({ success: true }))
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1125
|
+
</best_practices>
|
|
1126
|
+
|
|
1127
|
+
---
|
|
1128
|
+
|
|
1129
|
+
<version_info>
|
|
1130
|
+
|
|
1131
|
+
**Version:** TanStack Start/Router v1.159.4
|
|
1132
|
+
|
|
1133
|
+
**Key Features:**
|
|
1134
|
+
- Type-safe context passing (sendContext 포함)
|
|
1135
|
+
- Client-Server 양방향 컨텍스트 전송
|
|
1136
|
+
- 커스텀 헤더 설정 및 머징
|
|
1137
|
+
- 커스텀 fetch 구현 (미들웨어/호출 시점/전역)
|
|
1138
|
+
- Request/Server Function 두 가지 미들웨어 타입
|
|
1139
|
+
- 환경별 트리 셰이킹
|
|
1140
|
+
- 전역 및 라우트 레벨 미들웨어 지원
|
|
1141
|
+
- 특정 Server Route 메서드별 미들웨어 적용
|
|
1142
|
+
|
|
1143
|
+
</version_info>
|