@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,75 +1,993 @@
|
|
|
1
1
|
# TanStack Start - Server Functions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> 타입 안전한 백엔드 API
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- 자동 캐싱, 중복 요청 제거, 로딩/에러 상태 관리, invalidateQueries 동기화
|
|
7
|
+
<overview>
|
|
9
8
|
|
|
10
|
-
##
|
|
9
|
+
## Server Function이란?
|
|
10
|
+
|
|
11
|
+
Server Function은 서버에서만 실행되는 함수로, 클라이언트에서 타입 안전하게 호출할 수 있습니다.
|
|
12
|
+
|
|
13
|
+
| 특징 | 설명 |
|
|
14
|
+
|------|------|
|
|
15
|
+
| **타입 안전성** | 입출력 타입이 TypeScript로 자동 추론됨 |
|
|
16
|
+
| **직렬화** | JSON 자동 직렬화/역직렬화 |
|
|
17
|
+
| **검증** | Zod로 입력값 검증 (.inputValidator()) |
|
|
18
|
+
| **미들웨어** | 인증, 로깅, 권한 체크 등 (.middleware()) |
|
|
19
|
+
| **HTTP 메서드** | GET, POST, PUT, PATCH, DELETE 지원 |
|
|
20
|
+
| **클라이언트 호출** | TanStack Query (useQuery/useMutation) 필수 |
|
|
21
|
+
|
|
22
|
+
</overview>
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<http_methods>
|
|
27
|
+
|
|
28
|
+
## HTTP 메서드별 사용 패턴
|
|
29
|
+
|
|
30
|
+
| 메서드 | 사용 | inputValidator | middleware |
|
|
31
|
+
|--------|------|---------------|-----------|
|
|
32
|
+
| **GET** | 데이터 조회 | 선택 | 인증 시 필수 |
|
|
33
|
+
| **POST** | 데이터 생성 | 필수 | 인증 시 필수 |
|
|
34
|
+
| **PUT** | 전체 수정 | 필수 | 인증 시 필수 |
|
|
35
|
+
| **PATCH** | 부분 수정 | 필수 | 인증 시 필수 |
|
|
36
|
+
| **DELETE** | 데이터 삭제 | 선택 | 인증 시 필수 |
|
|
37
|
+
|
|
38
|
+
</http_methods>
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<get_method>
|
|
43
|
+
|
|
44
|
+
## GET: 데이터 조회
|
|
45
|
+
|
|
46
|
+
### 기본 GET
|
|
11
47
|
|
|
12
48
|
```typescript
|
|
13
|
-
//
|
|
49
|
+
// ✅ 간단한 조회
|
|
14
50
|
export const getUsers = createServerFn({ method: 'GET' })
|
|
15
|
-
.handler(async () =>
|
|
51
|
+
.handler(async (): Promise<User[]> => {
|
|
52
|
+
return prisma.user.findMany()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// ✅ GET + 쿼리 파라미터 (선택적 검증)
|
|
56
|
+
export const getUserById = createServerFn({ method: 'GET' })
|
|
57
|
+
.inputValidator(z.object({
|
|
58
|
+
id: z.string().uuid(),
|
|
59
|
+
}))
|
|
60
|
+
.handler(async ({ data }): Promise<User | null> => {
|
|
61
|
+
return prisma.user.findUnique({
|
|
62
|
+
where: { id: data.id },
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// ✅ GET + 필터링
|
|
67
|
+
const filterSchema = z.object({
|
|
68
|
+
status: z.enum(['active', 'inactive']).optional(),
|
|
69
|
+
limit: z.number().int().min(1).max(100).default(10),
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
export const searchUsers = createServerFn({ method: 'GET' })
|
|
73
|
+
.inputValidator(filterSchema)
|
|
74
|
+
.handler(async ({ data }): Promise<User[]> => {
|
|
75
|
+
return prisma.user.findMany({
|
|
76
|
+
where: {
|
|
77
|
+
...(data.status && { status: data.status }),
|
|
78
|
+
},
|
|
79
|
+
take: data.limit,
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
```
|
|
16
83
|
|
|
17
|
-
|
|
84
|
+
### GET + 인증
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// ✅ 현재 사용자 조회
|
|
88
|
+
export const getMyProfile = createServerFn({ method: 'GET' })
|
|
89
|
+
.middleware([authMiddleware])
|
|
90
|
+
.handler(async ({ context }): Promise<User> => {
|
|
91
|
+
return prisma.user.findUnique({
|
|
92
|
+
where: { id: context.user.id },
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// ✅ 사용자 전용 데이터
|
|
97
|
+
export const getMyPosts = createServerFn({ method: 'GET' })
|
|
98
|
+
.middleware([authMiddleware])
|
|
99
|
+
.handler(async ({ context }): Promise<Post[]> => {
|
|
100
|
+
return prisma.post.findMany({
|
|
101
|
+
where: { authorId: context.user.id },
|
|
102
|
+
orderBy: { createdAt: 'desc' },
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
</get_method>
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
<post_method>
|
|
112
|
+
|
|
113
|
+
## POST: 데이터 생성
|
|
114
|
+
|
|
115
|
+
### POST + inputValidator (필수)
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// ✅ inputValidator 필수
|
|
18
119
|
const createUserSchema = z.object({
|
|
19
|
-
email: z.email(),
|
|
20
|
-
name: z.string().min(1).max(100),
|
|
120
|
+
email: z.string().email(),
|
|
121
|
+
name: z.string().min(1).max(100).trim(),
|
|
122
|
+
age: z.number().int().min(0).max(150).optional(),
|
|
21
123
|
})
|
|
22
124
|
|
|
23
125
|
export const createUser = createServerFn({ method: 'POST' })
|
|
24
126
|
.inputValidator(createUserSchema)
|
|
25
|
-
.handler(async ({ data }) =>
|
|
127
|
+
.handler(async ({ data }): Promise<User> => {
|
|
128
|
+
// data는 자동으로 검증됨 (타입 안전)
|
|
129
|
+
return prisma.user.create({
|
|
130
|
+
data,
|
|
131
|
+
})
|
|
132
|
+
})
|
|
26
133
|
```
|
|
27
134
|
|
|
28
|
-
|
|
135
|
+
### POST + inputValidator + 인증
|
|
29
136
|
|
|
30
|
-
```
|
|
31
|
-
// ✅
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
137
|
+
```typescript
|
|
138
|
+
// ✅ POST + 검증 + 인증
|
|
139
|
+
const createPostSchema = z.object({
|
|
140
|
+
title: z.string().min(1).max(200),
|
|
141
|
+
content: z.string().min(1).max(10000),
|
|
142
|
+
tags: z.array(z.string()).max(5).default([]),
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
export const createPost = createServerFn({ method: 'POST' })
|
|
146
|
+
.middleware([authMiddleware])
|
|
147
|
+
.inputValidator(createPostSchema)
|
|
148
|
+
.handler(async ({ data, context }): Promise<Post> => {
|
|
149
|
+
return prisma.post.create({
|
|
150
|
+
data: {
|
|
151
|
+
...data,
|
|
152
|
+
authorId: context.user.id,
|
|
153
|
+
published: false,
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### POST + 부수 효과
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// ✅ 중복 체크 + 생성
|
|
163
|
+
const registerSchema = z.object({
|
|
164
|
+
email: z.string().email(),
|
|
165
|
+
password: z.string().min(8),
|
|
166
|
+
name: z.string().min(1),
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
export const registerUser = createServerFn({ method: 'POST' })
|
|
170
|
+
.inputValidator(registerSchema)
|
|
171
|
+
.handler(async ({ data }): Promise<{ user: User }> => {
|
|
172
|
+
// 중복 체크 (내부 헬퍼)
|
|
173
|
+
const exists = await prisma.user.findUnique({
|
|
174
|
+
where: { email: data.email },
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
if (exists) {
|
|
178
|
+
throw new Error('Email already registered')
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 비밀번호 해싱 (내부 헬퍼)
|
|
182
|
+
const hashedPassword = await hashPassword(data.password)
|
|
183
|
+
|
|
184
|
+
const user = await prisma.user.create({
|
|
185
|
+
data: {
|
|
186
|
+
...data,
|
|
187
|
+
password: hashedPassword,
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
return { user }
|
|
192
|
+
})
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### ❌ inputValidator 없이 POST (금지)
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// ❌ 금지: 입력 검증 없음
|
|
199
|
+
export const badCreate = createServerFn({ method: 'POST' })
|
|
200
|
+
.handler(async ({ data }) => {
|
|
201
|
+
// data 타입 불안전, 검증 없음
|
|
202
|
+
return prisma.user.create({ data })
|
|
203
|
+
})
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
</post_method>
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
<put_patch_methods>
|
|
211
|
+
|
|
212
|
+
## PUT/PATCH: 데이터 수정
|
|
213
|
+
|
|
214
|
+
### PUT: 전체 수정
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// ✅ PUT + inputValidator 필수
|
|
218
|
+
const updateUserSchema = z.object({
|
|
219
|
+
id: z.string().uuid(),
|
|
220
|
+
email: z.string().email(),
|
|
221
|
+
name: z.string().min(1).max(100),
|
|
222
|
+
age: z.number().int().min(0).optional(),
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
export const updateUser = createServerFn({ method: 'PUT' })
|
|
226
|
+
.middleware([authMiddleware])
|
|
227
|
+
.inputValidator(updateUserSchema)
|
|
228
|
+
.handler(async ({ data, context }): Promise<User> => {
|
|
229
|
+
// 권한 체크
|
|
230
|
+
const user = await prisma.user.findUnique({
|
|
231
|
+
where: { id: data.id },
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
if (user?.id !== context.user.id && context.user.role !== 'ADMIN') {
|
|
235
|
+
throw new Error('Unauthorized')
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return prisma.user.update({
|
|
239
|
+
where: { id: data.id },
|
|
240
|
+
data: {
|
|
241
|
+
email: data.email,
|
|
242
|
+
name: data.name,
|
|
243
|
+
...(data.age !== undefined && { age: data.age }),
|
|
244
|
+
},
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### PATCH: 부분 수정
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// ✅ PATCH + inputValidator 필수 (모든 필드 optional)
|
|
253
|
+
const patchUserSchema = z.object({
|
|
254
|
+
id: z.string().uuid(),
|
|
255
|
+
email: z.string().email().optional(),
|
|
256
|
+
name: z.string().min(1).max(100).optional(),
|
|
257
|
+
age: z.number().int().min(0).optional(),
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
export const patchUser = createServerFn({ method: 'PATCH' })
|
|
261
|
+
.middleware([authMiddleware])
|
|
262
|
+
.inputValidator(patchUserSchema)
|
|
263
|
+
.handler(async ({ data, context }): Promise<User> => {
|
|
264
|
+
const { id, ...updateData } = data
|
|
265
|
+
|
|
266
|
+
// 권한 체크
|
|
267
|
+
if (id !== context.user.id && context.user.role !== 'ADMIN') {
|
|
268
|
+
throw new Error('Unauthorized')
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return prisma.user.update({
|
|
272
|
+
where: { id },
|
|
273
|
+
data: updateData,
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
</put_patch_methods>
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
<delete_method>
|
|
283
|
+
|
|
284
|
+
## DELETE: 데이터 삭제
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// ✅ DELETE + 권한 체크
|
|
288
|
+
const deletePostSchema = z.object({
|
|
289
|
+
id: z.string().uuid(),
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
export const deletePost = createServerFn({ method: 'DELETE' })
|
|
293
|
+
.middleware([authMiddleware])
|
|
294
|
+
.inputValidator(deletePostSchema)
|
|
295
|
+
.handler(async ({ data, context }): Promise<{ success: true }> => {
|
|
296
|
+
// 권한 체크
|
|
297
|
+
const post = await prisma.post.findUnique({
|
|
298
|
+
where: { id: data.id },
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
if (!post) {
|
|
302
|
+
throw new Error('Post not found')
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (post.authorId !== context.user.id && context.user.role !== 'ADMIN') {
|
|
306
|
+
throw new Error('Forbidden')
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
await prisma.post.delete({
|
|
310
|
+
where: { id: data.id },
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
return { success: true }
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
// ✅ 관리자 전용 삭제
|
|
317
|
+
export const adminDeleteUser = createServerFn({ method: 'DELETE' })
|
|
318
|
+
.middleware([adminMiddleware])
|
|
319
|
+
.inputValidator(z.object({ id: z.string().uuid() }))
|
|
320
|
+
.handler(async ({ data }): Promise<{ success: true }> => {
|
|
321
|
+
await prisma.user.delete({
|
|
322
|
+
where: { id: data.id },
|
|
323
|
+
})
|
|
324
|
+
return { success: true }
|
|
325
|
+
})
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
</delete_method>
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
<input_validator>
|
|
333
|
+
|
|
334
|
+
## .inputValidator() - Zod 검증
|
|
335
|
+
|
|
336
|
+
### 기본 검증
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// ✅ 단순 객체
|
|
340
|
+
const emailSchema = z.object({
|
|
341
|
+
email: z.string().email(),
|
|
35
342
|
})
|
|
36
343
|
|
|
37
|
-
|
|
344
|
+
export const sendEmail = createServerFn({ method: 'POST' })
|
|
345
|
+
.inputValidator(emailSchema)
|
|
346
|
+
.handler(async ({ data }) => {
|
|
347
|
+
// data.email은 string (타입 안전)
|
|
348
|
+
await sendEmailService(data.email)
|
|
349
|
+
return { sent: true }
|
|
350
|
+
})
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### 복잡한 검증
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
// ✅ 중첩 객체, 배열, transform
|
|
357
|
+
const createProjectSchema = z.object({
|
|
358
|
+
name: z.string().min(1).max(100).trim(),
|
|
359
|
+
description: z.string().min(10).max(1000).optional(),
|
|
360
|
+
tags: z.array(z.string().min(1)).max(10),
|
|
361
|
+
settings: z.object({
|
|
362
|
+
isPublic: z.boolean().default(false),
|
|
363
|
+
allowComments: z.boolean().default(true),
|
|
364
|
+
}),
|
|
365
|
+
startDate: z.coerce.date(),
|
|
366
|
+
}).strict() // 추가 필드 금지
|
|
367
|
+
|
|
368
|
+
export const createProject = createServerFn({ method: 'POST' })
|
|
369
|
+
.inputValidator(createProjectSchema)
|
|
370
|
+
.handler(async ({ data }) => {
|
|
371
|
+
// data 완전 검증됨
|
|
372
|
+
return prisma.project.create({ data })
|
|
373
|
+
})
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### 조건부 검증
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
// ✅ Zod refine/superRefine
|
|
380
|
+
const passwordSchema = z.object({
|
|
381
|
+
password: z.string().min(8),
|
|
382
|
+
confirmPassword: z.string(),
|
|
383
|
+
}).refine((data) => data.password === data.confirmPassword, {
|
|
384
|
+
message: 'Passwords do not match',
|
|
385
|
+
path: ['confirmPassword'],
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
export const resetPassword = createServerFn({ method: 'POST' })
|
|
389
|
+
.inputValidator(passwordSchema)
|
|
390
|
+
.handler(async ({ data }) => {
|
|
391
|
+
// 비밀번호 업데이트
|
|
392
|
+
return { success: true }
|
|
393
|
+
})
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
</input_validator>
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
<middleware>
|
|
401
|
+
|
|
402
|
+
## .middleware() - 인증 및 권한 체크
|
|
403
|
+
|
|
404
|
+
### 기본 미들웨어
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
// ✅ 인증 미들웨어
|
|
408
|
+
const authMiddleware = createMiddleware({ type: 'function' })
|
|
409
|
+
.server(async ({ next, request }) => {
|
|
410
|
+
const session = await getSession(request)
|
|
411
|
+
if (!session?.user) {
|
|
412
|
+
throw redirect({ to: '/login' })
|
|
413
|
+
}
|
|
414
|
+
return next({ context: { user: session.user } })
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
// 적용
|
|
418
|
+
export const getMyPosts = createServerFn({ method: 'GET' })
|
|
419
|
+
.middleware([authMiddleware])
|
|
420
|
+
.handler(async ({ context }): Promise<Post[]> => {
|
|
421
|
+
return prisma.post.findMany({
|
|
422
|
+
where: { authorId: context.user.id },
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### 권한 체크 미들웨어
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
// ✅ 관리자 미들웨어
|
|
431
|
+
const adminMiddleware = createMiddleware({ type: 'function' })
|
|
432
|
+
.server(async ({ next, request }) => {
|
|
433
|
+
const session = await getSession(request)
|
|
434
|
+
if (session?.user?.role !== 'ADMIN') {
|
|
435
|
+
throw new Error('Forbidden: Admin only')
|
|
436
|
+
}
|
|
437
|
+
return next({ context: { user: session.user } })
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
// ✅ 역할 기반 미들웨어
|
|
441
|
+
const roleMiddleware = (allowedRoles: string[]) =>
|
|
442
|
+
createMiddleware({ type: 'function' })
|
|
443
|
+
.server(async ({ next, request }) => {
|
|
444
|
+
const session = await getSession(request)
|
|
445
|
+
if (!session?.user || !allowedRoles.includes(session.user.role)) {
|
|
446
|
+
throw new Error('Forbidden')
|
|
447
|
+
}
|
|
448
|
+
return next({ context: { user: session.user } })
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
// 사용
|
|
452
|
+
export const deleteAnyUser = createServerFn({ method: 'DELETE' })
|
|
453
|
+
.middleware([adminMiddleware])
|
|
454
|
+
.inputValidator(z.object({ id: z.string() }))
|
|
455
|
+
.handler(async ({ data }) => {
|
|
456
|
+
return prisma.user.delete({ where: { id: data.id } })
|
|
457
|
+
})
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### 미들웨어 체이닝
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
// ✅ 여러 미들웨어 조합
|
|
464
|
+
export const protectedFn = createServerFn({ method: 'POST' })
|
|
465
|
+
.middleware([
|
|
466
|
+
authMiddleware, // 1. 인증
|
|
467
|
+
adminMiddleware, // 2. 권한
|
|
468
|
+
])
|
|
469
|
+
.inputValidator(someSchema)
|
|
470
|
+
.handler(async ({ data, context }) => {
|
|
471
|
+
// context.user는 존재 (인증됨) + ADMIN 역할 확인됨
|
|
472
|
+
return { success: true }
|
|
473
|
+
})
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
</middleware>
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
<client_calling>
|
|
481
|
+
|
|
482
|
+
## 클라이언트에서 호출 (TanStack Query 필수)
|
|
483
|
+
|
|
484
|
+
### useQuery (GET)
|
|
485
|
+
|
|
486
|
+
```tsx
|
|
487
|
+
// ✅ 데이터 조회
|
|
488
|
+
const UsersPage = (): JSX.Element => {
|
|
489
|
+
const { data, isLoading, error } = useQuery({
|
|
490
|
+
queryKey: ['users'],
|
|
491
|
+
queryFn: () => getUsers(),
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
if (isLoading) return <div>Loading...</div>
|
|
495
|
+
if (error) return <div>Error: {error.message}</div>
|
|
496
|
+
|
|
497
|
+
return (
|
|
498
|
+
<ul>
|
|
499
|
+
{data?.map((user) => (
|
|
500
|
+
<li key={user.id}>{user.name}</li>
|
|
501
|
+
))}
|
|
502
|
+
</ul>
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### useMutation (POST/PUT/DELETE)
|
|
508
|
+
|
|
509
|
+
```tsx
|
|
510
|
+
// ✅ 데이터 생성/수정/삭제
|
|
511
|
+
const CreateUserForm = (): JSX.Element => {
|
|
512
|
+
const queryClient = useQueryClient()
|
|
513
|
+
|
|
514
|
+
const mutation = useMutation({
|
|
515
|
+
mutationFn: (payload: { email: string; name: string }) =>
|
|
516
|
+
createUser(payload),
|
|
517
|
+
onSuccess: () => {
|
|
518
|
+
// 캐시 무효화 → 자동 리페치
|
|
519
|
+
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
520
|
+
},
|
|
521
|
+
onError: (error) => {
|
|
522
|
+
console.error('Creation failed:', error.message)
|
|
523
|
+
},
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
527
|
+
e.preventDefault()
|
|
528
|
+
const formData = new FormData(e.currentTarget)
|
|
529
|
+
|
|
530
|
+
mutation.mutate({
|
|
531
|
+
email: formData.get('email') as string,
|
|
532
|
+
name: formData.get('name') as string,
|
|
533
|
+
})
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return (
|
|
537
|
+
<form onSubmit={handleSubmit}>
|
|
538
|
+
<input
|
|
539
|
+
name="email"
|
|
540
|
+
type="email"
|
|
541
|
+
required
|
|
542
|
+
disabled={mutation.isPending}
|
|
543
|
+
/>
|
|
544
|
+
<input
|
|
545
|
+
name="name"
|
|
546
|
+
required
|
|
547
|
+
disabled={mutation.isPending}
|
|
548
|
+
/>
|
|
549
|
+
<button type="submit" disabled={mutation.isPending}>
|
|
550
|
+
{mutation.isPending ? 'Creating...' : 'Create'}
|
|
551
|
+
</button>
|
|
552
|
+
{mutation.error && (
|
|
553
|
+
<p style={{ color: 'red' }}>Error: {mutation.error.message}</p>
|
|
554
|
+
)}
|
|
555
|
+
</form>
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### Optimistic Updates
|
|
561
|
+
|
|
562
|
+
```tsx
|
|
563
|
+
// ✅ 낙관적 업데이트
|
|
38
564
|
const mutation = useMutation({
|
|
39
|
-
mutationFn: createPost,
|
|
40
|
-
|
|
565
|
+
mutationFn: (newPost) => createPost(newPost),
|
|
566
|
+
onMutate: async (newPost) => {
|
|
567
|
+
// 기존 쿼리 취소
|
|
568
|
+
await queryClient.cancelQueries({ queryKey: ['posts'] })
|
|
569
|
+
|
|
570
|
+
// 이전 데이터 저장
|
|
571
|
+
const previousPosts = queryClient.getQueryData(['posts'])
|
|
572
|
+
|
|
573
|
+
// 낙관적 업데이트
|
|
574
|
+
queryClient.setQueryData(
|
|
575
|
+
['posts'],
|
|
576
|
+
(old: Post[] = []) => [...old, newPost],
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
return { previousPosts }
|
|
580
|
+
},
|
|
581
|
+
onError: (err, newPost, context) => {
|
|
582
|
+
// 실패 시 롤백
|
|
583
|
+
queryClient.setQueryData(['posts'], context?.previousPosts)
|
|
584
|
+
},
|
|
585
|
+
onSuccess: () => {
|
|
586
|
+
queryClient.invalidateQueries({ queryKey: ['posts'] })
|
|
587
|
+
},
|
|
41
588
|
})
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### ❌ 직접 호출 금지
|
|
592
|
+
|
|
593
|
+
```tsx
|
|
594
|
+
// ❌ 잘못된 패턴 (캐싱 없음, 동기화 안됨)
|
|
595
|
+
const BadComponent = (): JSX.Element => {
|
|
596
|
+
const [users, setUsers] = useState<User[]>([])
|
|
597
|
+
const [loading, setLoading] = useState(false)
|
|
42
598
|
|
|
43
|
-
|
|
599
|
+
useEffect(() => {
|
|
600
|
+
setLoading(true)
|
|
601
|
+
getUsers() // ❌ 직접 호출 (TanStack Query 미사용)
|
|
602
|
+
.then(setUsers)
|
|
603
|
+
.catch(console.error)
|
|
604
|
+
.finally(() => setLoading(false))
|
|
605
|
+
}, [])
|
|
606
|
+
|
|
607
|
+
return <div>{/* ... */}</div>
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
</client_calling>
|
|
612
|
+
|
|
613
|
+
---
|
|
614
|
+
|
|
615
|
+
<helper_functions>
|
|
616
|
+
|
|
617
|
+
## 헬퍼 함수 규칙
|
|
618
|
+
|
|
619
|
+
### ❌ 잘못된 구조
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
// functions/user-functions.ts
|
|
623
|
+
|
|
624
|
+
// ❌ 헬퍼 함수를 export (금지!)
|
|
625
|
+
export const validateUserEmail = async (email: string): Promise<void> => {
|
|
626
|
+
const exists = await prisma.user.findUnique({ where: { email } })
|
|
627
|
+
if (exists) throw new Error('Email already exists')
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Server Function
|
|
631
|
+
export const createUser = createServerFn({ method: 'POST' })
|
|
632
|
+
.inputValidator(createUserSchema)
|
|
633
|
+
.handler(async ({ data }) => {
|
|
634
|
+
await validateUserEmail(data.email) // ❌ export된 헬퍼
|
|
635
|
+
return prisma.user.create({ data })
|
|
636
|
+
})
|
|
44
637
|
```
|
|
45
638
|
|
|
46
|
-
|
|
639
|
+
### ✅ 올바른 구조
|
|
47
640
|
|
|
48
641
|
```typescript
|
|
49
|
-
//
|
|
50
|
-
|
|
642
|
+
// functions/user-functions.ts
|
|
643
|
+
|
|
644
|
+
// ✅ 헬퍼는 export 금지 (내부용)
|
|
645
|
+
const validateUserEmail = async (email: string): Promise<void> => {
|
|
646
|
+
const exists = await prisma.user.findUnique({ where: { email } })
|
|
647
|
+
if (exists) throw new Error('Email already exists')
|
|
648
|
+
}
|
|
51
649
|
|
|
52
|
-
// Server Function
|
|
650
|
+
// Server Function만 export
|
|
53
651
|
export const createUser = createServerFn({ method: 'POST' })
|
|
54
652
|
.inputValidator(createUserSchema)
|
|
55
653
|
.handler(async ({ data }) => {
|
|
56
|
-
await
|
|
654
|
+
await validateUserEmail(data.email) // ✅ 내부 헬퍼만 사용
|
|
57
655
|
return prisma.user.create({ data })
|
|
58
656
|
})
|
|
59
657
|
|
|
60
|
-
// index.ts
|
|
61
|
-
export { createUser } from './
|
|
62
|
-
// ❌ export {
|
|
658
|
+
// index.ts
|
|
659
|
+
export { createUser } from './user-functions'
|
|
660
|
+
// ❌ export { validateUserEmail } 금지
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
</helper_functions>
|
|
664
|
+
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
<environment_security>
|
|
668
|
+
|
|
669
|
+
## 환경 변수 보안
|
|
670
|
+
|
|
671
|
+
### ❌ 잘못된 패턴
|
|
672
|
+
|
|
673
|
+
```tsx
|
|
674
|
+
// routes/config.tsx
|
|
675
|
+
|
|
676
|
+
// ❌ loader에서 환경변수 직접 사용 (클라이언트에 노출)
|
|
677
|
+
export const Route = createFileRoute('/config')({
|
|
678
|
+
loader: () => {
|
|
679
|
+
const apiSecret = process.env.API_SECRET // ❌ 클라이언트에 노출됨!
|
|
680
|
+
return { apiSecret }
|
|
681
|
+
},
|
|
682
|
+
component: ConfigPage,
|
|
683
|
+
})
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### ✅ 올바른 패턴
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
// functions/config.ts
|
|
690
|
+
|
|
691
|
+
// ✅ Server Function에서만 환경변수 사용
|
|
692
|
+
export const getSecretConfig = createServerFn({ method: 'GET' })
|
|
693
|
+
.middleware([authMiddleware])
|
|
694
|
+
.handler(async (): Promise<{ apiSecret: string }> => {
|
|
695
|
+
return {
|
|
696
|
+
apiSecret: process.env.API_SECRET, // ✅ 서버에서만 실행
|
|
697
|
+
}
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
// routes/config.tsx
|
|
701
|
+
export const Route = createFileRoute('/config')({
|
|
702
|
+
loader: async () => {
|
|
703
|
+
const config = await getSecretConfig()
|
|
704
|
+
return config
|
|
705
|
+
},
|
|
706
|
+
component: ConfigPage,
|
|
707
|
+
})
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
</environment_security>
|
|
711
|
+
|
|
712
|
+
---
|
|
713
|
+
|
|
714
|
+
<static_server_functions>
|
|
715
|
+
|
|
716
|
+
## 정적 Server Functions (Experimental)
|
|
717
|
+
|
|
718
|
+
```typescript
|
|
719
|
+
// ✅ 정적 함수 (빌드 타임에 최적화)
|
|
720
|
+
import { staticFunctionMiddleware } from '@tanstack/react-start'
|
|
721
|
+
|
|
722
|
+
export const getStaticPosts = createServerFn({ method: 'GET' })
|
|
723
|
+
.middleware([staticFunctionMiddleware])
|
|
724
|
+
.handler(async (): Promise<Post[]> => {
|
|
725
|
+
return prisma.post.findMany({
|
|
726
|
+
where: { published: true },
|
|
727
|
+
})
|
|
728
|
+
})
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
**주의:** 이 기능은 실험적이며, TanStack Start 버전에 따라 변경될 수 있습니다.
|
|
732
|
+
|
|
733
|
+
</static_server_functions>
|
|
734
|
+
|
|
735
|
+
---
|
|
736
|
+
|
|
737
|
+
<return_types>
|
|
738
|
+
|
|
739
|
+
## 명시적 Return Type
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
// ✅ 명시적 return type 필수
|
|
743
|
+
export const getUser = createServerFn({ method: 'GET' })
|
|
744
|
+
.inputValidator(z.object({ id: z.string() }))
|
|
745
|
+
.handler(async ({ data }): Promise<User | null> => {
|
|
746
|
+
return prisma.user.findUnique({ where: { id: data.id } })
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
// ✅ 복잡한 타입
|
|
750
|
+
export const getStats = createServerFn({ method: 'GET' })
|
|
751
|
+
.middleware([authMiddleware])
|
|
752
|
+
.handler(async (): Promise<{
|
|
753
|
+
totalUsers: number
|
|
754
|
+
totalPosts: number
|
|
755
|
+
lastUpdated: Date
|
|
756
|
+
}> => {
|
|
757
|
+
return {
|
|
758
|
+
totalUsers: await prisma.user.count(),
|
|
759
|
+
totalPosts: await prisma.post.count(),
|
|
760
|
+
lastUpdated: new Date(),
|
|
761
|
+
}
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
// ❌ 암시적 타입 (금지)
|
|
765
|
+
export const badFn = createServerFn({ method: 'GET' })
|
|
766
|
+
.handler(async () => {
|
|
767
|
+
// return 타입이 추론됨 (불명확)
|
|
768
|
+
return { data: 'something' }
|
|
769
|
+
})
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
</return_types>
|
|
773
|
+
|
|
774
|
+
---
|
|
775
|
+
|
|
776
|
+
<file_organization>
|
|
777
|
+
|
|
778
|
+
## 파일 구조 권장사항
|
|
779
|
+
|
|
780
|
+
### 권장 구조
|
|
781
|
+
|
|
782
|
+
```
|
|
783
|
+
src/utils/
|
|
784
|
+
├── users.functions.ts # Server Function 래퍼 (createServerFn)
|
|
785
|
+
├── users.server.ts # 서버 전용 헬퍼 (DB 쿼리, 내부 로직)
|
|
786
|
+
└── schemas.ts # 공유 검증 스키마 (클라이언트에서도 안전)
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
| 파일 | 역할 | import 가능 위치 |
|
|
790
|
+
|------|------|-----------------|
|
|
791
|
+
| `.functions.ts` | createServerFn 래퍼 export | 어디서든 안전 |
|
|
792
|
+
| `.server.ts` | 서버 전용 코드 | Server Function handler 내부에서만 |
|
|
793
|
+
| `.ts` (접미사 없음) | 클라이언트 안전 코드 (타입, 스키마, 상수) | 어디서든 안전 |
|
|
794
|
+
|
|
795
|
+
### 예시
|
|
796
|
+
|
|
797
|
+
```typescript
|
|
798
|
+
// users.server.ts - 서버 전용 헬퍼
|
|
799
|
+
import { db } from '~/db'
|
|
800
|
+
|
|
801
|
+
export async function findUserById(id: string) {
|
|
802
|
+
return db.query.users.findFirst({ where: eq(users.id, id) })
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// users.functions.ts - Server Functions
|
|
806
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
807
|
+
import { findUserById } from './users.server'
|
|
808
|
+
|
|
809
|
+
export const getUser = createServerFn({ method: 'GET' })
|
|
810
|
+
.inputValidator((data: { id: string }) => data)
|
|
811
|
+
.handler(async ({ data }) => {
|
|
812
|
+
return findUserById(data.id)
|
|
813
|
+
})
|
|
63
814
|
```
|
|
64
815
|
|
|
65
|
-
|
|
816
|
+
### Static Import 안전성
|
|
817
|
+
|
|
818
|
+
Server Function은 클라이언트 컴포넌트에서 **정적으로 import 해도 안전**합니다:
|
|
819
|
+
|
|
820
|
+
```tsx
|
|
821
|
+
// ✅ 안전 - 빌드 프로세스가 환경별로 처리
|
|
822
|
+
import { getUser } from '~/utils/users.functions'
|
|
823
|
+
|
|
824
|
+
function UserProfile({ id }) {
|
|
825
|
+
const { data } = useQuery({
|
|
826
|
+
queryKey: ['user', id],
|
|
827
|
+
queryFn: () => getUser({ data: { id } }),
|
|
828
|
+
})
|
|
829
|
+
}
|
|
830
|
+
```
|
|
66
831
|
|
|
67
832
|
```tsx
|
|
68
|
-
// ❌
|
|
69
|
-
|
|
833
|
+
// ❌ 동적 import 피하기 (번들러 문제 발생 가능)
|
|
834
|
+
const { getUser } = await import('~/utils/users.functions')
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
</file_organization>
|
|
838
|
+
|
|
839
|
+
---
|
|
840
|
+
|
|
841
|
+
<error_redirects>
|
|
842
|
+
|
|
843
|
+
## 에러 처리 및 리다이렉트
|
|
844
|
+
|
|
845
|
+
### 기본 에러
|
|
846
|
+
|
|
847
|
+
```typescript
|
|
848
|
+
export const riskyFunction = createServerFn().handler(async () => {
|
|
849
|
+
if (Math.random() > 0.5) {
|
|
850
|
+
throw new Error('Something went wrong!')
|
|
851
|
+
}
|
|
852
|
+
return { success: true }
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
// 에러가 클라이언트로 직렬화됨
|
|
856
|
+
try {
|
|
857
|
+
await riskyFunction()
|
|
858
|
+
} catch (error) {
|
|
859
|
+
console.log(error.message) // "Something went wrong!"
|
|
860
|
+
}
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
### 리다이렉트
|
|
864
|
+
|
|
865
|
+
```typescript
|
|
866
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
867
|
+
import { redirect } from '@tanstack/react-router'
|
|
868
|
+
|
|
869
|
+
export const requireAuth = createServerFn().handler(async () => {
|
|
870
|
+
const user = await getCurrentUser()
|
|
871
|
+
|
|
872
|
+
if (!user) {
|
|
873
|
+
throw redirect({ to: '/login' })
|
|
874
|
+
}
|
|
70
875
|
|
|
71
|
-
|
|
72
|
-
const fn = createServerFn().handler(() => {
|
|
73
|
-
const secret = process.env.SECRET // 서버에서만
|
|
876
|
+
return user
|
|
74
877
|
})
|
|
75
878
|
```
|
|
879
|
+
|
|
880
|
+
### Not Found
|
|
881
|
+
|
|
882
|
+
```typescript
|
|
883
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
884
|
+
import { notFound } from '@tanstack/react-router'
|
|
885
|
+
|
|
886
|
+
export const getPost = createServerFn()
|
|
887
|
+
.inputValidator((data: { id: string }) => data)
|
|
888
|
+
.handler(async ({ data }) => {
|
|
889
|
+
const post = await db.findPost(data.id)
|
|
890
|
+
|
|
891
|
+
if (!post) {
|
|
892
|
+
throw notFound()
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return post
|
|
896
|
+
})
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
</error_redirects>
|
|
900
|
+
|
|
901
|
+
---
|
|
902
|
+
|
|
903
|
+
<server_context>
|
|
904
|
+
|
|
905
|
+
## Server Context 유틸리티
|
|
906
|
+
|
|
907
|
+
Server Function 내에서 요청/응답을 직접 제어할 수 있습니다:
|
|
908
|
+
|
|
909
|
+
```typescript
|
|
910
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
911
|
+
import {
|
|
912
|
+
getRequest,
|
|
913
|
+
getRequestHeader,
|
|
914
|
+
setResponseHeaders,
|
|
915
|
+
setResponseStatus,
|
|
916
|
+
} from '@tanstack/react-start/server'
|
|
917
|
+
|
|
918
|
+
export const getCachedData = createServerFn({ method: 'GET' }).handler(
|
|
919
|
+
async () => {
|
|
920
|
+
// 요청 정보 접근
|
|
921
|
+
const request = getRequest()
|
|
922
|
+
const authHeader = getRequestHeader('Authorization')
|
|
923
|
+
|
|
924
|
+
// 응답 헤더 설정 (캐싱 등)
|
|
925
|
+
setResponseHeaders(
|
|
926
|
+
new Headers({
|
|
927
|
+
'Cache-Control': 'public, max-age=300',
|
|
928
|
+
'CDN-Cache-Control': 'max-age=3600, stale-while-revalidate=600',
|
|
929
|
+
}),
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
// 상태 코드 설정 (선택)
|
|
933
|
+
setResponseStatus(200)
|
|
934
|
+
|
|
935
|
+
return fetchData()
|
|
936
|
+
},
|
|
937
|
+
)
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
### 사용 가능한 유틸리티
|
|
941
|
+
|
|
942
|
+
| 유틸리티 | 설명 |
|
|
943
|
+
|---------|------|
|
|
944
|
+
| `getRequest()` | 전체 Request 객체 접근 |
|
|
945
|
+
| `getRequestHeader(name)` | 특정 요청 헤더 읽기 |
|
|
946
|
+
| `setResponseHeader(name, value)` | 단일 응답 헤더 설정 |
|
|
947
|
+
| `setResponseHeaders(headers)` | Headers 객체로 복수 헤더 설정 |
|
|
948
|
+
| `setResponseStatus(code)` | HTTP 상태 코드 설정 |
|
|
949
|
+
|
|
950
|
+
</server_context>
|
|
951
|
+
|
|
952
|
+
---
|
|
953
|
+
|
|
954
|
+
<form_data>
|
|
955
|
+
|
|
956
|
+
## FormData 처리
|
|
957
|
+
|
|
958
|
+
```typescript
|
|
959
|
+
export const submitForm = createServerFn({ method: 'POST' })
|
|
960
|
+
.inputValidator((data) => {
|
|
961
|
+
if (!(data instanceof FormData)) {
|
|
962
|
+
throw new Error('Expected FormData')
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
return {
|
|
966
|
+
name: data.get('name')?.toString() || '',
|
|
967
|
+
email: data.get('email')?.toString() || '',
|
|
968
|
+
}
|
|
969
|
+
})
|
|
970
|
+
.handler(async ({ data }) => {
|
|
971
|
+
// data.name, data.email 사용
|
|
972
|
+
return { success: true }
|
|
973
|
+
})
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
</form_data>
|
|
977
|
+
|
|
978
|
+
---
|
|
979
|
+
|
|
980
|
+
<version_info>
|
|
981
|
+
|
|
982
|
+
**Version:** v1.159.4 (2026-02-09 기준)
|
|
983
|
+
|
|
984
|
+
**주요 변경사항:**
|
|
985
|
+
- `.inputValidator()` replaces `.validator()` (deprecated)
|
|
986
|
+
- Enhanced middleware system with context
|
|
987
|
+
- Improved type safety for async handlers
|
|
988
|
+
- Server context utilities (getRequest, setResponseHeaders 등)
|
|
989
|
+
- Static import safety (빌드 시 환경별 처리)
|
|
990
|
+
|
|
991
|
+
**패키지:** `@tanstack/react-start`
|
|
992
|
+
|
|
993
|
+
</version_info>
|