@kood/claude-code 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +21 -243
- package/package.json +1 -1
- package/templates/hono/CLAUDE.md +10 -6
- package/templates/hono/docs/deployment/index.md +5 -0
- package/templates/hono/docs/library/hono/index.md +6 -0
- package/templates/hono/docs/library/prisma/index.md +3 -0
- package/templates/npx/CLAUDE.md +8 -2
- package/templates/tanstack-start/CLAUDE.md +103 -255
- package/templates/tanstack-start/docs/deployment/cloudflare.md +37 -424
- package/templates/tanstack-start/docs/deployment/index.md +57 -286
- package/templates/tanstack-start/docs/deployment/nitro.md +36 -318
- package/templates/tanstack-start/docs/deployment/railway.md +40 -409
- package/templates/tanstack-start/docs/deployment/vercel.md +43 -465
- package/templates/tanstack-start/docs/design/accessibility.md +56 -326
- package/templates/tanstack-start/docs/design/color.md +37 -179
- package/templates/tanstack-start/docs/design/components.md +77 -311
- package/templates/tanstack-start/docs/design/index.md +24 -87
- package/templates/tanstack-start/docs/design/safe-area.md +51 -250
- package/templates/tanstack-start/docs/design/spacing.md +57 -276
- package/templates/tanstack-start/docs/design/tailwind-setup.md +45 -359
- package/templates/tanstack-start/docs/design/typography.md +40 -284
- package/templates/tanstack-start/docs/library/better-auth/2fa.md +27 -115
- package/templates/tanstack-start/docs/library/better-auth/advanced.md +22 -105
- package/templates/tanstack-start/docs/library/better-auth/index.md +17 -66
- package/templates/tanstack-start/docs/library/better-auth/plugins.md +11 -88
- package/templates/tanstack-start/docs/library/better-auth/session.md +12 -92
- package/templates/tanstack-start/docs/library/better-auth/setup.md +9 -91
- package/templates/tanstack-start/docs/library/prisma/cloudflare-d1.md +30 -358
- package/templates/tanstack-start/docs/library/prisma/config.md +27 -327
- package/templates/tanstack-start/docs/library/prisma/crud.md +46 -174
- package/templates/tanstack-start/docs/library/prisma/index.md +23 -113
- package/templates/tanstack-start/docs/library/prisma/relations.md +31 -153
- package/templates/tanstack-start/docs/library/prisma/schema.md +40 -217
- package/templates/tanstack-start/docs/library/prisma/setup.md +12 -112
- package/templates/tanstack-start/docs/library/prisma/transactions.md +20 -110
- package/templates/tanstack-start/docs/library/tanstack-query/index.md +12 -99
- package/templates/tanstack-start/docs/library/tanstack-query/invalidation.md +28 -107
- package/templates/tanstack-start/docs/library/tanstack-query/optimistic-updates.md +44 -146
- package/templates/tanstack-start/docs/library/tanstack-query/setup.md +11 -70
- package/templates/tanstack-start/docs/library/tanstack-query/use-mutation.md +33 -127
- package/templates/tanstack-start/docs/library/tanstack-query/use-query.md +49 -149
- package/templates/tanstack-start/docs/library/tanstack-start/auth-patterns.md +19 -112
- package/templates/tanstack-start/docs/library/tanstack-start/index.md +33 -80
- package/templates/tanstack-start/docs/library/tanstack-start/middleware.md +28 -106
- package/templates/tanstack-start/docs/library/tanstack-start/routing.md +21 -118
- package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +34 -246
- package/templates/tanstack-start/docs/library/tanstack-start/setup.md +6 -39
- package/templates/tanstack-start/docs/library/zod/basic-types.md +33 -145
- package/templates/tanstack-start/docs/library/zod/complex-types.md +32 -156
- package/templates/tanstack-start/docs/library/zod/index.md +22 -150
- package/templates/tanstack-start/docs/library/zod/transforms.md +20 -129
- package/templates/tanstack-start/docs/library/zod/validation.md +39 -155
- package/templates/hono/docs/commands/git.md +0 -145
- package/templates/hono/docs/mcp/context7.md +0 -106
- package/templates/hono/docs/mcp/index.md +0 -176
- package/templates/hono/docs/mcp/sequential-thinking.md +0 -101
- package/templates/hono/docs/mcp/serena.md +0 -269
- package/templates/hono/docs/mcp/sgrep.md +0 -105
- package/templates/hono/docs/skills/gemini-review/SKILL.md +0 -220
- package/templates/hono/docs/skills/gemini-review/references/checklists.md +0 -136
- package/templates/hono/docs/skills/gemini-review/references/prompt-templates.md +0 -303
- package/templates/npx/docs/commands/git.md +0 -145
- package/templates/npx/docs/mcp/index.md +0 -60
- package/templates/npx/docs/skills/gemini-review/SKILL.md +0 -220
- package/templates/npx/docs/skills/gemini-review/references/checklists.md +0 -134
- package/templates/npx/docs/skills/gemini-review/references/prompt-templates.md +0 -301
- package/templates/tanstack-start/docs/commands/git.md +0 -145
- package/templates/tanstack-start/docs/mcp/context7.md +0 -204
- package/templates/tanstack-start/docs/mcp/index.md +0 -177
- package/templates/tanstack-start/docs/mcp/sequential-thinking.md +0 -180
- package/templates/tanstack-start/docs/mcp/serena.md +0 -269
- package/templates/tanstack-start/docs/mcp/sgrep.md +0 -174
- package/templates/tanstack-start/docs/skills/gemini-review/SKILL.md +0 -220
- package/templates/tanstack-start/docs/skills/gemini-review/references/checklists.md +0 -144
- package/templates/tanstack-start/docs/skills/gemini-review/references/prompt-templates.md +0 -292
|
@@ -1,142 +1,64 @@
|
|
|
1
1
|
# TanStack Start - Middleware
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Server Function 및 라우트에 공통 로직 적용.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Server Function Middleware
|
|
5
|
+
## 기본 패턴
|
|
8
6
|
|
|
9
7
|
```typescript
|
|
10
|
-
import { createMiddleware, createServerFn } from '@tanstack/react-start'
|
|
11
|
-
|
|
12
8
|
// 미들웨어 정의
|
|
13
9
|
const loggingMiddleware = createMiddleware({ type: 'function' })
|
|
14
|
-
.client(() => {
|
|
15
|
-
console.log('Client: Server function called')
|
|
16
|
-
})
|
|
17
10
|
.server(({ next }) => {
|
|
18
|
-
console.log('
|
|
11
|
+
console.log('Processing request')
|
|
19
12
|
return next()
|
|
20
13
|
})
|
|
21
14
|
|
|
22
|
-
//
|
|
15
|
+
// 적용
|
|
23
16
|
const fn = createServerFn()
|
|
24
17
|
.middleware([loggingMiddleware])
|
|
25
|
-
.handler(async () => {
|
|
26
|
-
return { message: 'Hello' }
|
|
27
|
-
})
|
|
18
|
+
.handler(async () => ({ message: 'Hello' }))
|
|
28
19
|
```
|
|
29
20
|
|
|
30
|
-
##
|
|
21
|
+
## 인증 미들웨어
|
|
31
22
|
|
|
32
23
|
```typescript
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
workspaceId: z.string(),
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
const workspaceMiddleware = createMiddleware({ type: 'function' })
|
|
42
|
-
.inputValidator(zodValidator(mySchema))
|
|
43
|
-
.server(({ next, data }) => {
|
|
44
|
-
console.log('Workspace ID:', data.workspaceId)
|
|
45
|
-
return next()
|
|
24
|
+
const authMiddleware = createMiddleware({ type: 'function' })
|
|
25
|
+
.server(async ({ next }) => {
|
|
26
|
+
const session = await getSession()
|
|
27
|
+
if (!session) throw redirect({ to: '/login' })
|
|
28
|
+
return next({ context: { user: session.user } })
|
|
46
29
|
})
|
|
47
|
-
```
|
|
48
30
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
import { createStart, createMiddleware } from '@tanstack/react-start'
|
|
54
|
-
|
|
55
|
-
const myGlobalMiddleware = createMiddleware().server(({ next }) => {
|
|
56
|
-
console.log('Global middleware running')
|
|
57
|
-
return next()
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
export const startInstance = createStart(() => {
|
|
61
|
-
return {
|
|
62
|
-
requestMiddleware: [myGlobalMiddleware],
|
|
63
|
-
}
|
|
64
|
-
})
|
|
31
|
+
// 사용
|
|
32
|
+
export const protectedFn = createServerFn({ method: 'GET' })
|
|
33
|
+
.middleware([authMiddleware])
|
|
34
|
+
.handler(async ({ context }) => ({ user: context.user }))
|
|
65
35
|
```
|
|
66
36
|
|
|
67
|
-
##
|
|
37
|
+
## Zod Validation Middleware
|
|
68
38
|
|
|
69
39
|
```typescript
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
export const startInstance = createStart(() => {
|
|
75
|
-
return {
|
|
76
|
-
functionMiddleware: [loggingMiddleware],
|
|
77
|
-
}
|
|
78
|
-
})
|
|
40
|
+
const workspaceMiddleware = createMiddleware({ type: 'function' })
|
|
41
|
+
.inputValidator(zodValidator(z.object({ workspaceId: z.string() })))
|
|
42
|
+
.server(({ next, data }) => next())
|
|
79
43
|
```
|
|
80
44
|
|
|
81
|
-
##
|
|
45
|
+
## Global Middleware
|
|
82
46
|
|
|
83
47
|
```typescript
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return new Response('Hello, World! from ' + request.url)
|
|
90
|
-
},
|
|
91
|
-
POST: async ({ request }) => {
|
|
92
|
-
const body = await request.json()
|
|
93
|
-
return new Response(`Hello, ${body.name}!`)
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
})
|
|
48
|
+
// src/start.ts
|
|
49
|
+
export const startInstance = createStart(() => ({
|
|
50
|
+
requestMiddleware: [globalMiddleware], // 모든 요청
|
|
51
|
+
functionMiddleware: [loggingMiddleware], // 모든 Server Function
|
|
52
|
+
}))
|
|
98
53
|
```
|
|
99
54
|
|
|
100
|
-
##
|
|
55
|
+
## Route-Level
|
|
101
56
|
|
|
102
57
|
```typescript
|
|
103
58
|
export const Route = createFileRoute('/hello')({
|
|
104
59
|
server: {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
GET: {
|
|
108
|
-
middleware: [loggerMiddleware], // GET에만 적용
|
|
109
|
-
handler: async ({ request }) => {
|
|
110
|
-
return new Response('Hello, World! from ' + request.url)
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
}),
|
|
60
|
+
middleware: [authMiddleware], // 모든 핸들러
|
|
61
|
+
handlers: { GET: async ({ request }) => new Response('Hello') },
|
|
114
62
|
},
|
|
115
63
|
})
|
|
116
64
|
```
|
|
117
|
-
|
|
118
|
-
## 인증 미들웨어 예시
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
import { createMiddleware } from '@tanstack/react-start'
|
|
122
|
-
import { redirect } from '@tanstack/react-router'
|
|
123
|
-
|
|
124
|
-
const authMiddleware = createMiddleware({ type: 'function' })
|
|
125
|
-
.server(async ({ next }) => {
|
|
126
|
-
const session = await getSession()
|
|
127
|
-
|
|
128
|
-
if (!session) {
|
|
129
|
-
throw redirect({ to: '/login' })
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return next({ context: { user: session.user } })
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
// 사용
|
|
136
|
-
export const protectedFn = createServerFn({ method: 'GET' })
|
|
137
|
-
.middleware([authMiddleware])
|
|
138
|
-
.handler(async ({ context }) => {
|
|
139
|
-
// context.user 사용 가능
|
|
140
|
-
return { user: context.user }
|
|
141
|
-
})
|
|
142
|
-
```
|
|
@@ -1,163 +1,66 @@
|
|
|
1
1
|
# TanStack Start - Routing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
파일 기반 라우팅.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## 기본 라우트
|
|
5
|
+
## 기본 패턴
|
|
8
6
|
|
|
9
7
|
```tsx
|
|
10
8
|
// routes/about.tsx
|
|
11
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
12
|
-
|
|
13
9
|
export const Route = createFileRoute('/about')({
|
|
14
10
|
component: AboutPage,
|
|
15
11
|
})
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
return <h1>About</h1>
|
|
19
|
-
}
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Loader를 사용한 데이터 로딩
|
|
23
|
-
|
|
24
|
-
```tsx
|
|
25
|
-
// routes/index.tsx
|
|
26
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
27
|
-
|
|
13
|
+
// Loader
|
|
28
14
|
export const Route = createFileRoute('/')({
|
|
29
15
|
component: Page,
|
|
30
|
-
loader: async () =>
|
|
31
|
-
const res = await fetch('https://api.example.com/posts')
|
|
32
|
-
return res.json()
|
|
33
|
-
},
|
|
16
|
+
loader: async () => fetch('/api/posts').then(r => r.json()),
|
|
34
17
|
})
|
|
35
18
|
|
|
36
19
|
function Page() {
|
|
37
20
|
const posts = Route.useLoaderData()
|
|
38
|
-
return (
|
|
39
|
-
<ul>
|
|
40
|
-
{posts.map((post) => (
|
|
41
|
-
<li key={post.id}>{post.title}</li>
|
|
42
|
-
))}
|
|
43
|
-
</ul>
|
|
44
|
-
)
|
|
21
|
+
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
|
|
45
22
|
}
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## 동적 라우트
|
|
49
|
-
|
|
50
|
-
```tsx
|
|
51
|
-
// routes/users/$id.tsx
|
|
52
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
53
23
|
|
|
24
|
+
// 동적 라우트
|
|
54
25
|
export const Route = createFileRoute('/users/$id')({
|
|
55
|
-
loader: async ({ params }) => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
component: UserDetailPage,
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
function UserDetailPage() {
|
|
63
|
-
const { user } = Route.useLoaderData()
|
|
64
|
-
return <h1>{user.name}</h1>
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## SSR 설정
|
|
69
|
-
|
|
70
|
-
```tsx
|
|
71
|
-
// routes/posts/$postId.tsx
|
|
72
|
-
export const Route = createFileRoute('/posts/$postId')({
|
|
73
|
-
ssr: true, // SSR 활성화
|
|
74
|
-
beforeLoad: () => {
|
|
75
|
-
console.log('서버에서 초기 요청 시 실행')
|
|
76
|
-
},
|
|
77
|
-
loader: () => {
|
|
78
|
-
console.log('서버에서 초기 요청 시 실행')
|
|
26
|
+
loader: async ({ params }) => ({ user: await getUserById(params.id) }),
|
|
27
|
+
component: () => {
|
|
28
|
+
const { user } = Route.useLoaderData()
|
|
29
|
+
return <h1>{user.name}</h1>
|
|
79
30
|
},
|
|
80
|
-
component: () => <div>서버에서 렌더링됨</div>,
|
|
81
31
|
})
|
|
82
32
|
```
|
|
83
33
|
|
|
84
|
-
|
|
34
|
+
## SSR 옵션
|
|
85
35
|
|
|
86
36
|
```typescript
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
37
|
+
ssr: true // 전체 SSR (기본값)
|
|
38
|
+
ssr: false // 클라이언트만
|
|
39
|
+
ssr: 'data-only' // 데이터만 서버, 렌더링은 클라이언트
|
|
90
40
|
```
|
|
91
41
|
|
|
92
42
|
## Server Routes (API)
|
|
93
43
|
|
|
94
|
-
### 기본 API 라우트
|
|
95
|
-
|
|
96
44
|
```typescript
|
|
97
|
-
// routes/api/hello.ts
|
|
98
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
99
|
-
|
|
100
45
|
export const Route = createFileRoute('/api/hello')({
|
|
101
46
|
server: {
|
|
102
47
|
handlers: {
|
|
103
|
-
GET: async (
|
|
104
|
-
return new Response('Hello, World!')
|
|
105
|
-
},
|
|
48
|
+
GET: async () => new Response('Hello'),
|
|
106
49
|
POST: async ({ request }) => {
|
|
107
50
|
const body = await request.json()
|
|
108
|
-
return
|
|
51
|
+
return json({ name: body.name })
|
|
109
52
|
},
|
|
110
53
|
},
|
|
111
54
|
},
|
|
112
55
|
})
|
|
113
56
|
```
|
|
114
57
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
```typescript
|
|
118
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
119
|
-
import { json } from '@tanstack/react-start'
|
|
120
|
-
|
|
121
|
-
export const Route = createFileRoute('/api/users')({
|
|
122
|
-
server: {
|
|
123
|
-
handlers: {
|
|
124
|
-
GET: async ({ request }) => {
|
|
125
|
-
const users = await getUsers()
|
|
126
|
-
return json({ users })
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
})
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
## 라우트 파일 구조
|
|
58
|
+
## 구조
|
|
134
59
|
|
|
135
60
|
```
|
|
136
61
|
routes/
|
|
137
|
-
├── __root.tsx
|
|
138
|
-
├── index.tsx
|
|
139
|
-
├──
|
|
140
|
-
├──
|
|
141
|
-
│ ├── index.tsx → /users
|
|
142
|
-
│ └── $id.tsx → /users/:id
|
|
143
|
-
├── posts/
|
|
144
|
-
│ ├── index.tsx → /posts
|
|
145
|
-
│ ├── $postId.tsx → /posts/:postId
|
|
146
|
-
│ └── new.tsx → /posts/new
|
|
147
|
-
└── api/
|
|
148
|
-
├── hello.ts → /api/hello
|
|
149
|
-
└── users.ts → /api/users
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
## Catch-All 라우트
|
|
153
|
-
|
|
154
|
-
```tsx
|
|
155
|
-
// routes/$.tsx - 모든 매칭되지 않는 경로 처리
|
|
156
|
-
export const Route = createFileRoute('/$')({
|
|
157
|
-
component: NotFoundPage,
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
function NotFoundPage() {
|
|
161
|
-
return <h1>404 - Page Not Found</h1>
|
|
162
|
-
}
|
|
62
|
+
├── __root.tsx → Root layout
|
|
63
|
+
├── index.tsx → /
|
|
64
|
+
├── users/$id.tsx → /users/:id
|
|
65
|
+
├── $.tsx → Catch-all (404)
|
|
163
66
|
```
|
|
@@ -1,287 +1,75 @@
|
|
|
1
1
|
# TanStack Start - Server Functions
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Server Functions는 서버에서만 실행되는 타입 안전한 함수입니다.
|
|
3
|
+
서버에서만 실행되는 타입 안전한 함수.
|
|
6
4
|
|
|
7
5
|
## ⚠️ 필수: TanStack Query 사용
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
❌ 금지: Server Function 직접 호출
|
|
13
|
-
✅ 필수: useQuery/useMutation과 함께 사용
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
**이유**:
|
|
17
|
-
- 자동 캐싱 및 중복 요청 제거
|
|
18
|
-
- 로딩/에러 상태 관리
|
|
19
|
-
- 자동 재시도 및 백그라운드 갱신
|
|
20
|
-
- invalidateQueries로 일관된 데이터 동기화
|
|
7
|
+
클라이언트 호출 시 반드시 useQuery/useMutation 사용.
|
|
8
|
+
- 자동 캐싱, 중복 요청 제거, 로딩/에러 상태 관리, invalidateQueries 동기화
|
|
21
9
|
|
|
22
|
-
## 기본
|
|
10
|
+
## 기본 패턴
|
|
23
11
|
|
|
24
12
|
```typescript
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// GET 요청 (데이터 조회)
|
|
13
|
+
// GET
|
|
28
14
|
export const getUsers = createServerFn({ method: 'GET' })
|
|
29
|
-
.handler(async () =>
|
|
30
|
-
return prisma.user.findMany()
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
// POST 요청 (데이터 생성/수정)
|
|
34
|
-
export const createUser = createServerFn({ method: 'POST' })
|
|
35
|
-
.handler(async () => {
|
|
36
|
-
return { success: true }
|
|
37
|
-
})
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Input Validation
|
|
41
|
-
|
|
42
|
-
### 기본 Validator
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
import { createServerFn } from '@tanstack/react-start'
|
|
46
|
-
|
|
47
|
-
export const createUser = createServerFn({ method: 'POST' })
|
|
48
|
-
.inputValidator((data: { email: string; name: string }) => data)
|
|
49
|
-
.handler(async ({ data }) => {
|
|
50
|
-
// data는 타입 안전함
|
|
51
|
-
return prisma.user.create({ data })
|
|
52
|
-
})
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### Zod Validation 사용
|
|
56
|
-
|
|
57
|
-
```typescript
|
|
58
|
-
import { createServerFn } from '@tanstack/react-start'
|
|
59
|
-
import { zodValidator } from '@tanstack/react-start/validators'
|
|
60
|
-
import { z } from 'zod'
|
|
15
|
+
.handler(async () => prisma.user.findMany())
|
|
61
16
|
|
|
17
|
+
// POST + Zod Validation
|
|
62
18
|
const createUserSchema = z.object({
|
|
63
|
-
email: z.
|
|
19
|
+
email: z.email(),
|
|
64
20
|
name: z.string().min(1).max(100),
|
|
65
|
-
bio: z.string().max(500).optional(),
|
|
66
21
|
})
|
|
67
22
|
|
|
68
23
|
export const createUser = createServerFn({ method: 'POST' })
|
|
69
24
|
.inputValidator(zodValidator(createUserSchema))
|
|
70
|
-
.handler(async ({ data }) => {
|
|
71
|
-
return prisma.user.create({ data })
|
|
72
|
-
})
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## FormData 처리
|
|
76
|
-
|
|
77
|
-
```typescript
|
|
78
|
-
import { createServerFn } from '@tanstack/react-start'
|
|
79
|
-
|
|
80
|
-
export const submitForm = createServerFn({ method: 'POST' })
|
|
81
|
-
.inputValidator((formData: FormData) => {
|
|
82
|
-
const name = formData.get('name') as string
|
|
83
|
-
const email = formData.get('email') as string
|
|
84
|
-
return { name, email }
|
|
85
|
-
})
|
|
86
|
-
.handler(async ({ data }) => {
|
|
87
|
-
return prisma.user.create({ data })
|
|
88
|
-
})
|
|
25
|
+
.handler(async ({ data }) => prisma.user.create({ data }))
|
|
89
26
|
```
|
|
90
27
|
|
|
91
|
-
## 컴포넌트에서 호출
|
|
92
|
-
|
|
93
|
-
### ✅ 올바른 패턴: useQuery 사용 (데이터 조회)
|
|
28
|
+
## 컴포넌트에서 호출
|
|
94
29
|
|
|
95
30
|
```tsx
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
queryKey: ['posts'],
|
|
102
|
-
queryFn: () => getServerPosts(),
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
if (isLoading) return <div>Loading...</div>
|
|
106
|
-
if (error) return <div>Error: {error.message}</div>
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<ul>
|
|
110
|
-
{data?.map((post) => (
|
|
111
|
-
<li key={post.id}>{post.title}</li>
|
|
112
|
-
))}
|
|
113
|
-
</ul>
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### ✅ 올바른 패턴: useMutation 사용 (데이터 변경)
|
|
119
|
-
|
|
120
|
-
```tsx
|
|
121
|
-
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
122
|
-
import { createPost, deletePost } from '@/lib/server-functions'
|
|
123
|
-
|
|
124
|
-
function PostForm() {
|
|
125
|
-
const queryClient = useQueryClient()
|
|
126
|
-
|
|
127
|
-
const mutation = useMutation({
|
|
128
|
-
mutationFn: (data: { title: string; content: string }) => createPost({ data }),
|
|
129
|
-
onSuccess: () => {
|
|
130
|
-
// 관련 쿼리 무효화로 데이터 동기화
|
|
131
|
-
queryClient.invalidateQueries({ queryKey: ['posts'] })
|
|
132
|
-
},
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
136
|
-
e.preventDefault()
|
|
137
|
-
const formData = new FormData(e.currentTarget)
|
|
138
|
-
mutation.mutate({
|
|
139
|
-
title: formData.get('title') as string,
|
|
140
|
-
content: formData.get('content') as string,
|
|
141
|
-
})
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return (
|
|
145
|
-
<form onSubmit={handleSubmit}>
|
|
146
|
-
<input name="title" required />
|
|
147
|
-
<textarea name="content" required />
|
|
148
|
-
<button type="submit" disabled={mutation.isPending}>
|
|
149
|
-
{mutation.isPending ? '저장 중...' : '저장'}
|
|
150
|
-
</button>
|
|
151
|
-
</form>
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
### ❌ 금지: Server Function 직접 호출
|
|
157
|
-
|
|
158
|
-
```tsx
|
|
159
|
-
// ❌ 이렇게 하지 마세요!
|
|
160
|
-
function BadExample() {
|
|
161
|
-
const [posts, setPosts] = useState([])
|
|
162
|
-
const [loading, setLoading] = useState(false)
|
|
163
|
-
|
|
164
|
-
useEffect(() => {
|
|
165
|
-
setLoading(true)
|
|
166
|
-
getPosts()
|
|
167
|
-
.then(setPosts)
|
|
168
|
-
.finally(() => setLoading(false))
|
|
169
|
-
}, [])
|
|
170
|
-
|
|
171
|
-
// 문제점:
|
|
172
|
-
// - 중복 요청 발생 가능
|
|
173
|
-
// - 캐싱 없음
|
|
174
|
-
// - 에러 처리 수동
|
|
175
|
-
// - 다른 컴포넌트와 데이터 동기화 안됨
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
## ⚠️ 함수 분리 시 유의사항
|
|
180
|
-
|
|
181
|
-
Server Function 내부 로직을 별도 함수로 분리할 때 반드시 아래 규칙을 따르세요.
|
|
31
|
+
// ✅ useQuery (조회)
|
|
32
|
+
const { data, isLoading } = useQuery({
|
|
33
|
+
queryKey: ['posts'],
|
|
34
|
+
queryFn: () => getServerPosts(),
|
|
35
|
+
})
|
|
182
36
|
|
|
183
|
-
|
|
37
|
+
// ✅ useMutation (변경)
|
|
38
|
+
const mutation = useMutation({
|
|
39
|
+
mutationFn: createPost,
|
|
40
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
|
|
41
|
+
})
|
|
184
42
|
|
|
185
|
-
|
|
186
|
-
1. 분리한 함수는 createServerFn 내부에서만 호출
|
|
187
|
-
2. 분리한 함수는 createServerFn으로 감싸지 않음
|
|
188
|
-
3. 분리한 함수는 index.ts에서 export 금지 (프론트엔드 import 방지)
|
|
43
|
+
// ❌ 직접 호출 금지 (캐싱 없음, 동기화 안됨)
|
|
189
44
|
```
|
|
190
45
|
|
|
191
|
-
|
|
46
|
+
## 함수 분리 규칙
|
|
192
47
|
|
|
193
48
|
```typescript
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
197
|
-
// 내부 헬퍼 함수 (export 금지!)
|
|
198
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
199
|
-
const validateUserData = async (email: string) => {
|
|
200
|
-
const existing = await prisma.user.findUnique({ where: { email } })
|
|
201
|
-
if (existing) throw new Error('Email already exists')
|
|
202
|
-
return true
|
|
203
|
-
}
|
|
49
|
+
// 내부 헬퍼 (export 금지!)
|
|
50
|
+
const validateUserData = async (email: string) => { ... }
|
|
204
51
|
|
|
205
|
-
const sendWelcomeEmail = async (userId: string, email: string) => {
|
|
206
|
-
await emailService.send({
|
|
207
|
-
to: email,
|
|
208
|
-
template: 'welcome',
|
|
209
|
-
data: { userId },
|
|
210
|
-
})
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
214
52
|
// Server Function (export 가능)
|
|
215
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
216
53
|
export const createUser = createServerFn({ method: 'POST' })
|
|
217
54
|
.inputValidator(createUserSchema)
|
|
218
55
|
.handler(async ({ data }) => {
|
|
219
|
-
// 내부 헬퍼 함수 호출
|
|
220
56
|
await validateUserData(data.email)
|
|
221
|
-
|
|
222
|
-
const user = await prisma.user.create({ data })
|
|
223
|
-
|
|
224
|
-
// 내부 헬퍼 함수 호출
|
|
225
|
-
await sendWelcomeEmail(user.id, user.email)
|
|
226
|
-
|
|
227
|
-
return user
|
|
228
|
-
})
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
```typescript
|
|
232
|
-
// services/user/index.ts
|
|
233
|
-
|
|
234
|
-
// ✅ Server Function만 export
|
|
235
|
-
export { createUser, updateUser, deleteUser } from './mutations'
|
|
236
|
-
export { getUsers, getUserById } from './queries'
|
|
237
|
-
|
|
238
|
-
// ❌ 내부 헬퍼 함수는 export 금지!
|
|
239
|
-
// export { validateUserData, sendWelcomeEmail } from './mutations'
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
### ❌ 잘못된 패턴
|
|
243
|
-
|
|
244
|
-
```typescript
|
|
245
|
-
// ❌ 분리한 함수를 createServerFn으로 감싸지 마세요
|
|
246
|
-
export const validateUserData = createServerFn({ method: 'POST' })
|
|
247
|
-
.handler(async ({ data }) => {
|
|
248
|
-
// ...
|
|
57
|
+
return prisma.user.create({ data })
|
|
249
58
|
})
|
|
250
59
|
|
|
251
|
-
//
|
|
252
|
-
export
|
|
253
|
-
|
|
254
|
-
}
|
|
60
|
+
// index.ts: Server Function만 export
|
|
61
|
+
export { createUser } from './mutations'
|
|
62
|
+
// ❌ export { validateUserData } 금지
|
|
255
63
|
```
|
|
256
64
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
- **보안**: 내부 로직이 프론트엔드 번들에 포함되지 않음
|
|
260
|
-
- **명확한 API**: Server Function만 외부에 노출되어 API 경계가 명확함
|
|
261
|
-
- **트리 쉐이킹**: export하지 않은 함수는 번들에서 제거됨
|
|
262
|
-
|
|
263
|
-
---
|
|
264
|
-
|
|
265
|
-
## 보안 패턴
|
|
266
|
-
|
|
267
|
-
### 서버 전용 데이터 보호
|
|
65
|
+
## 보안
|
|
268
66
|
|
|
269
67
|
```tsx
|
|
270
|
-
// ❌
|
|
271
|
-
|
|
272
|
-
loader: () => {
|
|
273
|
-
const secret = process.env.SECRET // 클라이언트에 노출!
|
|
274
|
-
return fetch(`/api/users?key=${secret}`)
|
|
275
|
-
},
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
// ✅ 올바른 방법 - 서버 함수 사용
|
|
279
|
-
const getUsersSecurely = createServerFn().handler(() => {
|
|
280
|
-
const secret = process.env.SECRET // 서버에서만 접근
|
|
281
|
-
return fetch(`/api/users?key=${secret}`)
|
|
282
|
-
})
|
|
68
|
+
// ❌ loader에서 환경변수 직접 사용 (노출됨)
|
|
69
|
+
loader: () => { const secret = process.env.SECRET }
|
|
283
70
|
|
|
284
|
-
|
|
285
|
-
|
|
71
|
+
// ✅ Server Function 사용
|
|
72
|
+
const fn = createServerFn().handler(() => {
|
|
73
|
+
const secret = process.env.SECRET // 서버에서만
|
|
286
74
|
})
|
|
287
75
|
```
|