@kood/claude-code 0.1.2 → 0.1.4
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 +129 -5
- package/package.json +2 -2
- package/templates/hono/CLAUDE.md +20 -2
- package/templates/hono/docs/architecture/architecture.md +909 -0
- package/templates/hono/docs/commands/git.md +275 -0
- package/templates/hono/docs/deployment/cloudflare.md +527 -190
- package/templates/hono/docs/deployment/docker.md +514 -0
- package/templates/hono/docs/deployment/index.md +179 -214
- package/templates/hono/docs/deployment/railway.md +416 -0
- package/templates/hono/docs/deployment/vercel.md +567 -0
- package/templates/hono/docs/library/ai-sdk/index.md +427 -0
- package/templates/hono/docs/library/ai-sdk/openrouter.md +479 -0
- package/templates/hono/docs/library/ai-sdk/providers.md +468 -0
- package/templates/hono/docs/library/ai-sdk/streaming.md +447 -0
- package/templates/hono/docs/library/ai-sdk/structured-output.md +493 -0
- package/templates/hono/docs/library/ai-sdk/tools.md +513 -0
- package/templates/hono/docs/library/hono/env-setup.md +458 -0
- package/templates/hono/docs/library/hono/index.md +1 -3
- package/templates/hono/docs/library/pino/index.md +437 -0
- package/templates/hono/docs/library/prisma/cloudflare-d1.md +503 -0
- package/templates/hono/docs/library/prisma/config.md +362 -0
- package/templates/hono/docs/library/prisma/index.md +86 -13
- package/templates/hono/docs/skills/gemini-review/SKILL.md +116 -116
- package/templates/hono/docs/skills/gemini-review/references/checklists.md +125 -125
- package/templates/hono/docs/skills/gemini-review/references/prompt-templates.md +191 -191
- package/templates/npx/CLAUDE.md +309 -0
- package/templates/npx/docs/commands/git.md +275 -0
- package/templates/npx/docs/library/commander/index.md +164 -0
- package/templates/npx/docs/library/fs-extra/index.md +171 -0
- package/templates/npx/docs/library/prompts/index.md +253 -0
- package/templates/npx/docs/mcp/index.md +60 -0
- package/templates/npx/docs/skills/gemini-review/SKILL.md +220 -0
- package/templates/npx/docs/skills/gemini-review/references/checklists.md +134 -0
- package/templates/npx/docs/skills/gemini-review/references/prompt-templates.md +301 -0
- package/templates/tanstack-start/CLAUDE.md +43 -5
- package/templates/tanstack-start/docs/architecture/architecture.md +134 -4
- package/templates/tanstack-start/docs/commands/git.md +275 -0
- package/templates/tanstack-start/docs/deployment/cloudflare.md +223 -50
- package/templates/tanstack-start/docs/deployment/index.md +320 -30
- package/templates/tanstack-start/docs/deployment/nitro.md +195 -14
- package/templates/tanstack-start/docs/deployment/railway.md +302 -150
- package/templates/tanstack-start/docs/deployment/vercel.md +345 -75
- package/templates/tanstack-start/docs/guides/best-practices.md +203 -1
- package/templates/tanstack-start/docs/guides/env-setup.md +450 -0
- package/templates/tanstack-start/docs/library/ai-sdk/hooks.md +472 -0
- package/templates/tanstack-start/docs/library/ai-sdk/index.md +264 -0
- package/templates/tanstack-start/docs/library/ai-sdk/openrouter.md +371 -0
- package/templates/tanstack-start/docs/library/ai-sdk/providers.md +403 -0
- package/templates/tanstack-start/docs/library/ai-sdk/streaming.md +320 -0
- package/templates/tanstack-start/docs/library/ai-sdk/structured-output.md +454 -0
- package/templates/tanstack-start/docs/library/ai-sdk/tools.md +473 -0
- package/templates/tanstack-start/docs/library/pino/index.md +320 -0
- package/templates/tanstack-start/docs/library/prisma/cloudflare-d1.md +404 -0
- package/templates/tanstack-start/docs/library/prisma/config.md +377 -0
- package/templates/tanstack-start/docs/library/prisma/index.md +3 -5
- package/templates/tanstack-start/docs/library/prisma/schema.md +123 -25
- package/templates/tanstack-start/docs/library/prisma/setup.md +0 -7
- package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +80 -2
- package/templates/tanstack-start/docs/skills/gemini-review/SKILL.md +116 -116
- package/templates/tanstack-start/docs/skills/gemini-review/references/checklists.md +138 -144
- package/templates/tanstack-start/docs/skills/gemini-review/references/prompt-templates.md +186 -187
- package/templates/hono/docs/git/index.md +0 -180
- package/templates/tanstack-start/docs/git/index.md +0 -203
|
@@ -146,6 +146,49 @@ services/
|
|
|
146
146
|
└── mutations.ts
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
+
## 코드 작성 규칙
|
|
150
|
+
|
|
151
|
+
### UTF-8 인코딩 유지
|
|
152
|
+
|
|
153
|
+
모든 한글 텍스트는 UTF-8 인코딩이 깨지지 않도록 작성합니다.
|
|
154
|
+
|
|
155
|
+
### 한글 주석 작성 규칙
|
|
156
|
+
|
|
157
|
+
**묶음 단위로 한글 주석을 작성합니다** (너무 세세하게 X)
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
161
|
+
// 사용자 관련 상태
|
|
162
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
163
|
+
const [user, setUser] = useState<User | null>(null)
|
|
164
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
165
|
+
|
|
166
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
167
|
+
// 데이터 조회
|
|
168
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
169
|
+
const { data: users } = useQuery({
|
|
170
|
+
queryKey: ['users'],
|
|
171
|
+
queryFn: () => getUsers(),
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
175
|
+
// 이벤트 핸들러
|
|
176
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
177
|
+
const handleSubmit = () => { /* ... */ }
|
|
178
|
+
const handleDelete = () => { /* ... */ }
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### ❌ 너무 세세한 주석 (금지)
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// ❌ 이렇게 하지 마세요
|
|
185
|
+
const [user, setUser] = useState(null) // 사용자 상태
|
|
186
|
+
const [isLoading, setIsLoading] = useState(false) // 로딩 상태
|
|
187
|
+
const [error, setError] = useState(null) // 에러 상태
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
149
192
|
## TypeScript Standards
|
|
150
193
|
|
|
151
194
|
### Use `const` for Functions
|
|
@@ -234,7 +277,164 @@ const UsersPage = (): JSX.Element => {
|
|
|
234
277
|
}
|
|
235
278
|
```
|
|
236
279
|
|
|
237
|
-
###
|
|
280
|
+
### Custom Hook 작성 규칙
|
|
281
|
+
|
|
282
|
+
**Purpose**: 페이지 또는 섹션의 **모든 로직, 상태, 라이프사이클**을 중앙화합니다.
|
|
283
|
+
|
|
284
|
+
- 페이지 훅: 페이지 전체 로직 담당
|
|
285
|
+
- 섹션 훅: 해당 섹션의 로직만 담당 (섹션으로 분리한 경우)
|
|
286
|
+
|
|
287
|
+
### ⚠️ 필수: Custom Hook 내부 순서
|
|
288
|
+
|
|
289
|
+
훅 내부 코드는 **반드시 아래 순서**를 따릅니다:
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
1. State (useState, zustand store)
|
|
293
|
+
2. Global Hooks (useParams, useNavigate, useQueryClient 등)
|
|
294
|
+
3. React Query (useQuery → useMutation 순서)
|
|
295
|
+
4. Event Handlers & Functions
|
|
296
|
+
5. useMemo
|
|
297
|
+
6. useEffect
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### ✅ 올바른 Custom Hook 예시
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// routes/users/-hooks/use-users.ts
|
|
304
|
+
import { useState, useMemo, useEffect, useCallback } from 'react'
|
|
305
|
+
import { useParams, useNavigate } from '@tanstack/react-router'
|
|
306
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
307
|
+
import { useServerFn } from '@tanstack/react-start'
|
|
308
|
+
import { useAuthStore } from '@/stores/auth'
|
|
309
|
+
import { getUsers, createUser, deleteUser } from '@/services/user'
|
|
310
|
+
import type { User } from '@/types'
|
|
311
|
+
|
|
312
|
+
interface UseUsersReturn {
|
|
313
|
+
users: User[] | undefined
|
|
314
|
+
filteredUsers: User[]
|
|
315
|
+
isLoading: boolean
|
|
316
|
+
error: Error | null
|
|
317
|
+
search: string
|
|
318
|
+
setSearch: (value: string) => void
|
|
319
|
+
handleCreate: (data: { email: string; name: string }) => void
|
|
320
|
+
handleDelete: (id: string) => void
|
|
321
|
+
isCreating: boolean
|
|
322
|
+
isDeleting: boolean
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export const useUsers = (): UseUsersReturn => {
|
|
326
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
327
|
+
// 1. State (useState, zustand store)
|
|
328
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
329
|
+
const [search, setSearch] = useState('')
|
|
330
|
+
const { user: currentUser } = useAuthStore()
|
|
331
|
+
|
|
332
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
333
|
+
// 2. Global Hooks
|
|
334
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
335
|
+
const params = useParams({ from: '/users/$id' })
|
|
336
|
+
const navigate = useNavigate()
|
|
337
|
+
const queryClient = useQueryClient()
|
|
338
|
+
|
|
339
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
340
|
+
// 3. React Query (useQuery → useMutation)
|
|
341
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
342
|
+
const getUsersFn = useServerFn(getUsers)
|
|
343
|
+
const createUserFn = useServerFn(createUser)
|
|
344
|
+
const deleteUserFn = useServerFn(deleteUser)
|
|
345
|
+
|
|
346
|
+
const { data: users, isLoading, error } = useQuery({
|
|
347
|
+
queryKey: ['users'],
|
|
348
|
+
queryFn: () => getUsersFn(),
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
const createMutation = useMutation({
|
|
352
|
+
mutationFn: createUserFn,
|
|
353
|
+
onSuccess: () => {
|
|
354
|
+
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
355
|
+
},
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
const deleteMutation = useMutation({
|
|
359
|
+
mutationFn: deleteUserFn,
|
|
360
|
+
onSuccess: () => {
|
|
361
|
+
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
366
|
+
// 4. Event Handlers & Functions
|
|
367
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
368
|
+
const handleCreate = useCallback(
|
|
369
|
+
(data: { email: string; name: string }) => {
|
|
370
|
+
createMutation.mutate({ data })
|
|
371
|
+
},
|
|
372
|
+
[createMutation]
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
const handleDelete = useCallback(
|
|
376
|
+
(id: string) => {
|
|
377
|
+
deleteMutation.mutate({ data: id })
|
|
378
|
+
},
|
|
379
|
+
[deleteMutation]
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
383
|
+
// 5. useMemo
|
|
384
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
385
|
+
const filteredUsers = useMemo(() => {
|
|
386
|
+
if (!users) return []
|
|
387
|
+
if (!search) return users
|
|
388
|
+
return users.filter((user) =>
|
|
389
|
+
user.name.toLowerCase().includes(search.toLowerCase())
|
|
390
|
+
)
|
|
391
|
+
}, [users, search])
|
|
392
|
+
|
|
393
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
394
|
+
// 6. useEffect
|
|
395
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
396
|
+
useEffect(() => {
|
|
397
|
+
if (!currentUser) {
|
|
398
|
+
navigate({ to: '/login' })
|
|
399
|
+
}
|
|
400
|
+
}, [currentUser, navigate])
|
|
401
|
+
|
|
402
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
403
|
+
// Return
|
|
404
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
405
|
+
return {
|
|
406
|
+
users,
|
|
407
|
+
filteredUsers,
|
|
408
|
+
isLoading,
|
|
409
|
+
error,
|
|
410
|
+
search,
|
|
411
|
+
setSearch,
|
|
412
|
+
handleCreate,
|
|
413
|
+
handleDelete,
|
|
414
|
+
isCreating: createMutation.isPending,
|
|
415
|
+
isDeleting: deleteMutation.isPending,
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### ❌ 잘못된 순서 (금지)
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
// ❌ 순서가 뒤섞인 잘못된 예시
|
|
424
|
+
export const useBadHook = () => {
|
|
425
|
+
const queryClient = useQueryClient() // ❌ Global Hook이 먼저
|
|
426
|
+
|
|
427
|
+
useEffect(() => { /* ... */ }, []) // ❌ useEffect가 중간에
|
|
428
|
+
|
|
429
|
+
const [state, setState] = useState() // ❌ State가 나중에
|
|
430
|
+
|
|
431
|
+
const { data } = useQuery({ /* ... */ }) // ❌ Query가 Effect 다음에
|
|
432
|
+
|
|
433
|
+
const computed = useMemo(() => {}, []) // ❌ useMemo 위치 잘못됨
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Page Hook (간단한 예시)
|
|
238
438
|
|
|
239
439
|
```typescript
|
|
240
440
|
// routes/users/-hooks/use-users.ts
|
|
@@ -253,8 +453,10 @@ interface UseUsersReturn {
|
|
|
253
453
|
}
|
|
254
454
|
|
|
255
455
|
export const useUsers = (): UseUsersReturn => {
|
|
456
|
+
// 2. Global Hooks
|
|
256
457
|
const queryClient = useQueryClient()
|
|
257
458
|
|
|
459
|
+
// 3. React Query (useQuery → useMutation)
|
|
258
460
|
const { data: users, isLoading, error } = useQuery({
|
|
259
461
|
queryKey: ['users'],
|
|
260
462
|
queryFn: () => getUsers(),
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
# TanStack Start - 환경 변수 설정
|
|
2
|
+
|
|
3
|
+
> **상위 문서**: [Best Practices](./best-practices.md)
|
|
4
|
+
|
|
5
|
+
TanStack Start는 Vite 기반으로 동작하며, 환경별 `.env` 파일을 통해 환경 변수를 관리합니다.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 핵심 개념
|
|
10
|
+
|
|
11
|
+
### 서버 vs 클라이언트 환경 변수
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
서버 전용 → process.env.DATABASE_URL (노출 X)
|
|
15
|
+
클라이언트용 → import.meta.env.VITE_* (노출 O)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| 접근 방식 | 접근 가능 위치 | 용도 |
|
|
19
|
+
|-----------|---------------|------|
|
|
20
|
+
| `process.env.*` | Server Function, 서버 코드 | DB 연결, API 키, 시크릿 |
|
|
21
|
+
| `import.meta.env.VITE_*` | 클라이언트 + 서버 | 공개 설정, API URL |
|
|
22
|
+
|
|
23
|
+
⚠️ **중요**: `VITE_` 접두사가 없는 변수는 클라이언트에 노출되지 않습니다.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 환경 파일 구조
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
프로젝트/
|
|
31
|
+
├── .env # 기본 환경 변수 (공통, 커밋 O)
|
|
32
|
+
├── .env.development # 개발 환경 (커밋 O)
|
|
33
|
+
├── .env.production # 프로덕션 환경 (커밋 O)
|
|
34
|
+
├── .env.local # 로컬 오버라이드 (커밋 X, gitignore)
|
|
35
|
+
├── .env.development.local # 개발 로컬 오버라이드 (커밋 X)
|
|
36
|
+
├── .env.production.local # 프로덕션 로컬 오버라이드 (커밋 X)
|
|
37
|
+
└── app/
|
|
38
|
+
└── config/
|
|
39
|
+
└── env.ts # 환경 변수 검증 및 타입
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 로드 우선순위 (높은 순)
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
1. .env.{mode}.local # 최우선 (gitignore)
|
|
46
|
+
2. .env.local # 로컬 오버라이드 (gitignore)
|
|
47
|
+
3. .env.{mode} # 환경별 설정
|
|
48
|
+
4. .env # 기본 설정
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 환경 변수 파일 예시
|
|
54
|
+
|
|
55
|
+
### .env (기본 설정, 커밋용)
|
|
56
|
+
|
|
57
|
+
```env
|
|
58
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
59
|
+
# 기본 환경 변수 (모든 환경에서 공통)
|
|
60
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
61
|
+
|
|
62
|
+
# 클라이언트 공개 설정 (VITE_ 접두사 필수)
|
|
63
|
+
VITE_APP_NAME=My TanStack App
|
|
64
|
+
VITE_API_URL=https://api.example.com
|
|
65
|
+
|
|
66
|
+
# 서버 설정 템플릿 (실제 값은 .env.local에서 오버라이드)
|
|
67
|
+
DATABASE_URL=postgresql://localhost:5432/myapp_dev
|
|
68
|
+
REDIS_URL=redis://localhost:6379
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### .env.development
|
|
72
|
+
|
|
73
|
+
```env
|
|
74
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
75
|
+
# 개발 환경 설정
|
|
76
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
77
|
+
|
|
78
|
+
NODE_ENV=development
|
|
79
|
+
|
|
80
|
+
# 클라이언트 공개 설정
|
|
81
|
+
VITE_APP_NAME=My App (Dev)
|
|
82
|
+
VITE_API_URL=http://localhost:3001/api
|
|
83
|
+
VITE_DEBUG=true
|
|
84
|
+
|
|
85
|
+
# 서버 설정
|
|
86
|
+
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/myapp_dev
|
|
87
|
+
DATABASE_POOL_SIZE=5
|
|
88
|
+
|
|
89
|
+
# 개발용 설정
|
|
90
|
+
LOG_LEVEL=debug
|
|
91
|
+
CORS_ORIGIN=http://localhost:3000
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### .env.production
|
|
95
|
+
|
|
96
|
+
```env
|
|
97
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
98
|
+
# 프로덕션 환경 설정
|
|
99
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
100
|
+
|
|
101
|
+
NODE_ENV=production
|
|
102
|
+
|
|
103
|
+
# 클라이언트 공개 설정
|
|
104
|
+
VITE_APP_NAME=My App
|
|
105
|
+
VITE_API_URL=https://api.myapp.com
|
|
106
|
+
VITE_DEBUG=false
|
|
107
|
+
|
|
108
|
+
# 서버 설정 (실제 값은 CI/CD 또는 호스팅 환경에서 주입)
|
|
109
|
+
DATABASE_POOL_SIZE=20
|
|
110
|
+
|
|
111
|
+
# 프로덕션 설정
|
|
112
|
+
LOG_LEVEL=info
|
|
113
|
+
CORS_ORIGIN=https://myapp.com
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### .env.local (gitignore, 실제 시크릿)
|
|
117
|
+
|
|
118
|
+
```env
|
|
119
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
120
|
+
# 로컬 개발용 시크릿 (절대 커밋 금지!)
|
|
121
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
122
|
+
|
|
123
|
+
# 데이터베이스
|
|
124
|
+
DATABASE_URL=postgresql://user:realpassword@localhost:5432/myapp_local
|
|
125
|
+
|
|
126
|
+
# 인증
|
|
127
|
+
JWT_SECRET=your-super-secret-jwt-key-at-least-32-chars
|
|
128
|
+
AUTH_SECRET=your-auth-secret-key
|
|
129
|
+
|
|
130
|
+
# 외부 서비스 API 키
|
|
131
|
+
OPENAI_API_KEY=sk-xxx
|
|
132
|
+
STRIPE_SECRET_KEY=sk_test_xxx
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 타입 안전한 환경 변수 (Zod)
|
|
138
|
+
|
|
139
|
+
### app/config/env.ts
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// app/config/env.ts
|
|
143
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
144
|
+
// 환경 변수 검증 및 타입 정의
|
|
145
|
+
// 서버/클라이언트 환경 변수를 분리하여 관리합니다
|
|
146
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
147
|
+
import { z } from 'zod'
|
|
148
|
+
|
|
149
|
+
// 서버 환경 변수 스키마
|
|
150
|
+
const serverEnvSchema = z.object({
|
|
151
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
152
|
+
|
|
153
|
+
// 데이터베이스
|
|
154
|
+
DATABASE_URL: z.string().url(),
|
|
155
|
+
DATABASE_POOL_SIZE: z.coerce.number().default(10),
|
|
156
|
+
|
|
157
|
+
// 인증
|
|
158
|
+
JWT_SECRET: z.string().min(32),
|
|
159
|
+
AUTH_SECRET: z.string().min(16).optional(),
|
|
160
|
+
|
|
161
|
+
// 외부 서비스 (선택적)
|
|
162
|
+
OPENAI_API_KEY: z.string().optional(),
|
|
163
|
+
STRIPE_SECRET_KEY: z.string().optional(),
|
|
164
|
+
|
|
165
|
+
// 설정
|
|
166
|
+
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
|
167
|
+
CORS_ORIGIN: z.string().default('*'),
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// 클라이언트 환경 변수 스키마 (VITE_ 접두사)
|
|
171
|
+
const clientEnvSchema = z.object({
|
|
172
|
+
VITE_APP_NAME: z.string(),
|
|
173
|
+
VITE_API_URL: z.string().url(),
|
|
174
|
+
VITE_DEBUG: z.coerce.boolean().default(false),
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// 타입 추출
|
|
178
|
+
export type ServerEnv = z.infer<typeof serverEnvSchema>
|
|
179
|
+
export type ClientEnv = z.infer<typeof clientEnvSchema>
|
|
180
|
+
|
|
181
|
+
// 서버 환경 변수 파싱 (Server Function에서만 호출)
|
|
182
|
+
const parseServerEnv = (): ServerEnv => {
|
|
183
|
+
const result = serverEnvSchema.safeParse(process.env)
|
|
184
|
+
|
|
185
|
+
if (!result.success) {
|
|
186
|
+
console.error('❌ 서버 환경 변수 검증 실패:')
|
|
187
|
+
console.error(result.error.format())
|
|
188
|
+
throw new Error('Server environment validation failed')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return result.data
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 클라이언트 환경 변수 파싱
|
|
195
|
+
const parseClientEnv = (): ClientEnv => {
|
|
196
|
+
const result = clientEnvSchema.safeParse(import.meta.env)
|
|
197
|
+
|
|
198
|
+
if (!result.success) {
|
|
199
|
+
console.error('❌ 클라이언트 환경 변수 검증 실패:')
|
|
200
|
+
console.error(result.error.format())
|
|
201
|
+
throw new Error('Client environment validation failed')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return result.data
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Export (서버 환경 변수는 lazy evaluation)
|
|
208
|
+
let _serverEnv: ServerEnv | null = null
|
|
209
|
+
|
|
210
|
+
export const getServerEnv = (): ServerEnv => {
|
|
211
|
+
if (!_serverEnv) {
|
|
212
|
+
_serverEnv = parseServerEnv()
|
|
213
|
+
}
|
|
214
|
+
return _serverEnv
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export const clientEnv = parseClientEnv()
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 사용 예시
|
|
223
|
+
|
|
224
|
+
### Server Function에서 사용
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// app/server-functions/users.ts
|
|
228
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
229
|
+
import { getServerEnv } from '~/config/env'
|
|
230
|
+
|
|
231
|
+
export const getUsers = createServerFn({ method: 'GET' })
|
|
232
|
+
.handler(async () => {
|
|
233
|
+
const env = getServerEnv()
|
|
234
|
+
|
|
235
|
+
// ✅ 서버 전용 환경 변수 사용
|
|
236
|
+
const config = {
|
|
237
|
+
url: env.DATABASE_URL,
|
|
238
|
+
maxConnections: env.DATABASE_POOL_SIZE,
|
|
239
|
+
ssl: env.NODE_ENV === 'production',
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const db = await createConnection(config)
|
|
243
|
+
return db.user.findMany()
|
|
244
|
+
})
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 클라이언트 컴포넌트에서 사용
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
// app/components/AppHeader.tsx
|
|
251
|
+
import { clientEnv } from '~/config/env'
|
|
252
|
+
|
|
253
|
+
export const AppHeader = () => {
|
|
254
|
+
return (
|
|
255
|
+
<header>
|
|
256
|
+
{/* ✅ VITE_ 접두사 변수만 접근 가능 */}
|
|
257
|
+
<h1>{clientEnv.VITE_APP_NAME}</h1>
|
|
258
|
+
|
|
259
|
+
{clientEnv.VITE_DEBUG && (
|
|
260
|
+
<span className="text-red-500">Debug Mode</span>
|
|
261
|
+
)}
|
|
262
|
+
</header>
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### 직접 접근 (검증 없이)
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
// Server Function 내부
|
|
271
|
+
const dbUrl = process.env.DATABASE_URL // ✅ 서버에서만
|
|
272
|
+
|
|
273
|
+
// 클라이언트 컴포넌트
|
|
274
|
+
const appName = import.meta.env.VITE_APP_NAME // ✅ VITE_ 접두사만
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## 인증 설정 예시
|
|
280
|
+
|
|
281
|
+
### 서버 측 인증 설정
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
// app/lib/auth.ts
|
|
285
|
+
import { getServerEnv } from '~/config/env'
|
|
286
|
+
|
|
287
|
+
export const getAuthConfig = () => {
|
|
288
|
+
const env = getServerEnv()
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
secret: env.AUTH_SECRET,
|
|
292
|
+
providers: {
|
|
293
|
+
auth0: {
|
|
294
|
+
domain: process.env.AUTH0_DOMAIN,
|
|
295
|
+
clientId: process.env.AUTH0_CLIENT_ID,
|
|
296
|
+
clientSecret: process.env.AUTH0_CLIENT_SECRET, // ✅ 서버 전용
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### 클라이언트 측 인증 Provider
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
// app/components/AuthProvider.tsx
|
|
307
|
+
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
|
308
|
+
return (
|
|
309
|
+
<Auth0Provider
|
|
310
|
+
domain={import.meta.env.VITE_AUTH0_DOMAIN}
|
|
311
|
+
clientId={import.meta.env.VITE_AUTH0_CLIENT_ID}
|
|
312
|
+
// ❌ clientSecret은 여기에 없음 - 서버에만 존재
|
|
313
|
+
>
|
|
314
|
+
{children}
|
|
315
|
+
</Auth0Provider>
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## 필수 환경 변수 검증
|
|
323
|
+
|
|
324
|
+
### 앱 시작 시 검증
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
// app/config/validation.ts
|
|
328
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
329
|
+
// 필수 환경 변수 검증
|
|
330
|
+
// 앱 시작 시 누락된 환경 변수를 조기에 발견합니다
|
|
331
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
332
|
+
|
|
333
|
+
const requiredServerEnv = [
|
|
334
|
+
'DATABASE_URL',
|
|
335
|
+
'JWT_SECRET',
|
|
336
|
+
] as const
|
|
337
|
+
|
|
338
|
+
const requiredClientEnv = [
|
|
339
|
+
'VITE_APP_NAME',
|
|
340
|
+
'VITE_API_URL',
|
|
341
|
+
] as const
|
|
342
|
+
|
|
343
|
+
export const validateEnv = () => {
|
|
344
|
+
// 서버 환경 변수 검증
|
|
345
|
+
for (const key of requiredServerEnv) {
|
|
346
|
+
if (!process.env[key]) {
|
|
347
|
+
throw new Error(`❌ Missing required server env: ${key}`)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 클라이언트 환경 변수 검증
|
|
352
|
+
for (const key of requiredClientEnv) {
|
|
353
|
+
if (!import.meta.env[key]) {
|
|
354
|
+
throw new Error(`❌ Missing required client env: ${key}`)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
console.log('✅ Environment variables validated')
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## .gitignore 설정
|
|
365
|
+
|
|
366
|
+
```gitignore
|
|
367
|
+
# 환경 변수 파일
|
|
368
|
+
.env.local
|
|
369
|
+
.env.*.local
|
|
370
|
+
.env.development.local
|
|
371
|
+
.env.production.local
|
|
372
|
+
|
|
373
|
+
# 커밋해도 되는 파일 (시크릿 없음)
|
|
374
|
+
!.env
|
|
375
|
+
!.env.development
|
|
376
|
+
!.env.production
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
⚠️ **중요**: `.env.development`와 `.env.production`에는 **실제 시크릿을 절대 넣지 마세요!**
|
|
380
|
+
- 실제 시크릿은 `.env.local` 또는 CI/CD 환경 변수로 관리
|
|
381
|
+
- `.env.*` 파일에는 플레이스홀더나 기본값만 포함
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## 빌드 모드
|
|
386
|
+
|
|
387
|
+
### 개발 서버
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
# 기본 development 모드
|
|
391
|
+
npm run dev
|
|
392
|
+
|
|
393
|
+
# .env.development + .env 로드
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### 프로덕션 빌드
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
# 기본 production 모드
|
|
400
|
+
npm run build
|
|
401
|
+
|
|
402
|
+
# .env.production + .env 로드
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### 커스텀 모드
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
# staging 모드로 빌드
|
|
409
|
+
npm run build -- --mode staging
|
|
410
|
+
|
|
411
|
+
# .env.staging + .env 로드
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
```env
|
|
415
|
+
# .env.staging
|
|
416
|
+
NODE_ENV=production
|
|
417
|
+
VITE_APP_NAME=My App (Staging)
|
|
418
|
+
VITE_API_URL=https://staging-api.myapp.com
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## TypeScript 타입 정의
|
|
424
|
+
|
|
425
|
+
### vite-env.d.ts
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
// app/vite-env.d.ts
|
|
429
|
+
/// <reference types="vite/client" />
|
|
430
|
+
|
|
431
|
+
interface ImportMetaEnv {
|
|
432
|
+
readonly VITE_APP_NAME: string
|
|
433
|
+
readonly VITE_API_URL: string
|
|
434
|
+
readonly VITE_DEBUG: string
|
|
435
|
+
readonly VITE_AUTH0_DOMAIN?: string
|
|
436
|
+
readonly VITE_AUTH0_CLIENT_ID?: string
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
interface ImportMeta {
|
|
440
|
+
readonly env: ImportMetaEnv
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## 관련 문서
|
|
447
|
+
|
|
448
|
+
- [Best Practices](./best-practices.md)
|
|
449
|
+
- [Server Functions](../library/tanstack-start/server-functions.md)
|
|
450
|
+
- [Vite 환경 변수 공식 문서](https://vitejs.dev/guide/env-and-mode.html)
|