@kood/claude-code 0.3.6 → 0.3.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 +1 -1
- package/package.json +1 -1
- package/templates/nextjs/CLAUDE.md +228 -0
- package/templates/nextjs/docs/design.md +558 -0
- package/templates/nextjs/docs/guides/conventions.md +343 -0
- package/templates/nextjs/docs/guides/getting-started.md +367 -0
- package/templates/nextjs/docs/guides/routes.md +342 -0
- package/templates/nextjs/docs/library/better-auth/index.md +541 -0
- package/templates/nextjs/docs/library/nextjs/app-router.md +269 -0
- package/templates/nextjs/docs/library/nextjs/caching.md +351 -0
- package/templates/nextjs/docs/library/nextjs/index.md +291 -0
- package/templates/nextjs/docs/library/nextjs/middleware.md +391 -0
- package/templates/nextjs/docs/library/nextjs/route-handlers.md +382 -0
- package/templates/nextjs/docs/library/nextjs/server-actions.md +366 -0
- package/templates/nextjs/docs/library/prisma/cloudflare-d1.md +76 -0
- package/templates/nextjs/docs/library/prisma/config.md +77 -0
- package/templates/nextjs/docs/library/prisma/crud.md +90 -0
- package/templates/nextjs/docs/library/prisma/index.md +73 -0
- package/templates/nextjs/docs/library/prisma/relations.md +69 -0
- package/templates/nextjs/docs/library/prisma/schema.md +98 -0
- package/templates/nextjs/docs/library/prisma/setup.md +49 -0
- package/templates/nextjs/docs/library/prisma/transactions.md +50 -0
- package/templates/nextjs/docs/library/tanstack-query/index.md +66 -0
- package/templates/nextjs/docs/library/tanstack-query/invalidation.md +54 -0
- package/templates/nextjs/docs/library/tanstack-query/optimistic-updates.md +77 -0
- package/templates/nextjs/docs/library/tanstack-query/use-mutation.md +63 -0
- package/templates/nextjs/docs/library/tanstack-query/use-query.md +70 -0
- package/templates/nextjs/docs/library/zod/complex-types.md +61 -0
- package/templates/nextjs/docs/library/zod/index.md +56 -0
- package/templates/nextjs/docs/library/zod/transforms.md +51 -0
- package/templates/nextjs/docs/library/zod/validation.md +70 -0
- package/templates/tanstack-start/CLAUDE.md +7 -3
- package/templates/tanstack-start/docs/guides/hooks.md +28 -0
- package/templates/tanstack-start/docs/guides/routes.md +29 -10
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# App Router
|
|
2
|
+
|
|
3
|
+
> 파일 기반 라우팅 시스템
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 파일 구조와 라우팅
|
|
8
|
+
|
|
9
|
+
### 기본 규칙
|
|
10
|
+
|
|
11
|
+
| 파일 | 라우트 | 설명 |
|
|
12
|
+
|------|--------|------|
|
|
13
|
+
| `app/page.tsx` | `/` | 홈 페이지 |
|
|
14
|
+
| `app/about/page.tsx` | `/about` | About 페이지 |
|
|
15
|
+
| `app/blog/[slug]/page.tsx` | `/blog/:slug` | 동적 라우트 |
|
|
16
|
+
| `app/shop/[...slug]/page.tsx` | `/shop/*` | Catch-all |
|
|
17
|
+
| `app/docs/[[...slug]]/page.tsx` | `/docs/*` | Optional catch-all |
|
|
18
|
+
|
|
19
|
+
### 예시
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// app/blog/[slug]/page.tsx
|
|
23
|
+
interface PageProps {
|
|
24
|
+
params: { slug: string }
|
|
25
|
+
searchParams: { [key: string]: string | string[] | undefined }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default async function BlogPost({ params, searchParams }: PageProps) {
|
|
29
|
+
const post = await prisma.post.findUnique({ where: { slug: params.slug } })
|
|
30
|
+
|
|
31
|
+
if (!post) notFound()
|
|
32
|
+
|
|
33
|
+
return <article>{post.content}</article>
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Layouts
|
|
40
|
+
|
|
41
|
+
### Root Layout (필수)
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// app/layout.tsx
|
|
45
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
46
|
+
return (
|
|
47
|
+
<html lang="ko">
|
|
48
|
+
<body>{children}</body>
|
|
49
|
+
</html>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 중첩 Layout
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// app/dashboard/layout.tsx
|
|
58
|
+
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|
59
|
+
return (
|
|
60
|
+
<div>
|
|
61
|
+
<nav>Dashboard Nav</nav>
|
|
62
|
+
<main>{children}</main>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**특징:**
|
|
69
|
+
- 중첩 가능 (부모 → 자식 순서)
|
|
70
|
+
- 리렌더링 없이 유지됨
|
|
71
|
+
- props로 `children` 받음
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Route Groups
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
app/
|
|
79
|
+
├── (marketing)/
|
|
80
|
+
│ ├── layout.tsx # Marketing layout
|
|
81
|
+
│ ├── page.tsx # /
|
|
82
|
+
│ └── about/
|
|
83
|
+
│ └── page.tsx # /about
|
|
84
|
+
└── (shop)/
|
|
85
|
+
├── layout.tsx # Shop layout
|
|
86
|
+
└── products/
|
|
87
|
+
└── page.tsx # /products
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**용도:**
|
|
91
|
+
- URL에 영향 없이 폴더 그룹화
|
|
92
|
+
- 다른 레이아웃 적용
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 동적 라우트
|
|
97
|
+
|
|
98
|
+
### 단일 파라미터
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// app/posts/[id]/page.tsx
|
|
102
|
+
export default async function PostPage({ params }: { params: { id: string } }) {
|
|
103
|
+
const post = await prisma.post.findUnique({ where: { id: params.id } })
|
|
104
|
+
return <article>{post.title}</article>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 정적 생성 (빌드 시)
|
|
108
|
+
export async function generateStaticParams() {
|
|
109
|
+
const posts = await prisma.post.findMany()
|
|
110
|
+
return posts.map(post => ({ id: post.id }))
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Catch-all
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// app/docs/[...slug]/page.tsx
|
|
118
|
+
export default function DocsPage({ params }: { params: { slug: string[] } }) {
|
|
119
|
+
// /docs/a/b/c → params.slug = ["a", "b", "c"]
|
|
120
|
+
return <div>{params.slug.join("/")}</div>
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 병렬 라우트
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
app/
|
|
130
|
+
├── @analytics/
|
|
131
|
+
│ └── page.tsx
|
|
132
|
+
├── @team/
|
|
133
|
+
│ └── page.tsx
|
|
134
|
+
└── layout.tsx
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// app/layout.tsx
|
|
139
|
+
export default function Layout({
|
|
140
|
+
children,
|
|
141
|
+
analytics,
|
|
142
|
+
team,
|
|
143
|
+
}: {
|
|
144
|
+
children: React.ReactNode
|
|
145
|
+
analytics: React.ReactNode
|
|
146
|
+
team: React.ReactNode
|
|
147
|
+
}) {
|
|
148
|
+
return (
|
|
149
|
+
<>
|
|
150
|
+
{children}
|
|
151
|
+
{analytics}
|
|
152
|
+
{team}
|
|
153
|
+
</>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 인터셉팅 라우트
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
app/
|
|
164
|
+
├── feed/
|
|
165
|
+
│ └── page.tsx
|
|
166
|
+
├── photo/
|
|
167
|
+
│ └── [id]/
|
|
168
|
+
│ └── page.tsx
|
|
169
|
+
└── @modal/
|
|
170
|
+
└── (.)photo/
|
|
171
|
+
└── [id]/
|
|
172
|
+
└── page.tsx
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**컨벤션:**
|
|
176
|
+
- `(.)` - 같은 레벨
|
|
177
|
+
- `(..)` - 한 단계 위
|
|
178
|
+
- `(..)(..)` - 두 단계 위
|
|
179
|
+
- `(...)` - 루트부터
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Metadata
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
// app/blog/[slug]/page.tsx
|
|
187
|
+
import type { Metadata } from "next"
|
|
188
|
+
|
|
189
|
+
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
|
|
190
|
+
const post = await prisma.post.findUnique({ where: { slug: params.slug } })
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
title: post.title,
|
|
194
|
+
description: post.excerpt,
|
|
195
|
+
openGraph: {
|
|
196
|
+
title: post.title,
|
|
197
|
+
description: post.excerpt,
|
|
198
|
+
images: [post.image],
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## 특수 파일
|
|
207
|
+
|
|
208
|
+
| 파일 | 용도 |
|
|
209
|
+
|------|------|
|
|
210
|
+
| `loading.tsx` | Suspense 폴백 |
|
|
211
|
+
| `error.tsx` | Error Boundary |
|
|
212
|
+
| `not-found.tsx` | 404 페이지 |
|
|
213
|
+
| `template.tsx` | 리렌더링되는 Layout |
|
|
214
|
+
| `default.tsx` | 병렬 라우트 폴백 |
|
|
215
|
+
|
|
216
|
+
### Loading UI
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
// app/dashboard/loading.tsx
|
|
220
|
+
export default function Loading() {
|
|
221
|
+
return <div>Loading...</div>
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Error UI
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// app/dashboard/error.tsx
|
|
229
|
+
"use client"
|
|
230
|
+
|
|
231
|
+
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
|
232
|
+
return (
|
|
233
|
+
<div>
|
|
234
|
+
<h2>오류 발생</h2>
|
|
235
|
+
<button onClick={reset}>다시 시도</button>
|
|
236
|
+
</div>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## 네비게이션
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
"use client"
|
|
247
|
+
|
|
248
|
+
import { useRouter, usePathname, useSearchParams } from "next/navigation"
|
|
249
|
+
import Link from "next/link"
|
|
250
|
+
|
|
251
|
+
export function Navigation() {
|
|
252
|
+
const router = useRouter()
|
|
253
|
+
const pathname = usePathname() // 현재 경로
|
|
254
|
+
const searchParams = useSearchParams() // 쿼리 파라미터
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<>
|
|
258
|
+
<Link href="/about">About</Link>
|
|
259
|
+
<button onClick={() => router.push("/posts")}>Go to Posts</button>
|
|
260
|
+
</>
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 참조
|
|
268
|
+
|
|
269
|
+
- [Next.js App Router 공식 문서](https://nextjs.org/docs/app)
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# Caching
|
|
2
|
+
|
|
3
|
+
> Next.js 캐싱 전략
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 캐시 레벨
|
|
8
|
+
|
|
9
|
+
| 레벨 | 설명 |
|
|
10
|
+
|------|------|
|
|
11
|
+
| **Request Memoization** | 같은 요청 중복 제거 (React) |
|
|
12
|
+
| **Data Cache** | 서버 데이터 캐시 (영구) |
|
|
13
|
+
| **Full Route Cache** | 빌드 시 정적 렌더링 |
|
|
14
|
+
| **Router Cache** | 클라이언트 라우터 캐시 |
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Request Memoization
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// 같은 요청은 한 번만 실행됨
|
|
22
|
+
async function getUser(id: string) {
|
|
23
|
+
const res = await fetch(`https://api.example.com/users/${id}`)
|
|
24
|
+
return res.json()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default async function Page() {
|
|
28
|
+
const user1 = await getUser("1") // fetch 실행
|
|
29
|
+
const user2 = await getUser("1") // 캐시 사용 (중복 제거)
|
|
30
|
+
|
|
31
|
+
return <div>{user1.name}</div>
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Data Cache (fetch)
|
|
38
|
+
|
|
39
|
+
### 기본 (캐시 사용)
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// 기본적으로 캐시됨
|
|
43
|
+
const res = await fetch("https://api.example.com/posts")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 캐시 비활성화
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// 매 요청마다 새로 가져옴
|
|
50
|
+
const res = await fetch("https://api.example.com/posts", {
|
|
51
|
+
cache: "no-store",
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Revalidate (시간 기반)
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// 60초마다 재검증
|
|
59
|
+
const res = await fetch("https://api.example.com/posts", {
|
|
60
|
+
next: { revalidate: 60 },
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Tag 기반 캐시
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// 태그 설정
|
|
68
|
+
const res = await fetch("https://api.example.com/posts", {
|
|
69
|
+
next: { tags: ["posts"] },
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// Server Action에서 태그 무효화
|
|
73
|
+
"use server"
|
|
74
|
+
import { revalidateTag } from "next/cache"
|
|
75
|
+
|
|
76
|
+
export async function createPost(data: PostInput) {
|
|
77
|
+
await prisma.post.create({ data })
|
|
78
|
+
revalidateTag("posts") // "posts" 태그 캐시 무효화
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## unstable_cache (함수 캐싱)
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { unstable_cache } from "next/cache"
|
|
88
|
+
|
|
89
|
+
const getCachedPosts = unstable_cache(
|
|
90
|
+
async () => {
|
|
91
|
+
return prisma.post.findMany()
|
|
92
|
+
},
|
|
93
|
+
["posts"], // 캐시 키
|
|
94
|
+
{
|
|
95
|
+
revalidate: 60, // 60초
|
|
96
|
+
tags: ["posts"], // 태그
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
export default async function PostsPage() {
|
|
101
|
+
const posts = await getCachedPosts()
|
|
102
|
+
return <PostsList posts={posts} />
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## revalidatePath
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
"use server"
|
|
112
|
+
|
|
113
|
+
import { revalidatePath } from "next/cache"
|
|
114
|
+
|
|
115
|
+
export async function createPost(data: PostInput) {
|
|
116
|
+
const post = await prisma.post.create({ data })
|
|
117
|
+
|
|
118
|
+
// 특정 경로 캐시 무효화
|
|
119
|
+
revalidatePath("/posts")
|
|
120
|
+
revalidatePath(`/posts/${post.id}`)
|
|
121
|
+
|
|
122
|
+
// 레이아웃 포함 모든 캐시 무효화
|
|
123
|
+
revalidatePath("/posts", "layout")
|
|
124
|
+
|
|
125
|
+
return post
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## revalidateTag
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
"use server"
|
|
135
|
+
|
|
136
|
+
import { revalidateTag } from "next/cache"
|
|
137
|
+
|
|
138
|
+
export async function createPost(data: PostInput) {
|
|
139
|
+
const post = await prisma.post.create({ data })
|
|
140
|
+
|
|
141
|
+
// "posts" 태그가 붙은 모든 캐시 무효화
|
|
142
|
+
revalidateTag("posts")
|
|
143
|
+
|
|
144
|
+
return post
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Full Route Cache (정적 렌더링)
|
|
151
|
+
|
|
152
|
+
### 정적 페이지
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// 빌드 시 생성 (기본)
|
|
156
|
+
export default async function PostsPage() {
|
|
157
|
+
const posts = await prisma.post.findMany()
|
|
158
|
+
return <PostsList posts={posts} />
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### 동적 페이지 (캐시 비활성화)
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// 매 요청마다 렌더링
|
|
166
|
+
export const dynamic = "force-dynamic"
|
|
167
|
+
|
|
168
|
+
export default async function PostsPage() {
|
|
169
|
+
const posts = await prisma.post.findMany()
|
|
170
|
+
return <PostsList posts={posts} />
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Revalidate (시간 기반)
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// 60초마다 재생성
|
|
178
|
+
export const revalidate = 60
|
|
179
|
+
|
|
180
|
+
export default async function PostsPage() {
|
|
181
|
+
const posts = await prisma.post.findMany()
|
|
182
|
+
return <PostsList posts={posts} />
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Route Segment Config
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// app/posts/page.tsx
|
|
192
|
+
|
|
193
|
+
// 동적 렌더링 강제
|
|
194
|
+
export const dynamic = "force-dynamic" // "auto" | "force-static" | "error"
|
|
195
|
+
|
|
196
|
+
// Revalidate 주기 (초)
|
|
197
|
+
export const revalidate = 60 // false | 0 | number
|
|
198
|
+
|
|
199
|
+
// 런타임 설정
|
|
200
|
+
export const runtime = "nodejs" // "edge"
|
|
201
|
+
|
|
202
|
+
// 최대 실행 시간 (초)
|
|
203
|
+
export const maxDuration = 60
|
|
204
|
+
|
|
205
|
+
export default async function PostsPage() {
|
|
206
|
+
// ...
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Router Cache (클라이언트)
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
"use client"
|
|
216
|
+
|
|
217
|
+
import { useRouter } from "next/navigation"
|
|
218
|
+
|
|
219
|
+
export function Navigation() {
|
|
220
|
+
const router = useRouter()
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<button
|
|
224
|
+
onClick={() => {
|
|
225
|
+
router.push("/posts") // 캐시된 페이지 사용
|
|
226
|
+
router.refresh() // 강제 새로고침
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
Go to Posts
|
|
230
|
+
</button>
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## generateStaticParams (동적 라우트)
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// app/posts/[id]/page.tsx
|
|
241
|
+
|
|
242
|
+
// 빌드 시 생성할 페이지 목록
|
|
243
|
+
export async function generateStaticParams() {
|
|
244
|
+
const posts = await prisma.post.findMany({ select: { id: true } })
|
|
245
|
+
return posts.map(post => ({ id: post.id }))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export default async function PostPage({ params }: { params: { id: string } }) {
|
|
249
|
+
const post = await prisma.post.findUnique({ where: { id: params.id } })
|
|
250
|
+
return <article>{post.title}</article>
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## 캐싱 플로우
|
|
257
|
+
|
|
258
|
+
### 정적 페이지
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
1. 빌드 시 렌더링
|
|
262
|
+
2. Full Route Cache 저장
|
|
263
|
+
3. 이후 요청은 캐시 사용
|
|
264
|
+
4. revalidate 시간 후 재생성
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### 동적 페이지
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
1. 매 요청마다 렌더링
|
|
271
|
+
2. 캐시 없음
|
|
272
|
+
3. Server Actions로 데이터 업데이트
|
|
273
|
+
4. revalidatePath로 특정 경로만 무효화
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 캐시 무효화 전략
|
|
279
|
+
|
|
280
|
+
### 시간 기반
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// 60초마다 재생성
|
|
284
|
+
export const revalidate = 60
|
|
285
|
+
|
|
286
|
+
const res = await fetch("...", { next: { revalidate: 60 } })
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### 온디맨드 (Server Actions)
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
"use server"
|
|
293
|
+
|
|
294
|
+
import { revalidatePath, revalidateTag } from "next/cache"
|
|
295
|
+
|
|
296
|
+
export async function updatePost(id: string, data: PostInput) {
|
|
297
|
+
await prisma.post.update({ where: { id }, data })
|
|
298
|
+
|
|
299
|
+
// 경로 무효화
|
|
300
|
+
revalidatePath(`/posts/${id}`)
|
|
301
|
+
|
|
302
|
+
// 태그 무효화
|
|
303
|
+
revalidateTag("posts")
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## 베스트 프랙티스
|
|
310
|
+
|
|
311
|
+
### ✅ DO
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
// 1. 정적 데이터는 기본 캐시 사용
|
|
315
|
+
const posts = await fetch("https://api.example.com/posts")
|
|
316
|
+
|
|
317
|
+
// 2. 동적 데이터는 no-store
|
|
318
|
+
const user = await fetch("https://api.example.com/user", {
|
|
319
|
+
cache: "no-store",
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
// 3. 태그 기반 무효화
|
|
323
|
+
const posts = await fetch("...", { next: { tags: ["posts"] } })
|
|
324
|
+
revalidateTag("posts")
|
|
325
|
+
|
|
326
|
+
// 4. 함수 캐싱
|
|
327
|
+
const getCachedData = unstable_cache(
|
|
328
|
+
async () => prisma.post.findMany(),
|
|
329
|
+
["posts"],
|
|
330
|
+
{ revalidate: 60 }
|
|
331
|
+
)
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### ❌ DON'T
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
// 1. 민감한 데이터 캐싱
|
|
338
|
+
const user = await fetch("/api/user") // ❌ 개인정보 캐싱 금지
|
|
339
|
+
|
|
340
|
+
// 2. 과도한 revalidatePath
|
|
341
|
+
revalidatePath("/") // ❌ 전체 사이트 무효화
|
|
342
|
+
|
|
343
|
+
// 3. 짧은 revalidate
|
|
344
|
+
export const revalidate = 1 // ❌ 부하 증가
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## 참조
|
|
350
|
+
|
|
351
|
+
- [Next.js Caching](https://nextjs.org/docs/app/building-your-application/caching)
|