@wooojin/forgen 0.4.8 → 0.4.9
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/.claude-plugin/plugin.json +1 -1
- package/assets/dev-guide/be/README.md +226 -0
- package/assets/dev-guide/be/adapters/build-agents-md.sh +63 -0
- package/assets/dev-guide/be/principles/common.md +433 -0
- package/assets/dev-guide/be/principles/go.md +469 -0
- package/assets/dev-guide/be/principles/node.md +388 -0
- package/assets/dev-guide/be/skills/go/be-build/SKILL.md +262 -0
- package/assets/dev-guide/be/skills/go/be-perf/SKILL.md +308 -0
- package/assets/dev-guide/be/skills/go/be-review/SKILL.md +119 -0
- package/assets/dev-guide/be/skills/go/be-security/SKILL.md +362 -0
- package/assets/dev-guide/be/skills/node/be-build/SKILL.md +239 -0
- package/assets/dev-guide/be/skills/node/be-perf/SKILL.md +272 -0
- package/assets/dev-guide/be/skills/node/be-review/SKILL.md +118 -0
- package/assets/dev-guide/be/skills/node/be-security/SKILL.md +355 -0
- package/assets/dev-guide/be/sources/12factor/INDEX.md +53 -0
- package/assets/dev-guide/be/sources/api-design/INDEX.md +56 -0
- package/assets/dev-guide/be/sources/ddia/INDEX.md +55 -0
- package/assets/dev-guide/be/sources/go-runtime/INDEX.md +62 -0
- package/assets/dev-guide/be/sources/node-runtime/INDEX.md +60 -0
- package/assets/dev-guide/be/sources/otel/INDEX.md +53 -0
- package/assets/dev-guide/be/sources/owasp-api/INDEX.md +52 -0
- package/assets/dev-guide/be/sources/postgres/INDEX.md +55 -0
- package/assets/dev-guide/be/sources/sre-book/INDEX.md +48 -0
- package/assets/dev-guide/fe/README.md +197 -0
- package/assets/dev-guide/fe/adapters/build-agents-md.sh +63 -0
- package/assets/dev-guide/fe/adapters/refresh.sh +68 -0
- package/assets/dev-guide/fe/principles/common.md +160 -0
- package/assets/dev-guide/fe/principles/react.md +183 -0
- package/assets/dev-guide/fe/principles/vue.md +196 -0
- package/assets/dev-guide/fe/skills/react/fe-build/SKILL.md +139 -0
- package/assets/dev-guide/fe/skills/react/fe-perf/SKILL.md +179 -0
- package/assets/dev-guide/fe/skills/react/fe-review/SKILL.md +141 -0
- package/assets/dev-guide/fe/skills/vue/fe-build/SKILL.md +148 -0
- package/assets/dev-guide/fe/skills/vue/fe-perf/SKILL.md +163 -0
- package/assets/dev-guide/fe/skills/vue/fe-review/SKILL.md +136 -0
- package/assets/dev-guide/fe/sources/a11y-dx/INDEX.md +41 -0
- package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-memory.md +150 -0
- package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-performance.md +99 -0
- package/assets/dev-guide/fe/sources/a11y-dx/lighthouse-audits.md +146 -0
- package/assets/dev-guide/fe/sources/a11y-dx/react-devtools-profiler.md +128 -0
- package/assets/dev-guide/fe/sources/a11y-dx/wcag22-new-criteria.md +174 -0
- package/assets/dev-guide/fe/sources/perf/01-core-web-vitals.md +58 -0
- package/assets/dev-guide/fe/sources/perf/02-inp.md +83 -0
- package/assets/dev-guide/fe/sources/perf/03-lcp-cls.md +130 -0
- package/assets/dev-guide/fe/sources/perf/04-speculation-rules.md +148 -0
- package/assets/dev-guide/fe/sources/perf/05-view-transitions.md +153 -0
- package/assets/dev-guide/fe/sources/perf/06-nextjs-caching.md +188 -0
- package/assets/dev-guide/fe/sources/perf/07-server-components.md +181 -0
- package/assets/dev-guide/fe/sources/perf/08-ppr.md +133 -0
- package/assets/dev-guide/fe/sources/perf/09-nextjs-image.md +200 -0
- package/assets/dev-guide/fe/sources/perf/10-optimize-lcp.md +201 -0
- package/assets/dev-guide/fe/sources/perf/INDEX.md +88 -0
- package/assets/dev-guide/fe/sources/react/INDEX.md +41 -0
- package/assets/dev-guide/fe/sources/react/keeping-components-pure.md +135 -0
- package/assets/dev-guide/fe/sources/react/no-effect-patterns.md +183 -0
- package/assets/dev-guide/fe/sources/react/react-compiler.md +182 -0
- package/assets/dev-guide/fe/sources/react/server-components.md +194 -0
- package/assets/dev-guide/fe/sources/react/server-functions.md +192 -0
- package/assets/dev-guide/fe/sources/react/suspense.md +218 -0
- package/assets/dev-guide/fe/sources/react/use-action-state.md +123 -0
- package/assets/dev-guide/fe/sources/react/use-form-status.md +158 -0
- package/assets/dev-guide/fe/sources/react/use-hook.md +153 -0
- package/assets/dev-guide/fe/sources/react/use-optimistic.md +194 -0
- package/assets/dev-guide/fe/sources/toss-ff/INDEX.md +58 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-code-directory.md +79 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-form-fields.md +110 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-magic-number.md +47 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-item-edit-modal.md +124 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-use-bottom-sheet.md +57 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-use-page-state.md +71 -0
- package/assets/dev-guide/fe/sources/toss-ff/overview-4-principles.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-hidden-logic.md +59 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-http.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-use-user.md +110 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-comparison-order.md +52 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-condition-name.md +64 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-login-start-page.md +183 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-magic-number.md +53 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-submit-button.md +73 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-ternary-operator.md +38 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-use-page-state.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-user-policy.md +98 -0
- package/assets/dev-guide/fe/sources/vue/INDEX.md +17 -0
- package/assets/dev-guide/fe/sources/vue/composition-api.md +251 -0
- package/assets/dev-guide/fe/sources/vue/nuxt-data-fetching.md +232 -0
- package/assets/dev-guide/fe/sources/vue/pinia-state-management.md +134 -0
- package/assets/dev-guide/fe/sources/vue/reactivity-pitfalls.md +261 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-a.md +117 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-b.md +231 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-c.md +86 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-d.md +72 -0
- package/dist/cli.js +42 -0
- package/dist/core/dashboard-cli.d.ts +12 -0
- package/dist/core/dashboard-cli.js +226 -0
- package/dist/core/dev-guide-injector.d.ts +26 -0
- package/dist/core/dev-guide-injector.js +137 -0
- package/dist/core/init.js +53 -0
- package/dist/core/lifecycle-classifier.d.ts +23 -0
- package/dist/core/lifecycle-classifier.js +104 -0
- package/dist/core/observability-backfill.d.ts +31 -0
- package/dist/core/observability-backfill.js +178 -0
- package/dist/core/observability-store.d.ts +58 -0
- package/dist/core/observability-store.js +195 -0
- package/dist/core/session-store.js +4 -0
- package/dist/core/spawn.d.ts +17 -0
- package/dist/core/spawn.js +179 -2
- package/dist/core/statusline-cli.js +34 -1
- package/dist/engine/compound-extractor.js +39 -0
- package/dist/engine/compound-loop.js +6 -0
- package/dist/engine/compound-retire.d.ts +20 -0
- package/dist/engine/compound-retire.js +85 -0
- package/dist/hooks/context-guard.js +25 -1
- package/dist/hooks/post-tool-use.js +48 -0
- package/dist/hooks/solution-injector.js +93 -0
- package/dist/host/install-claude.d.ts +6 -2
- package/dist/host/install-claude.js +74 -2
- package/dist/host/install-codex.d.ts +4 -0
- package/dist/host/install-codex.js +71 -0
- package/dist/host/install-orchestrator.js +1 -0
- package/package.json +6 -6
- package/plugin.json +1 -1
- package/scripts/postinstall.js +134 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Next.js App Router 캐싱 전략 (Next.js 16 기준)
|
|
3
|
+
source: https://nextjs.org/docs/app/building-your-application/caching
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
category: caching
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 개요
|
|
9
|
+
|
|
10
|
+
Next.js 16의 캐싱 모델은 `cacheComponents` 플래그 도입으로 재편되었다.
|
|
11
|
+
데이터는 기본 **비캐싱(dynamic)**, 필요한 곳에만 `use cache` 지시어로 캐싱 opt-in.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// next.config.ts
|
|
15
|
+
import type { NextConfig } from 'next'
|
|
16
|
+
|
|
17
|
+
const nextConfig: NextConfig = {
|
|
18
|
+
cacheComponents: true, // use cache + PPR 통합 활성화
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default nextConfig
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 캐싱 계층 구조
|
|
27
|
+
|
|
28
|
+
### 1. fetch() 캐싱
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// 캐시 저장
|
|
32
|
+
const data = await fetch('https://api.example.com/data', {
|
|
33
|
+
cache: 'force-cache'
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// 캐시 없음 (기본값)
|
|
37
|
+
const data = await fetch('https://api.example.com/data')
|
|
38
|
+
// 또는
|
|
39
|
+
const data = await fetch('https://api.example.com/data', {
|
|
40
|
+
cache: 'no-store'
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// 시간 기반 재검증 (3600초 = 1시간)
|
|
44
|
+
const data = await fetch('https://api.example.com/data', {
|
|
45
|
+
next: { revalidate: 3600 }
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// 태그 기반 온디맨드 재검증
|
|
49
|
+
const data = await fetch('https://api.example.com/users', {
|
|
50
|
+
next: { tags: ['user'] }
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. unstable_cache (non-fetch 함수)
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { unstable_cache } from 'next/cache'
|
|
58
|
+
import { db } from '@/lib/db'
|
|
59
|
+
|
|
60
|
+
export const getCachedUser = unstable_cache(
|
|
61
|
+
async (id: string) => {
|
|
62
|
+
return db.select().from(users).where(eq(users.id, id)).then(r => r[0])
|
|
63
|
+
},
|
|
64
|
+
['user'], // 캐시 키 prefix
|
|
65
|
+
{
|
|
66
|
+
tags: ['user'],
|
|
67
|
+
revalidate: 3600,
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 3. use cache 지시어 (cacheComponents 활성화 시)
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// 컴포넌트 레벨 캐싱
|
|
76
|
+
async function CachedDashboard() {
|
|
77
|
+
'use cache'
|
|
78
|
+
const data = await fetchDashboardData()
|
|
79
|
+
return <Dashboard data={data} />
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 함수 레벨 캐싱
|
|
83
|
+
async function getCachedData(id: string) {
|
|
84
|
+
'use cache'
|
|
85
|
+
return await db.query.findById(id)
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 4. React cache (요청 단위 메모이제이션)
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { cache } from 'react'
|
|
93
|
+
|
|
94
|
+
// 단일 렌더 패스 내 중복 요청 제거
|
|
95
|
+
export const getPost = cache(async (id: string) => {
|
|
96
|
+
const post = await db.query.posts.findFirst({
|
|
97
|
+
where: eq(posts.id, parseInt(id)),
|
|
98
|
+
})
|
|
99
|
+
return post
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 온디맨드 재검증
|
|
106
|
+
|
|
107
|
+
### revalidateTag — 태그로 무효화
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import { revalidateTag } from 'next/cache'
|
|
111
|
+
|
|
112
|
+
export async function updateUser(id: string) {
|
|
113
|
+
await db.update(users).set({ name: 'new' }).where(eq(users.id, id))
|
|
114
|
+
revalidateTag('user') // 'user' 태그를 가진 모든 캐시 무효화
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### revalidatePath — 경로로 무효화
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { revalidatePath } from 'next/cache'
|
|
122
|
+
|
|
123
|
+
export async function updatePost() {
|
|
124
|
+
await db.update(posts)...
|
|
125
|
+
revalidatePath('/blog') // /blog 경로의 모든 캐시 무효화
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Route Segment Config
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// layout.tsx | page.tsx | route.ts
|
|
135
|
+
|
|
136
|
+
// 항상 동적 렌더링
|
|
137
|
+
export const dynamic = 'force-dynamic'
|
|
138
|
+
|
|
139
|
+
// 항상 정적 (빌드 시 생성)
|
|
140
|
+
export const dynamic = 'force-static'
|
|
141
|
+
|
|
142
|
+
// 기본 재검증 주기 설정 (초)
|
|
143
|
+
export const revalidate = 3600
|
|
144
|
+
|
|
145
|
+
// fetch 캐시 정책 일괄 설정
|
|
146
|
+
export const fetchCache = 'force-cache'
|
|
147
|
+
// 'auto' | 'default-cache' | 'only-cache' | 'force-cache'
|
|
148
|
+
// 'default-no-store' | 'only-no-store' | 'force-no-store'
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 데이터 Preload 패턴
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// utils/get-item.ts
|
|
157
|
+
import { cache } from 'react'
|
|
158
|
+
import 'server-only'
|
|
159
|
+
|
|
160
|
+
export const getItem = cache(async (id: string) => {
|
|
161
|
+
// DB 또는 API 호출
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// 블로킹 작업 전에 미리 데이터 로딩 시작
|
|
165
|
+
export const preload = (id: string) => {
|
|
166
|
+
void getItem(id)
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// page.tsx
|
|
172
|
+
import { getItem, preload, checkIsAvailable } from '@/lib/data'
|
|
173
|
+
|
|
174
|
+
export default async function Page({ params }) {
|
|
175
|
+
const { id } = await params
|
|
176
|
+
preload(id) // 즉시 데이터 로딩 시작
|
|
177
|
+
const isAvailable = await checkIsAvailable() // 병렬 진행
|
|
178
|
+
return isAvailable ? <Item id={id} /> : null
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 재검증 빈도 규칙
|
|
185
|
+
|
|
186
|
+
- 라우트의 재검증 빈도 = 해당 라우트 내 레이아웃+페이지 중 **가장 낮은 revalidate 값**
|
|
187
|
+
- 개별 fetch가 라우트 기본값보다 낮은 revalidate를 갖고 있으면 그 값이 전체 라우트에 영향
|
|
188
|
+
- `revalidate` 값은 정적으로 분석 가능해야 함 (`revalidate = 600` O, `revalidate = 60 * 10` X)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Next.js Server Components & Client Components
|
|
3
|
+
source: https://nextjs.org/docs/app/building-your-application/rendering/server-components
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
category: caching
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 기본 원칙
|
|
9
|
+
|
|
10
|
+
App Router에서 레이아웃과 페이지는 기본적으로 **Server Component**.
|
|
11
|
+
상호작용·브라우저 API가 필요한 경우에만 `'use client'`로 Client Component 지정.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Server vs Client 선택 기준
|
|
16
|
+
|
|
17
|
+
| 필요한 것 | 권장 컴포넌트 |
|
|
18
|
+
|-----------|--------------|
|
|
19
|
+
| DB/API 직접 접근 | Server |
|
|
20
|
+
| API 키·시크릿 보호 | Server |
|
|
21
|
+
| JS 번들 크기 최소화 | Server |
|
|
22
|
+
| FCP 개선, 점진적 스트리밍 | Server |
|
|
23
|
+
| `onClick`, `onChange` 등 이벤트 핸들러 | Client |
|
|
24
|
+
| `useState`, `useEffect` | Client |
|
|
25
|
+
| `localStorage`, `window` 등 브라우저 API | Client |
|
|
26
|
+
| 커스텀 훅 (상태 기반) | Client |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 렌더링 흐름
|
|
31
|
+
|
|
32
|
+
### 서버 측
|
|
33
|
+
1. Server Component → RSC Payload (바이너리 포맷) 생성
|
|
34
|
+
2. Client Component + RSC Payload → HTML prerender
|
|
35
|
+
|
|
36
|
+
> **RSC Payload**: Server Component 렌더 결과 + Client Component 위치 플레이스홀더 + 참조 파일 경로
|
|
37
|
+
|
|
38
|
+
### 클라이언트 측 (최초 로드)
|
|
39
|
+
1. HTML → 즉시 non-interactive 프리뷰 표시
|
|
40
|
+
2. RSC Payload → Client/Server 컴포넌트 트리 조정(reconcile)
|
|
41
|
+
3. JavaScript → Client Component 하이드레이션 (인터랙티브)
|
|
42
|
+
|
|
43
|
+
### 이후 네비게이션
|
|
44
|
+
- RSC Payload prefetch + 캐시 → 즉시 네비게이션
|
|
45
|
+
- Client Component는 클라이언트에서만 렌더 (서버 HTML 없음)
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 코드 예시
|
|
50
|
+
|
|
51
|
+
### 기본 패턴: Server + Client 조합
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
// app/[id]/page.tsx — Server Component
|
|
55
|
+
import LikeButton from '@/app/ui/like-button'
|
|
56
|
+
import { getPost } from '@/lib/data'
|
|
57
|
+
|
|
58
|
+
export default async function Page({
|
|
59
|
+
params,
|
|
60
|
+
}: {
|
|
61
|
+
params: Promise<{ id: string }>
|
|
62
|
+
}) {
|
|
63
|
+
const { id } = await params
|
|
64
|
+
const post = await getPost(id) // 서버에서 직접 DB 접근
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div>
|
|
68
|
+
<h1>{post.title}</h1>
|
|
69
|
+
<LikeButton likes={post.likes} /> {/* Client Component에 props 전달 */}
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
// app/ui/like-button.tsx — Client Component
|
|
77
|
+
'use client'
|
|
78
|
+
|
|
79
|
+
import { useState } from 'react'
|
|
80
|
+
|
|
81
|
+
export default function LikeButton({ likes }: { likes: number }) {
|
|
82
|
+
const [count, setCount] = useState(likes)
|
|
83
|
+
return <button onClick={() => setCount(c => c + 1)}>{count} Likes</button>
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### JS 번들 최소화 패턴
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
// app/layout.tsx — Server Component
|
|
91
|
+
import Search from './search' // Client Component (검색창만)
|
|
92
|
+
import Logo from './logo' // Server Component (정적)
|
|
93
|
+
|
|
94
|
+
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
95
|
+
return (
|
|
96
|
+
<>
|
|
97
|
+
<nav>
|
|
98
|
+
<Logo /> {/* 서버 렌더 */}
|
|
99
|
+
<Search /> {/* 클라이언트 번들 포함 */}
|
|
100
|
+
</nav>
|
|
101
|
+
<main>{children}</main>
|
|
102
|
+
</>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Server를 Client에 children으로 전달
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
// 'use client' Modal에 Server Component인 Cart를 children으로 전달 가능
|
|
111
|
+
// app/ui/modal.tsx
|
|
112
|
+
'use client'
|
|
113
|
+
export default function Modal({ children }: { children: React.ReactNode }) {
|
|
114
|
+
return <div>{children}</div>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// app/page.tsx
|
|
118
|
+
import Modal from './ui/modal'
|
|
119
|
+
import Cart from './ui/cart' // Server Component
|
|
120
|
+
|
|
121
|
+
export default function Page() {
|
|
122
|
+
return (
|
|
123
|
+
<Modal>
|
|
124
|
+
<Cart /> {/* 서버에서 미리 렌더링됨 */}
|
|
125
|
+
</Modal>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Context Provider 패턴
|
|
133
|
+
|
|
134
|
+
React context는 Server Component에서 직접 사용 불가. Client Component로 감싸기:
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
// app/theme-provider.tsx
|
|
138
|
+
'use client'
|
|
139
|
+
import { createContext } from 'react'
|
|
140
|
+
|
|
141
|
+
export const ThemeContext = createContext({})
|
|
142
|
+
|
|
143
|
+
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
144
|
+
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
// app/layout.tsx — Server Component
|
|
150
|
+
import ThemeProvider from './theme-provider'
|
|
151
|
+
|
|
152
|
+
export default function RootLayout({ children }) {
|
|
153
|
+
return (
|
|
154
|
+
<html>
|
|
155
|
+
<body>
|
|
156
|
+
<ThemeProvider>{children}</ThemeProvider>
|
|
157
|
+
{/* Provider를 트리 가능한 한 깊숙이 배치 → 정적 부분 최적화 */}
|
|
158
|
+
</body>
|
|
159
|
+
</html>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 환경 오염 방지
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
// lib/data.js — 서버 전용 강제
|
|
170
|
+
import 'server-only' // 클라이언트에서 import 시 빌드 에러
|
|
171
|
+
|
|
172
|
+
export async function getData() {
|
|
173
|
+
const res = await fetch('https://api.example.com', {
|
|
174
|
+
headers: { authorization: process.env.API_KEY }, // 서버에서만 접근
|
|
175
|
+
})
|
|
176
|
+
return res.json()
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
- `NEXT_PUBLIC_` 접두사 없는 환경변수는 클라이언트 번들에서 빈 문자열로 치환됨
|
|
181
|
+
- 반대로 클라이언트 전용 코드: `client-only` 패키지 사용
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: PPR — Partial Prerendering (부분 프리렌더링)
|
|
3
|
+
source: https://nextjs.org/docs/app/api-reference/next-config-js/ppr
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
category: caching
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 개요
|
|
9
|
+
|
|
10
|
+
PPR(Partial Prerendering)은 단일 라우트에서 **정적 콘텐츠(HTML shell)**와 **동적 콘텐츠(스트리밍)**를 혼합한다.
|
|
11
|
+
빌드 시 정적 shell을 즉시 제공하고, 동적 부분은 준비되는 대로 스트림.
|
|
12
|
+
|
|
13
|
+
**Next.js 16**: `cacheComponents: true` 설정 시 PPR이 App Router 기본 동작.
|
|
14
|
+
`experimental.ppr` 플래그와 `experimental_ppr` 라우트 세그먼트 설정은 제거됨.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 활성화 (Next.js 16)
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// next.config.ts
|
|
22
|
+
import type { NextConfig } from 'next'
|
|
23
|
+
|
|
24
|
+
const nextConfig: NextConfig = {
|
|
25
|
+
cacheComponents: true, // PPR + use cache + dynamicIO 통합 활성화
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default nextConfig
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 동작 원리
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
요청 수신
|
|
37
|
+
↓
|
|
38
|
+
정적 HTML shell → 즉시 응답 (TTFB 최소화)
|
|
39
|
+
↓
|
|
40
|
+
동적 콘텐츠 → Suspense 경계를 통해 스트리밍
|
|
41
|
+
↓
|
|
42
|
+
최종 완성된 페이지
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Suspense로 정적/동적 경계 설정
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// app/page.tsx
|
|
51
|
+
import { Suspense } from 'react'
|
|
52
|
+
import { StaticContent } from './static-content'
|
|
53
|
+
import { DynamicFeed } from './dynamic-feed'
|
|
54
|
+
|
|
55
|
+
export default function Page() {
|
|
56
|
+
return (
|
|
57
|
+
<main>
|
|
58
|
+
{/* 정적 — 빌드 시 HTML에 포함 */}
|
|
59
|
+
<StaticContent />
|
|
60
|
+
|
|
61
|
+
{/* 동적 — Suspense 경계 안에서 스트리밍 */}
|
|
62
|
+
<Suspense fallback={<FeedSkeleton />}>
|
|
63
|
+
<DynamicFeed />
|
|
64
|
+
</Suspense>
|
|
65
|
+
</main>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
// dynamic-feed.tsx
|
|
72
|
+
async function DynamicFeed() {
|
|
73
|
+
// 이 함수는 request time에 실행됨 (cookies, headers 등 사용 가능)
|
|
74
|
+
const feed = await getUserFeed()
|
|
75
|
+
return <Feed items={feed} />
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## use cache 지시어와 함께 사용
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
// cacheComponents 활성화 시 컴포넌트/함수 레벨 캐싱
|
|
85
|
+
async function CachedSidebar() {
|
|
86
|
+
'use cache'
|
|
87
|
+
const nav = await getNavigation() // 캐시됨 (정적 shell에 포함)
|
|
88
|
+
return <Sidebar items={nav} />
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function DynamicUserPanel() {
|
|
92
|
+
// use cache 없음 → 동적 (스트리밍)
|
|
93
|
+
const user = await getCurrentUser()
|
|
94
|
+
return <UserPanel user={user} />
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Activity 기반 네비게이션 (cacheComponents 활성화 시)
|
|
101
|
+
|
|
102
|
+
React `<Activity>` 컴포넌트로 클라이언트 네비게이션 시 이전 라우트 상태 보존:
|
|
103
|
+
- 라우트 이동 시 이전 라우트를 unmount 대신 `"hidden"` 모드로 전환
|
|
104
|
+
- 뒤로 가기 시 상태 그대로 복원 (form 입력, 스크롤 위치 등)
|
|
105
|
+
- `"hidden"` 상태에서 effects 정리, 복귀 시 재생성
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 이전 버전 (Next.js 13-15) experimental PPR
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// next.config.ts (Next.js 13-15)
|
|
113
|
+
const nextConfig = {
|
|
114
|
+
experimental: {
|
|
115
|
+
ppr: 'incremental', // 또는 true
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 라우트 세그먼트에서 opt-in
|
|
120
|
+
export const experimental_ppr = true
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## PPR vs 기존 방식 비교
|
|
126
|
+
|
|
127
|
+
| 방식 | 특징 |
|
|
128
|
+
|------|------|
|
|
129
|
+
| 완전 정적 (SSG) | 빌드 시 모두 생성, 개인화 불가 |
|
|
130
|
+
| 완전 동적 (SSR) | 매 요청마다 렌더, TTFB 높음 |
|
|
131
|
+
| **PPR** | 정적 shell 즉시 + 동적 부분 스트리밍 |
|
|
132
|
+
|
|
133
|
+
PPR은 첫 HTML 응답 속도(≒ TTFB)와 개인화 동적 콘텐츠를 동시에 달성.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Next.js Image 컴포넌트 최적화 가이드
|
|
3
|
+
source: https://nextjs.org/docs/app/api-reference/components/image
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
category: images
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 개요
|
|
9
|
+
|
|
10
|
+
`next/image`는 HTML `<img>`를 확장하여 자동 이미지 최적화를 제공한다:
|
|
11
|
+
- 자동 WebP/AVIF 변환
|
|
12
|
+
- 반응형 srcset 생성
|
|
13
|
+
- Lazy loading 기본 적용
|
|
14
|
+
- CLS 방지를 위한 크기 예약
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 기본 사용
|
|
19
|
+
|
|
20
|
+
```jsx
|
|
21
|
+
import Image from 'next/image'
|
|
22
|
+
|
|
23
|
+
export default function Page() {
|
|
24
|
+
return (
|
|
25
|
+
<Image
|
|
26
|
+
src="/profile.png"
|
|
27
|
+
width={500}
|
|
28
|
+
height={500}
|
|
29
|
+
alt="프로필 사진"
|
|
30
|
+
/>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 핵심 Props 레퍼런스
|
|
38
|
+
|
|
39
|
+
| Prop | 타입 | 기본값 | 설명 |
|
|
40
|
+
|------|------|--------|------|
|
|
41
|
+
| `src` | String | 필수 | 내부 경로, 외부 URL, static import |
|
|
42
|
+
| `alt` | String | 필수 | 접근성 대체 텍스트 |
|
|
43
|
+
| `width` | Integer(px) | - | 인트린식 너비 (aspect ratio 계산용) |
|
|
44
|
+
| `height` | Integer(px) | - | 인트린식 높이 |
|
|
45
|
+
| `fill` | Boolean | false | 부모 요소 크기에 맞게 채우기 |
|
|
46
|
+
| `sizes` | String | - | 반응형 이미지 크기 힌트 |
|
|
47
|
+
| `quality` | Integer(1-100) | 75 | 최적화 품질 |
|
|
48
|
+
| `loading` | String | `'lazy'` | `'lazy'` \| `'eager'` |
|
|
49
|
+
| `preload` | Boolean | false | `<link rel="preload">` 삽입 |
|
|
50
|
+
| `placeholder` | String | `'empty'` | `'empty'` \| `'blur'` \| data URL |
|
|
51
|
+
| `blurDataURL` | String | - | blur placeholder용 base64 이미지 |
|
|
52
|
+
| `unoptimized` | Boolean | false | 최적화 비활성화 |
|
|
53
|
+
| `decoding` | String | `'async'` | 이미지 디코딩 힌트 |
|
|
54
|
+
|
|
55
|
+
> **주의**: `priority` prop은 Next.js 16에서 deprecated → `preload` 사용 권장
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## LCP 이미지 최적화
|
|
60
|
+
|
|
61
|
+
```jsx
|
|
62
|
+
// 히어로 이미지 (LCP 요소) — 즉시 로딩
|
|
63
|
+
<Image
|
|
64
|
+
src="/hero.webp"
|
|
65
|
+
width={1200}
|
|
66
|
+
height={600}
|
|
67
|
+
alt="히어로 이미지"
|
|
68
|
+
preload={true} // <link rel="preload"> 삽입
|
|
69
|
+
loading="eager" // lazy loading 비활성화
|
|
70
|
+
quality={85}
|
|
71
|
+
/>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**언제 `preload={true}`를 쓰나:**
|
|
75
|
+
- LCP 요소인 경우
|
|
76
|
+
- 폴드 위(above the fold) 이미지
|
|
77
|
+
- `<head>`에서 미리 로드하고 싶은 경우
|
|
78
|
+
|
|
79
|
+
**쓰지 말아야 할 때:**
|
|
80
|
+
- 뷰포트에 따라 LCP 요소가 달라지는 경우
|
|
81
|
+
- `loading` 또는 `fetchPriority` prop과 함께 사용 시
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## fill + sizes 반응형 패턴
|
|
86
|
+
|
|
87
|
+
```jsx
|
|
88
|
+
// fill: 부모 요소 크기에 맞게 채움
|
|
89
|
+
// 부모에 position: relative/fixed/absolute 필요
|
|
90
|
+
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
|
|
91
|
+
<Image
|
|
92
|
+
src="/banner.jpg"
|
|
93
|
+
fill
|
|
94
|
+
alt="배너"
|
|
95
|
+
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
|
96
|
+
style={{ objectFit: 'cover' }}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**sizes 미지정 시**: 브라우저는 이미지가 100vw라고 가정 → 불필요하게 큰 이미지 다운로드.
|
|
102
|
+
|
|
103
|
+
**sizes 효과**:
|
|
104
|
+
- `sizes` 없음: `1x, 2x` srcset 생성 (고정 크기)
|
|
105
|
+
- `sizes` 있음: `640w, 750w, 828w, ...` 전체 srcset 생성 (반응형)
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## placeholder blur 패턴
|
|
110
|
+
|
|
111
|
+
```jsx
|
|
112
|
+
import Image from 'next/image'
|
|
113
|
+
import profilePic from './profile.jpg' // 정적 import → blurDataURL 자동 생성
|
|
114
|
+
|
|
115
|
+
export default function Profile() {
|
|
116
|
+
return (
|
|
117
|
+
<Image
|
|
118
|
+
src={profilePic}
|
|
119
|
+
alt="프로필"
|
|
120
|
+
placeholder="blur" // 로딩 중 블러 표시
|
|
121
|
+
/>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
외부 이미지의 경우 `blurDataURL` 직접 제공:
|
|
127
|
+
```jsx
|
|
128
|
+
<Image
|
|
129
|
+
src="https://example.com/image.jpg"
|
|
130
|
+
width={500}
|
|
131
|
+
height={500}
|
|
132
|
+
alt="외부 이미지"
|
|
133
|
+
placeholder="blur"
|
|
134
|
+
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."
|
|
135
|
+
/>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 커스텀 loader
|
|
141
|
+
|
|
142
|
+
```jsx
|
|
143
|
+
'use client'
|
|
144
|
+
|
|
145
|
+
import Image from 'next/image'
|
|
146
|
+
|
|
147
|
+
const imageLoader = ({ src, width, quality }) => {
|
|
148
|
+
return `https://cdn.example.com/${src}?w=${width}&q=${quality || 75}`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export default function Page() {
|
|
152
|
+
return (
|
|
153
|
+
<Image
|
|
154
|
+
loader={imageLoader}
|
|
155
|
+
src="profile.jpg"
|
|
156
|
+
alt="프로필"
|
|
157
|
+
width={500}
|
|
158
|
+
height={500}
|
|
159
|
+
/>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 외부 이미지 설정 (next.config.ts)
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const nextConfig = {
|
|
170
|
+
images: {
|
|
171
|
+
remotePatterns: [
|
|
172
|
+
{
|
|
173
|
+
protocol: 'https',
|
|
174
|
+
hostname: 'cdn.example.com',
|
|
175
|
+
port: '',
|
|
176
|
+
pathname: '/images/**',
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
// 허용 품질 값 제한
|
|
180
|
+
qualities: [75, 85, 100],
|
|
181
|
+
// 포맷 우선순위
|
|
182
|
+
formats: ['image/avif', 'image/webp'],
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## CLS 방지 핵심
|
|
190
|
+
|
|
191
|
+
`width`와 `height`는 렌더 크기가 아닌 **aspect ratio 계산**에 사용된다.
|
|
192
|
+
브라우저가 이미지 로드 전 공간을 예약하여 레이아웃 이동(CLS) 방지.
|
|
193
|
+
|
|
194
|
+
```jsx
|
|
195
|
+
// fill 없이 사용 시 width + height 필수
|
|
196
|
+
<Image src="/img.jpg" width={800} height={600} alt="..." />
|
|
197
|
+
|
|
198
|
+
// fill 사용 시 width/height 불필요 (부모 크기 따라감)
|
|
199
|
+
<Image src="/img.jpg" fill alt="..." />
|
|
200
|
+
```
|