@kood/claude-code 0.1.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.d.ts +2 -0
- package/dist/index.js +297 -0
- package/package.json +47 -0
- package/templates/hono/CLAUDE.md +376 -0
- package/templates/hono/docs/deployment/cloudflare.md +328 -0
- package/templates/hono/docs/deployment/index.md +291 -0
- package/templates/hono/docs/git/index.md +180 -0
- package/templates/hono/docs/library/hono/error-handling.md +400 -0
- package/templates/hono/docs/library/hono/index.md +241 -0
- package/templates/hono/docs/library/hono/middleware.md +334 -0
- package/templates/hono/docs/library/hono/rpc.md +454 -0
- package/templates/hono/docs/library/hono/validation.md +328 -0
- package/templates/hono/docs/library/prisma/index.md +427 -0
- package/templates/hono/docs/library/zod/index.md +413 -0
- package/templates/hono/docs/mcp/context7.md +106 -0
- package/templates/hono/docs/mcp/index.md +94 -0
- package/templates/hono/docs/mcp/sequential-thinking.md +101 -0
- package/templates/hono/docs/mcp/sgrep.md +105 -0
- package/templates/hono/docs/skills/gemini-review/SKILL.md +220 -0
- package/templates/hono/docs/skills/gemini-review/references/checklists.md +136 -0
- package/templates/hono/docs/skills/gemini-review/references/prompt-templates.md +303 -0
- package/templates/tanstack-start/CLAUDE.md +279 -0
- package/templates/tanstack-start/docs/architecture/architecture.md +547 -0
- package/templates/tanstack-start/docs/deployment/cloudflare.md +346 -0
- package/templates/tanstack-start/docs/deployment/index.md +102 -0
- package/templates/tanstack-start/docs/deployment/nitro.md +211 -0
- package/templates/tanstack-start/docs/deployment/railway.md +364 -0
- package/templates/tanstack-start/docs/deployment/vercel.md +287 -0
- package/templates/tanstack-start/docs/design/accessibility.md +433 -0
- package/templates/tanstack-start/docs/design/color.md +235 -0
- package/templates/tanstack-start/docs/design/components.md +409 -0
- package/templates/tanstack-start/docs/design/index.md +107 -0
- package/templates/tanstack-start/docs/design/safe-area.md +317 -0
- package/templates/tanstack-start/docs/design/spacing.md +341 -0
- package/templates/tanstack-start/docs/design/tailwind-setup.md +470 -0
- package/templates/tanstack-start/docs/design/typography.md +324 -0
- package/templates/tanstack-start/docs/git/index.md +203 -0
- package/templates/tanstack-start/docs/guides/best-practices.md +753 -0
- package/templates/tanstack-start/docs/guides/getting-started.md +304 -0
- package/templates/tanstack-start/docs/guides/husky-lint-staged.md +303 -0
- package/templates/tanstack-start/docs/guides/prettier.md +189 -0
- package/templates/tanstack-start/docs/guides/project-templates.md +710 -0
- package/templates/tanstack-start/docs/library/better-auth/2fa.md +136 -0
- package/templates/tanstack-start/docs/library/better-auth/advanced.md +138 -0
- package/templates/tanstack-start/docs/library/better-auth/index.md +83 -0
- package/templates/tanstack-start/docs/library/better-auth/plugins.md +111 -0
- package/templates/tanstack-start/docs/library/better-auth/session.md +127 -0
- package/templates/tanstack-start/docs/library/better-auth/setup.md +123 -0
- package/templates/tanstack-start/docs/library/prisma/crud.md +218 -0
- package/templates/tanstack-start/docs/library/prisma/index.md +165 -0
- package/templates/tanstack-start/docs/library/prisma/relations.md +191 -0
- package/templates/tanstack-start/docs/library/prisma/schema.md +177 -0
- package/templates/tanstack-start/docs/library/prisma/setup.md +156 -0
- package/templates/tanstack-start/docs/library/prisma/transactions.md +140 -0
- package/templates/tanstack-start/docs/library/tanstack-query/index.md +146 -0
- package/templates/tanstack-start/docs/library/tanstack-query/invalidation.md +146 -0
- package/templates/tanstack-start/docs/library/tanstack-query/optimistic-updates.md +196 -0
- package/templates/tanstack-start/docs/library/tanstack-query/setup.md +110 -0
- package/templates/tanstack-start/docs/library/tanstack-query/use-mutation.md +170 -0
- package/templates/tanstack-start/docs/library/tanstack-query/use-query.md +173 -0
- package/templates/tanstack-start/docs/library/tanstack-start/auth-patterns.md +171 -0
- package/templates/tanstack-start/docs/library/tanstack-start/index.md +114 -0
- package/templates/tanstack-start/docs/library/tanstack-start/middleware.md +142 -0
- package/templates/tanstack-start/docs/library/tanstack-start/routing.md +163 -0
- package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +128 -0
- package/templates/tanstack-start/docs/library/tanstack-start/setup.md +85 -0
- package/templates/tanstack-start/docs/library/zod/basic-types.md +186 -0
- package/templates/tanstack-start/docs/library/zod/complex-types.md +204 -0
- package/templates/tanstack-start/docs/library/zod/index.md +186 -0
- package/templates/tanstack-start/docs/library/zod/transforms.md +174 -0
- package/templates/tanstack-start/docs/library/zod/validation.md +208 -0
- package/templates/tanstack-start/docs/mcp/context7.md +204 -0
- package/templates/tanstack-start/docs/mcp/index.md +116 -0
- package/templates/tanstack-start/docs/mcp/sequential-thinking.md +180 -0
- package/templates/tanstack-start/docs/mcp/sgrep.md +174 -0
- package/templates/tanstack-start/docs/skills/gemini-review/SKILL.md +220 -0
- package/templates/tanstack-start/docs/skills/gemini-review/references/checklists.md +150 -0
- package/templates/tanstack-start/docs/skills/gemini-review/references/prompt-templates.md +293 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# TanStack Query - useQuery
|
|
2
|
+
|
|
3
|
+
> **상위 문서**: [TanStack Query](./index.md)
|
|
4
|
+
|
|
5
|
+
## 기본 사용법
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { useQuery } from '@tanstack/react-query'
|
|
9
|
+
|
|
10
|
+
function Todos() {
|
|
11
|
+
const { data, isLoading, error } = useQuery({
|
|
12
|
+
queryKey: ['todos'],
|
|
13
|
+
queryFn: getTodos,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
if (isLoading) return <div>Loading...</div>
|
|
17
|
+
if (error) return <div>Error: {error.message}</div>
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<ul>
|
|
21
|
+
{data?.map((todo) => (
|
|
22
|
+
<li key={todo.id}>{todo.title}</li>
|
|
23
|
+
))}
|
|
24
|
+
</ul>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## useQuery 반환 값
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
const {
|
|
33
|
+
data, // 쿼리 결과 데이터
|
|
34
|
+
error, // 에러 객체
|
|
35
|
+
isLoading, // 첫 로딩 중
|
|
36
|
+
isFetching, // 백그라운드 페칭 중
|
|
37
|
+
isError, // 에러 상태
|
|
38
|
+
isSuccess, // 성공 상태
|
|
39
|
+
isPending, // 데이터 없고 로딩 중
|
|
40
|
+
isStale, // 데이터가 stale 상태
|
|
41
|
+
refetch, // 수동 리페치 함수
|
|
42
|
+
status, // 'pending' | 'error' | 'success'
|
|
43
|
+
fetchStatus, // 'fetching' | 'paused' | 'idle'
|
|
44
|
+
} = useQuery({ queryKey, queryFn })
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## useQuery 옵션
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
const { data } = useQuery({
|
|
51
|
+
queryKey: ['todos'],
|
|
52
|
+
queryFn: fetchTodos,
|
|
53
|
+
|
|
54
|
+
// 캐시 관련
|
|
55
|
+
staleTime: 1000 * 60 * 5, // 5분 동안 fresh
|
|
56
|
+
gcTime: 1000 * 60 * 30, // 30분 후 가비지 컬렉션
|
|
57
|
+
|
|
58
|
+
// 리페치 관련
|
|
59
|
+
refetchOnWindowFocus: true, // 윈도우 포커스 시 리페치
|
|
60
|
+
refetchOnReconnect: true, // 네트워크 재연결 시 리페치
|
|
61
|
+
refetchInterval: 1000 * 60, // 1분마다 리페치
|
|
62
|
+
|
|
63
|
+
// 재시도 관련
|
|
64
|
+
retry: 3, // 실패 시 재시도 횟수
|
|
65
|
+
retryDelay: 1000, // 재시도 딜레이
|
|
66
|
+
|
|
67
|
+
// 조건부 실행
|
|
68
|
+
enabled: !!userId, // false면 쿼리 실행 안 함
|
|
69
|
+
|
|
70
|
+
// 초기 데이터
|
|
71
|
+
initialData: [], // 초기 데이터
|
|
72
|
+
|
|
73
|
+
// 플레이스홀더
|
|
74
|
+
placeholderData: [], // 로딩 중 임시 데이터
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 파라미터가 있는 쿼리
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
function Todo({ todoId }: { todoId: number }) {
|
|
82
|
+
const { data } = useQuery({
|
|
83
|
+
queryKey: ['todo', todoId],
|
|
84
|
+
queryFn: () => fetchTodoById(todoId),
|
|
85
|
+
enabled: !!todoId,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return <div>{data?.title}</div>
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 의존적 쿼리
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
function UserPosts({ userId }: { userId: number }) {
|
|
96
|
+
// 먼저 유저 정보를 가져옴
|
|
97
|
+
const { data: user } = useQuery({
|
|
98
|
+
queryKey: ['user', userId],
|
|
99
|
+
queryFn: () => fetchUserById(userId),
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// 유저 정보가 있어야 포스트를 가져옴
|
|
103
|
+
const { data: posts } = useQuery({
|
|
104
|
+
queryKey: ['posts', user?.id],
|
|
105
|
+
queryFn: () => fetchPostsByUserId(user!.id),
|
|
106
|
+
enabled: !!user?.id,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div>
|
|
111
|
+
<h1>{user?.name}</h1>
|
|
112
|
+
<ul>
|
|
113
|
+
{posts?.map((post) => (
|
|
114
|
+
<li key={post.id}>{post.title}</li>
|
|
115
|
+
))}
|
|
116
|
+
</ul>
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## 병렬 쿼리
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
function Dashboard() {
|
|
126
|
+
const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
|
|
127
|
+
const postsQuery = useQuery({ queryKey: ['posts'], queryFn: fetchPosts })
|
|
128
|
+
|
|
129
|
+
if (usersQuery.isLoading || postsQuery.isLoading) {
|
|
130
|
+
return <div>Loading...</div>
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div>
|
|
135
|
+
<UserList users={usersQuery.data} />
|
|
136
|
+
<PostList posts={postsQuery.data} />
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## useQueries (동적 병렬 쿼리)
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
function UsersList({ userIds }: { userIds: number[] }) {
|
|
146
|
+
const userQueries = useQueries({
|
|
147
|
+
queries: userIds.map((id) => ({
|
|
148
|
+
queryKey: ['user', id],
|
|
149
|
+
queryFn: () => fetchUserById(id),
|
|
150
|
+
})),
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<ul>
|
|
155
|
+
{userQueries.map((query, index) => (
|
|
156
|
+
<li key={userIds[index]}>
|
|
157
|
+
{query.isLoading ? 'Loading...' : query.data?.name}
|
|
158
|
+
</li>
|
|
159
|
+
))}
|
|
160
|
+
</ul>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Select로 데이터 변환
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
const { data } = useQuery({
|
|
169
|
+
queryKey: ['todos'],
|
|
170
|
+
queryFn: fetchTodos,
|
|
171
|
+
select: (todos) => todos.filter((todo) => todo.completed),
|
|
172
|
+
})
|
|
173
|
+
```
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# TanStack Start - 인증 패턴
|
|
2
|
+
|
|
3
|
+
> **상위 문서**: [TanStack Start](./index.md)
|
|
4
|
+
|
|
5
|
+
TanStack Start에서의 인증 구현 패턴입니다.
|
|
6
|
+
|
|
7
|
+
## 로그인
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
11
|
+
import { redirect } from '@tanstack/react-router'
|
|
12
|
+
import bcrypt from 'bcryptjs'
|
|
13
|
+
|
|
14
|
+
export const loginFn = createServerFn({ method: 'POST' })
|
|
15
|
+
.inputValidator((data: { email: string; password: string }) => data)
|
|
16
|
+
.handler(async ({ data }) => {
|
|
17
|
+
const user = await authenticateUser(data.email, data.password)
|
|
18
|
+
|
|
19
|
+
if (!user) {
|
|
20
|
+
return { error: 'Invalid credentials' }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const session = await useAppSession()
|
|
24
|
+
await session.update({
|
|
25
|
+
userId: user.id,
|
|
26
|
+
email: user.email,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
throw redirect({ to: '/dashboard' })
|
|
30
|
+
})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 로그아웃
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
export const logoutFn = createServerFn({ method: 'POST' })
|
|
37
|
+
.handler(async () => {
|
|
38
|
+
const session = await useAppSession()
|
|
39
|
+
await session.clear()
|
|
40
|
+
throw redirect({ to: '/' })
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 현재 사용자 조회
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
export const getCurrentUserFn = createServerFn({ method: 'GET' })
|
|
48
|
+
.handler(async () => {
|
|
49
|
+
const session = await useAppSession()
|
|
50
|
+
const userId = session.data.userId
|
|
51
|
+
|
|
52
|
+
if (!userId) {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return await getUserById(userId)
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 회원가입
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
export const registerFn = createServerFn({ method: 'POST' })
|
|
64
|
+
.inputValidator((data: { email: string; password: string; name: string }) => data)
|
|
65
|
+
.handler(async ({ data }) => {
|
|
66
|
+
const existingUser = await getUserByEmail(data.email)
|
|
67
|
+
if (existingUser) {
|
|
68
|
+
return { error: 'User already exists' }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const hashedPassword = await bcrypt.hash(data.password, 12)
|
|
72
|
+
|
|
73
|
+
const user = await createUser({
|
|
74
|
+
email: data.email,
|
|
75
|
+
password: hashedPassword,
|
|
76
|
+
name: data.name,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const session = await useAppSession()
|
|
80
|
+
await session.update({ userId: user.id })
|
|
81
|
+
|
|
82
|
+
return { success: true, user: { id: user.id, email: user.email } }
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 인증 미들웨어
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { createMiddleware } from '@tanstack/react-start'
|
|
90
|
+
import { redirect } from '@tanstack/react-router'
|
|
91
|
+
|
|
92
|
+
export const authMiddleware = createMiddleware({ type: 'function' })
|
|
93
|
+
.server(async ({ next }) => {
|
|
94
|
+
const session = await useAppSession()
|
|
95
|
+
|
|
96
|
+
if (!session.data.userId) {
|
|
97
|
+
throw redirect({ to: '/login' })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const user = await getUserById(session.data.userId)
|
|
101
|
+
|
|
102
|
+
return next({ context: { user } })
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## 보호된 Server Function
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
export const getProtectedData = createServerFn({ method: 'GET' })
|
|
110
|
+
.middleware([authMiddleware])
|
|
111
|
+
.handler(async ({ context }) => {
|
|
112
|
+
// context.user 사용 가능
|
|
113
|
+
return {
|
|
114
|
+
message: `Hello, ${context.user.name}!`,
|
|
115
|
+
data: await getPrivateData(context.user.id),
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## 보호된 라우트
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
// routes/dashboard.tsx
|
|
124
|
+
import { createFileRoute, redirect } from '@tanstack/react-router'
|
|
125
|
+
|
|
126
|
+
export const Route = createFileRoute('/dashboard')({
|
|
127
|
+
beforeLoad: async () => {
|
|
128
|
+
const user = await getCurrentUserFn()
|
|
129
|
+
|
|
130
|
+
if (!user) {
|
|
131
|
+
throw redirect({ to: '/login' })
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { user }
|
|
135
|
+
},
|
|
136
|
+
component: DashboardPage,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
function DashboardPage() {
|
|
140
|
+
const { user } = Route.useRouteContext()
|
|
141
|
+
return <h1>Welcome, {user.name}!</h1>
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Better Auth 통합
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// lib/auth.ts
|
|
149
|
+
import { betterAuth } from 'better-auth'
|
|
150
|
+
import { prismaAdapter } from 'better-auth/adapters/prisma'
|
|
151
|
+
import { prisma } from './prisma'
|
|
152
|
+
|
|
153
|
+
export const auth = betterAuth({
|
|
154
|
+
database: prismaAdapter(prisma),
|
|
155
|
+
emailAndPassword: {
|
|
156
|
+
enabled: true,
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Server Function에서 사용
|
|
161
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
162
|
+
import { auth } from '~/lib/auth'
|
|
163
|
+
|
|
164
|
+
export const getSession = createServerFn({ method: 'GET' })
|
|
165
|
+
.handler(async ({ request }) => {
|
|
166
|
+
const session = await auth.api.getSession({
|
|
167
|
+
headers: request.headers,
|
|
168
|
+
})
|
|
169
|
+
return session
|
|
170
|
+
})
|
|
171
|
+
```
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# TanStack Start
|
|
2
|
+
|
|
3
|
+
> **Version**: 1.x | Full-stack React Framework
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ⛔ 필수 규칙
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
❌ /api 라우터 사용 금지 → Server Functions 사용
|
|
11
|
+
❌ handler 내부 수동 검증 금지 → inputValidator 사용
|
|
12
|
+
❌ handler 내부 수동 인증 체크 금지 → middleware 사용
|
|
13
|
+
✅ POST/PUT/PATCH에는 반드시 inputValidator 추가
|
|
14
|
+
✅ 인증 필요 시 반드시 middleware 추가
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🚀 Quick Reference (복사용)
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// ✅ Server Function (GET + 인증)
|
|
23
|
+
export const getUsers = createServerFn({ method: 'GET' })
|
|
24
|
+
.middleware([authMiddleware]) // ⭐ 인증 미들웨어
|
|
25
|
+
.handler(async () => prisma.user.findMany())
|
|
26
|
+
|
|
27
|
+
// ✅ Server Function (POST + Validation + 인증)
|
|
28
|
+
export const createUser = createServerFn({ method: 'POST' })
|
|
29
|
+
.middleware([authMiddleware]) // ⭐ 인증 미들웨어
|
|
30
|
+
.inputValidator(createUserSchema) // ⭐ Zod 스키마
|
|
31
|
+
.handler(async ({ data }) => prisma.user.create({ data }))
|
|
32
|
+
|
|
33
|
+
// Route with Loader
|
|
34
|
+
export const Route = createFileRoute('/users')({
|
|
35
|
+
component: UsersPage,
|
|
36
|
+
loader: async () => ({ users: await getUsers() }),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Component에서 Loader 데이터 사용
|
|
40
|
+
const UsersPage = (): JSX.Element => {
|
|
41
|
+
const { users } = Route.useLoaderData()
|
|
42
|
+
return <div>{/* render */}</div>
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 라우트 파일 구조
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
routes/
|
|
50
|
+
├── __root.tsx # Root layout
|
|
51
|
+
├── index.tsx # /
|
|
52
|
+
├── about.tsx # /about
|
|
53
|
+
└── users/
|
|
54
|
+
├── index.tsx # /users
|
|
55
|
+
├── $id.tsx # /users/:id
|
|
56
|
+
├── -components/ # 페이지 전용 컴포넌트
|
|
57
|
+
└── -functions/ # 페이지 전용 Server Functions ⭐
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Server Functions 위치
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
공통 함수 (여러 라우트) → @/functions/
|
|
64
|
+
라우트 전용 함수 → routes/[경로]/-functions/
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 문서 구조
|
|
70
|
+
|
|
71
|
+
- [설치 및 설정](./setup.md) - 패키지 설치, Vite 설정, TypeScript 설정
|
|
72
|
+
- [Server Functions](./server-functions.md) - 서버 함수 정의 및 사용법
|
|
73
|
+
- [Middleware](./middleware.md) - 미들웨어 정의 및 적용
|
|
74
|
+
- [Routing](./routing.md) - 파일 기반 라우팅, 동적 라우트, SSR
|
|
75
|
+
- [인증 패턴](./auth-patterns.md) - 로그인, 로그아웃, 세션 관리
|
|
76
|
+
|
|
77
|
+
## 빠른 시작
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
yarn add @tanstack/react-start @tanstack/react-router vinxi
|
|
81
|
+
yarn add -D vite @vitejs/plugin-react vite-tsconfig-paths
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 핵심 개념
|
|
85
|
+
|
|
86
|
+
### Server Functions
|
|
87
|
+
서버에서만 실행되는 타입 안전한 함수를 정의합니다.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
91
|
+
|
|
92
|
+
export const getData = createServerFn({ method: 'GET' })
|
|
93
|
+
.handler(async () => {
|
|
94
|
+
return { message: 'Hello from server' }
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### File-based Routing
|
|
99
|
+
파일 시스템 기반의 자동 라우팅을 지원합니다.
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
routes/
|
|
103
|
+
├── index.tsx → /
|
|
104
|
+
├── about.tsx → /about
|
|
105
|
+
└── users/
|
|
106
|
+
├── index.tsx → /users
|
|
107
|
+
└── $id.tsx → /users/:id
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 참고 자료
|
|
111
|
+
|
|
112
|
+
- [TanStack Start 공식 문서](https://tanstack.com/start/latest)
|
|
113
|
+
- [TanStack Router 문서](https://tanstack.com/router/latest)
|
|
114
|
+
- [Vinxi 문서](https://vinxi.dev)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# TanStack Start - Middleware
|
|
2
|
+
|
|
3
|
+
> **상위 문서**: [TanStack Start](./index.md)
|
|
4
|
+
|
|
5
|
+
미들웨어는 Server Function 및 라우트 핸들러에 공통 로직을 적용합니다.
|
|
6
|
+
|
|
7
|
+
## Server Function Middleware
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { createMiddleware, createServerFn } from '@tanstack/react-start'
|
|
11
|
+
|
|
12
|
+
// 미들웨어 정의
|
|
13
|
+
const loggingMiddleware = createMiddleware({ type: 'function' })
|
|
14
|
+
.client(() => {
|
|
15
|
+
console.log('Client: Server function called')
|
|
16
|
+
})
|
|
17
|
+
.server(({ next }) => {
|
|
18
|
+
console.log('Server: Processing request')
|
|
19
|
+
return next()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// 미들웨어 적용
|
|
23
|
+
const fn = createServerFn()
|
|
24
|
+
.middleware([loggingMiddleware])
|
|
25
|
+
.handler(async () => {
|
|
26
|
+
return { message: 'Hello' }
|
|
27
|
+
})
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Zod Validation Middleware
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { createMiddleware } from '@tanstack/react-start'
|
|
34
|
+
import { zodValidator } from '@tanstack/react-start/validators'
|
|
35
|
+
import { z } from 'zod'
|
|
36
|
+
|
|
37
|
+
const mySchema = z.object({
|
|
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()
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Global Request Middleware
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// src/start.ts
|
|
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
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Global Server Function Middleware
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// src/start.ts
|
|
71
|
+
import { createStart } from '@tanstack/react-start'
|
|
72
|
+
import { loggingMiddleware } from './middleware'
|
|
73
|
+
|
|
74
|
+
export const startInstance = createStart(() => {
|
|
75
|
+
return {
|
|
76
|
+
functionMiddleware: [loggingMiddleware],
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Route-Level Middleware
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
export const Route = createFileRoute('/hello')({
|
|
85
|
+
server: {
|
|
86
|
+
middleware: [authMiddleware, loggerMiddleware], // 모든 핸들러에 적용
|
|
87
|
+
handlers: {
|
|
88
|
+
GET: async ({ request }) => {
|
|
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
|
+
})
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Handler-Specific Middleware
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
export const Route = createFileRoute('/hello')({
|
|
104
|
+
server: {
|
|
105
|
+
handlers: ({ createHandlers }) =>
|
|
106
|
+
createHandlers({
|
|
107
|
+
GET: {
|
|
108
|
+
middleware: [loggerMiddleware], // GET에만 적용
|
|
109
|
+
handler: async ({ request }) => {
|
|
110
|
+
return new Response('Hello, World! from ' + request.url)
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
```
|
|
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
|
+
```
|