@kood/claude-code 0.1.5 → 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 +105 -259
- 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/guides/best-practices.md +3 -8
- package/templates/tanstack-start/docs/guides/env-setup.md +3 -3
- 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 +13 -113
- 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 -73
- 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 +41 -172
- 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,206 +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
|
-
```
|
|
7
|
+
클라이언트 호출 시 반드시 useQuery/useMutation 사용.
|
|
8
|
+
- 자동 캐싱, 중복 요청 제거, 로딩/에러 상태 관리, invalidateQueries 동기화
|
|
15
9
|
|
|
16
|
-
|
|
17
|
-
- 자동 캐싱 및 중복 요청 제거
|
|
18
|
-
- 로딩/에러 상태 관리
|
|
19
|
-
- 자동 재시도 및 백그라운드 갱신
|
|
20
|
-
- invalidateQueries로 일관된 데이터 동기화
|
|
21
|
-
|
|
22
|
-
## 기본 Server Function
|
|
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
|
-
})
|
|
25
|
+
.handler(async ({ data }) => prisma.user.create({ data }))
|
|
73
26
|
```
|
|
74
27
|
|
|
75
|
-
##
|
|
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
|
-
})
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
## 컴포넌트에서 호출 (TanStack Query 필수)
|
|
92
|
-
|
|
93
|
-
### ✅ 올바른 패턴: useQuery 사용 (데이터 조회)
|
|
28
|
+
## 컴포넌트에서 호출
|
|
94
29
|
|
|
95
30
|
```tsx
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const getPosts = useServerFn(getServerPosts)
|
|
31
|
+
// ✅ useQuery (조회)
|
|
32
|
+
const { data, isLoading } = useQuery({
|
|
33
|
+
queryKey: ['posts'],
|
|
34
|
+
queryFn: () => getServerPosts(),
|
|
35
|
+
})
|
|
102
36
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
})
|
|
37
|
+
// ✅ useMutation (변경)
|
|
38
|
+
const mutation = useMutation({
|
|
39
|
+
mutationFn: createPost,
|
|
40
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
|
|
41
|
+
})
|
|
107
42
|
|
|
108
|
-
|
|
109
|
-
if (error) return <div>Error: {error.message}</div>
|
|
110
|
-
|
|
111
|
-
return (
|
|
112
|
-
<ul>
|
|
113
|
-
{data?.map((post) => (
|
|
114
|
-
<li key={post.id}>{post.title}</li>
|
|
115
|
-
))}
|
|
116
|
-
</ul>
|
|
117
|
-
)
|
|
118
|
-
}
|
|
43
|
+
// ❌ 직접 호출 금지 (캐싱 없음, 동기화 안됨)
|
|
119
44
|
```
|
|
120
45
|
|
|
121
|
-
|
|
46
|
+
## 함수 분리 규칙
|
|
122
47
|
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
import { createPost, deletePost } from '~/lib/server-functions'
|
|
127
|
-
|
|
128
|
-
function PostForm() {
|
|
129
|
-
const queryClient = useQueryClient()
|
|
130
|
-
const createPostFn = useServerFn(createPost)
|
|
131
|
-
|
|
132
|
-
const mutation = useMutation({
|
|
133
|
-
mutationFn: (data: { title: string; content: string }) => createPostFn({ data }),
|
|
134
|
-
onSuccess: () => {
|
|
135
|
-
// 관련 쿼리 무효화로 데이터 동기화
|
|
136
|
-
queryClient.invalidateQueries({ queryKey: ['posts'] })
|
|
137
|
-
},
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
141
|
-
e.preventDefault()
|
|
142
|
-
const formData = new FormData(e.currentTarget)
|
|
143
|
-
mutation.mutate({
|
|
144
|
-
title: formData.get('title') as string,
|
|
145
|
-
content: formData.get('content') as string,
|
|
146
|
-
})
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return (
|
|
150
|
-
<form onSubmit={handleSubmit}>
|
|
151
|
-
<input name="title" required />
|
|
152
|
-
<textarea name="content" required />
|
|
153
|
-
<button type="submit" disabled={mutation.isPending}>
|
|
154
|
-
{mutation.isPending ? '저장 중...' : '저장'}
|
|
155
|
-
</button>
|
|
156
|
-
</form>
|
|
157
|
-
)
|
|
158
|
-
}
|
|
159
|
-
```
|
|
48
|
+
```typescript
|
|
49
|
+
// 내부 헬퍼 (export 금지!)
|
|
50
|
+
const validateUserData = async (email: string) => { ... }
|
|
160
51
|
|
|
161
|
-
|
|
52
|
+
// Server Function (export 가능)
|
|
53
|
+
export const createUser = createServerFn({ method: 'POST' })
|
|
54
|
+
.inputValidator(createUserSchema)
|
|
55
|
+
.handler(async ({ data }) => {
|
|
56
|
+
await validateUserData(data.email)
|
|
57
|
+
return prisma.user.create({ data })
|
|
58
|
+
})
|
|
162
59
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const [posts, setPosts] = useState([])
|
|
167
|
-
const [loading, setLoading] = useState(false)
|
|
168
|
-
|
|
169
|
-
useEffect(() => {
|
|
170
|
-
setLoading(true)
|
|
171
|
-
getPosts()
|
|
172
|
-
.then(setPosts)
|
|
173
|
-
.finally(() => setLoading(false))
|
|
174
|
-
}, [])
|
|
175
|
-
|
|
176
|
-
// 문제점:
|
|
177
|
-
// - 중복 요청 발생 가능
|
|
178
|
-
// - 캐싱 없음
|
|
179
|
-
// - 에러 처리 수동
|
|
180
|
-
// - 다른 컴포넌트와 데이터 동기화 안됨
|
|
181
|
-
}
|
|
60
|
+
// index.ts: Server Function만 export
|
|
61
|
+
export { createUser } from './mutations'
|
|
62
|
+
// ❌ export { validateUserData } 금지
|
|
182
63
|
```
|
|
183
64
|
|
|
184
|
-
## 보안
|
|
185
|
-
|
|
186
|
-
### 서버 전용 데이터 보호
|
|
65
|
+
## 보안
|
|
187
66
|
|
|
188
67
|
```tsx
|
|
189
|
-
// ❌
|
|
190
|
-
|
|
191
|
-
loader: () => {
|
|
192
|
-
const secret = process.env.SECRET // 클라이언트에 노출!
|
|
193
|
-
return fetch(`/api/users?key=${secret}`)
|
|
194
|
-
},
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
// ✅ 올바른 방법 - 서버 함수 사용
|
|
198
|
-
const getUsersSecurely = createServerFn().handler(() => {
|
|
199
|
-
const secret = process.env.SECRET // 서버에서만 접근
|
|
200
|
-
return fetch(`/api/users?key=${secret}`)
|
|
201
|
-
})
|
|
68
|
+
// ❌ loader에서 환경변수 직접 사용 (노출됨)
|
|
69
|
+
loader: () => { const secret = process.env.SECRET }
|
|
202
70
|
|
|
203
|
-
|
|
204
|
-
|
|
71
|
+
// ✅ Server Function 사용
|
|
72
|
+
const fn = createServerFn().handler(() => {
|
|
73
|
+
const secret = process.env.SECRET // 서버에서만
|
|
205
74
|
})
|
|
206
75
|
```
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
# TanStack Start - 설치 및 설정
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## 패키지 설치
|
|
3
|
+
## 설치
|
|
6
4
|
|
|
7
5
|
```bash
|
|
8
6
|
yarn add @tanstack/react-start @tanstack/react-router vinxi
|
|
9
7
|
yarn add -D vite @vitejs/plugin-react vite-tsconfig-paths
|
|
10
8
|
```
|
|
11
9
|
|
|
12
|
-
##
|
|
10
|
+
## 설정
|
|
13
11
|
|
|
14
12
|
```typescript
|
|
15
13
|
// vite.config.ts
|
|
@@ -19,19 +17,11 @@ import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
|
|
19
17
|
import viteReact from '@vitejs/plugin-react'
|
|
20
18
|
|
|
21
19
|
export default defineConfig({
|
|
22
|
-
server: {
|
|
23
|
-
|
|
24
|
-
},
|
|
25
|
-
plugins: [
|
|
26
|
-
tsConfigPaths(),
|
|
27
|
-
tanstackStart(),
|
|
28
|
-
viteReact(),
|
|
29
|
-
],
|
|
20
|
+
server: { port: 3000 },
|
|
21
|
+
plugins: [tsConfigPaths(), tanstackStart(), viteReact()],
|
|
30
22
|
})
|
|
31
23
|
```
|
|
32
24
|
|
|
33
|
-
## TypeScript 설정
|
|
34
|
-
|
|
35
25
|
```json
|
|
36
26
|
// tsconfig.json
|
|
37
27
|
{
|
|
@@ -40,35 +30,12 @@ export default defineConfig({
|
|
|
40
30
|
"module": "ESNext",
|
|
41
31
|
"moduleResolution": "bundler",
|
|
42
32
|
"strict": true,
|
|
43
|
-
"esModuleInterop": true,
|
|
44
|
-
"skipLibCheck": true,
|
|
45
33
|
"jsx": "react-jsx",
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
"~/*": ["./src/*"]
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
"include": ["src/**/*"]
|
|
34
|
+
"paths": { "@/*": ["./src/*"] }
|
|
35
|
+
}
|
|
52
36
|
}
|
|
53
37
|
```
|
|
54
38
|
|
|
55
|
-
## 프로젝트 구조
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
project/
|
|
59
|
-
├── src/
|
|
60
|
-
│ ├── routes/
|
|
61
|
-
│ │ ├── __root.tsx
|
|
62
|
-
│ │ ├── index.tsx
|
|
63
|
-
│ │ └── about.tsx
|
|
64
|
-
│ ├── lib/
|
|
65
|
-
│ │ └── server-functions.ts
|
|
66
|
-
│ └── start.ts
|
|
67
|
-
├── vite.config.ts
|
|
68
|
-
├── tsconfig.json
|
|
69
|
-
└── package.json
|
|
70
|
-
```
|
|
71
|
-
|
|
72
39
|
## 환경 변수 검증
|
|
73
40
|
|
|
74
41
|
```typescript
|