@su-record/vibe 0.4.6 → 0.4.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.
@@ -0,0 +1,441 @@
1
+ # ⚡ TypeScript + Next.js 품질 규칙
2
+
3
+ ## 핵심 원칙 (core + React에서 상속)
4
+
5
+ ```markdown
6
+ ✅ 단일 책임 (SRP)
7
+ ✅ 중복 제거 (DRY)
8
+ ✅ 재사용성
9
+ ✅ 낮은 복잡도
10
+ ✅ 함수 ≤ 30줄, JSX ≤ 50줄
11
+ ✅ React 규칙 모두 적용
12
+ ```
13
+
14
+ ## Next.js 특화 규칙
15
+
16
+ ### 1. App Router (Next.js 13+) 우선
17
+
18
+ ```typescript
19
+ // ✅ App Router 구조
20
+ app/
21
+ ├── layout.tsx # 루트 레이아웃
22
+ ├── page.tsx # 홈 페이지
23
+ ├── loading.tsx # 로딩 UI
24
+ ├── error.tsx # 에러 UI
25
+ ├── not-found.tsx # 404 페이지
26
+ ├── users/
27
+ │ ├── page.tsx # /users
28
+ │ ├── [id]/
29
+ │ │ └── page.tsx # /users/:id
30
+ │ └── loading.tsx # /users 로딩
31
+ └── api/
32
+ └── users/
33
+ └── route.ts # API Route
34
+
35
+ // ✅ 서버 컴포넌트 (기본)
36
+ export default async function UsersPage() {
37
+ // 서버에서 데이터 페칭
38
+ const users = await getUsers();
39
+
40
+ return (
41
+ <div>
42
+ <h1>Users</h1>
43
+ <UserList users={users} />
44
+ </div>
45
+ );
46
+ }
47
+
48
+ // ✅ 클라이언트 컴포넌트 (필요 시에만)
49
+ 'use client';
50
+
51
+ import { useState } from 'react';
52
+
53
+ export function InteractiveButton() {
54
+ const [count, setCount] = useState(0);
55
+
56
+ return <button onClick={() => setCount(count + 1)}>{count}</button>;
57
+ }
58
+ ```
59
+
60
+ ### 2. 서버 컴포넌트 vs 클라이언트 컴포넌트
61
+
62
+ ```typescript
63
+ // ✅ 서버 컴포넌트 (권장)
64
+ // - 데이터 페칭
65
+ // - 환경 변수 접근
66
+ // - DB 직접 접근
67
+ // - 민감한 정보 처리
68
+
69
+ async function UserProfile({ userId }: { userId: string }) {
70
+ // 서버에서만 실행 (API 키 노출 안 됨)
71
+ const user = await db.user.findUnique({
72
+ where: { id: userId },
73
+ });
74
+
75
+ return <div>{user.name}</div>;
76
+ }
77
+
78
+ // ✅ 클라이언트 컴포넌트 (필요 시만)
79
+ // - useState, useEffect 사용
80
+ // - 이벤트 핸들러
81
+ // - 브라우저 API
82
+ // - 서드파티 라이브러리 (대부분)
83
+
84
+ 'use client';
85
+
86
+ function SearchBar() {
87
+ const [query, setQuery] = useState('');
88
+
89
+ return <input value={query} onChange={e => setQuery(e.target.value)} />;
90
+ }
91
+ ```
92
+
93
+ ### 3. Data Fetching 패턴
94
+
95
+ ```typescript
96
+ // ✅ 서버 컴포넌트에서 직접 fetch
97
+ async function PostsPage() {
98
+ // 자동 캐싱, 재검증
99
+ const posts = await fetch('https://api.example.com/posts', {
100
+ next: { revalidate: 60 }, // 60초 캐싱
101
+ }).then(res => res.json());
102
+
103
+ return <PostList posts={posts} />;
104
+ }
105
+
106
+ // ✅ 병렬 데이터 페칭
107
+ async function UserDashboard({ userId }: { userId: string }) {
108
+ const [user, posts, comments] = await Promise.all([
109
+ getUser(userId),
110
+ getUserPosts(userId),
111
+ getUserComments(userId),
112
+ ]);
113
+
114
+ return (
115
+ <div>
116
+ <UserCard user={user} />
117
+ <PostList posts={posts} />
118
+ <CommentList comments={comments} />
119
+ </div>
120
+ );
121
+ }
122
+
123
+ // ✅ 순차적 데이터 페칭 (의존 관계)
124
+ async function UserWithPosts({ username }: { username: string }) {
125
+ // 1. 사용자 조회
126
+ const user = await getUserByUsername(username);
127
+
128
+ // 2. 사용자 ID로 게시물 조회
129
+ const posts = await getUserPosts(user.id);
130
+
131
+ return (
132
+ <div>
133
+ <UserCard user={user} />
134
+ <PostList posts={posts} />
135
+ </div>
136
+ );
137
+ }
138
+ ```
139
+
140
+ ### 4. API Routes (Route Handlers)
141
+
142
+ ```typescript
143
+ // app/api/users/route.ts
144
+ import { NextRequest, NextResponse } from 'next/server';
145
+ import { z } from 'zod';
146
+
147
+ const createUserSchema = z.object({
148
+ email: z.string().email(),
149
+ name: z.string().min(1),
150
+ });
151
+
152
+ // ✅ GET /api/users
153
+ export async function GET(request: NextRequest) {
154
+ try {
155
+ const users = await db.user.findMany();
156
+ return NextResponse.json(users);
157
+ } catch (error) {
158
+ return NextResponse.json(
159
+ { error: 'Failed to fetch users' },
160
+ { status: 500 }
161
+ );
162
+ }
163
+ }
164
+
165
+ // ✅ POST /api/users
166
+ export async function POST(request: NextRequest) {
167
+ try {
168
+ const body = await request.json();
169
+ const data = createUserSchema.parse(body);
170
+
171
+ const user = await db.user.create({ data });
172
+
173
+ return NextResponse.json(user, { status: 201 });
174
+ } catch (error) {
175
+ if (error instanceof z.ZodError) {
176
+ return NextResponse.json({ error: error.errors }, { status: 400 });
177
+ }
178
+ return NextResponse.json(
179
+ { error: 'Failed to create user' },
180
+ { status: 500 }
181
+ );
182
+ }
183
+ }
184
+
185
+ // app/api/users/[id]/route.ts
186
+ // ✅ GET /api/users/:id
187
+ export async function GET(
188
+ request: NextRequest,
189
+ { params }: { params: { id: string } }
190
+ ) {
191
+ const user = await db.user.findUnique({
192
+ where: { id: params.id },
193
+ });
194
+
195
+ if (!user) {
196
+ return NextResponse.json({ error: 'User not found' }, { status: 404 });
197
+ }
198
+
199
+ return NextResponse.json(user);
200
+ }
201
+ ```
202
+
203
+ ### 5. Metadata & SEO
204
+
205
+ ```typescript
206
+ // ✅ 정적 메타데이터
207
+ import { Metadata } from 'next';
208
+
209
+ export const metadata: Metadata = {
210
+ title: 'My App',
211
+ description: 'My awesome app',
212
+ openGraph: {
213
+ title: 'My App',
214
+ description: 'My awesome app',
215
+ images: ['/og-image.png'],
216
+ },
217
+ };
218
+
219
+ // ✅ 동적 메타데이터
220
+ export async function generateMetadata({
221
+ params,
222
+ }: {
223
+ params: { id: string };
224
+ }): Promise<Metadata> {
225
+ const user = await getUser(params.id);
226
+
227
+ return {
228
+ title: user.name,
229
+ description: user.bio,
230
+ openGraph: {
231
+ title: user.name,
232
+ images: [user.avatar],
233
+ },
234
+ };
235
+ }
236
+ ```
237
+
238
+ ### 6. Streaming & Suspense
239
+
240
+ ```typescript
241
+ // ✅ Streaming으로 빠른 초기 렌더링
242
+ import { Suspense } from 'react';
243
+
244
+ export default function Dashboard() {
245
+ return (
246
+ <div>
247
+ <h1>Dashboard</h1>
248
+
249
+ {/* 빠른 컴포넌트 먼저 렌더 */}
250
+ <QuickStats />
251
+
252
+ {/* 느린 컴포넌트 나중에 스트리밍 */}
253
+ <Suspense fallback={<ChartSkeleton />}>
254
+ <SlowChart />
255
+ </Suspense>
256
+
257
+ <Suspense fallback={<TableSkeleton />}>
258
+ <SlowTable />
259
+ </Suspense>
260
+ </div>
261
+ );
262
+ }
263
+
264
+ async function SlowChart() {
265
+ const data = await fetchSlowData();
266
+ return <Chart data={data} />;
267
+ }
268
+ ```
269
+
270
+ ### 7. Server Actions
271
+
272
+ ```typescript
273
+ // ✅ Server Action (서버에서만 실행)
274
+ 'use server';
275
+
276
+ import { revalidatePath } from 'next/cache';
277
+
278
+ export async function createUser(formData: FormData) {
279
+ const email = formData.get('email') as string;
280
+ const name = formData.get('name') as string;
281
+
282
+ // 서버에서 직접 DB 접근
283
+ const user = await db.user.create({
284
+ data: { email, name },
285
+ });
286
+
287
+ // 캐시 재검증
288
+ revalidatePath('/users');
289
+
290
+ return user;
291
+ }
292
+
293
+ // ✅ 클라이언트에서 사용
294
+ 'use client';
295
+
296
+ import { createUser } from './actions';
297
+
298
+ export function CreateUserForm() {
299
+ return (
300
+ <form action={createUser}>
301
+ <input name="email" type="email" required />
302
+ <input name="name" required />
303
+ <button type="submit">Create</button>
304
+ </form>
305
+ );
306
+ }
307
+ ```
308
+
309
+ ### 8. Middleware
310
+
311
+ ```typescript
312
+ // middleware.ts (루트)
313
+ import { NextResponse } from 'next/server';
314
+ import type { NextRequest } from 'next/server';
315
+
316
+ export function middleware(request: NextRequest) {
317
+ // 인증 체크
318
+ const token = request.cookies.get('token')?.value;
319
+
320
+ if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
321
+ return NextResponse.redirect(new URL('/login', request.url));
322
+ }
323
+
324
+ // 헤더 추가
325
+ const response = NextResponse.next();
326
+ response.headers.set('X-Custom-Header', 'value');
327
+
328
+ return response;
329
+ }
330
+
331
+ export const config = {
332
+ matcher: ['/dashboard/:path*', '/api/:path*'],
333
+ };
334
+ ```
335
+
336
+ ### 9. 환경 변수
337
+
338
+ ```typescript
339
+ // ✅ 서버 전용 환경 변수
340
+ const dbUrl = process.env.DATABASE_URL; // 서버 컴포넌트에서만
341
+ const apiKey = process.env.API_SECRET_KEY;
342
+
343
+ // ✅ 클라이언트 노출 환경 변수 (NEXT_PUBLIC_ 접두사)
344
+ const publicUrl = process.env.NEXT_PUBLIC_API_URL;
345
+
346
+ // .env.local
347
+ DATABASE_URL=postgresql://...
348
+ API_SECRET_KEY=secret123
349
+ NEXT_PUBLIC_API_URL=https://api.example.com
350
+ ```
351
+
352
+ ### 10. 이미지 최적화
353
+
354
+ ```typescript
355
+ import Image from 'next/image';
356
+
357
+ // ✅ Next.js Image 컴포넌트 (자동 최적화)
358
+ export function UserAvatar({ user }: { user: User }) {
359
+ return (
360
+ <Image
361
+ src={user.avatar}
362
+ alt={user.name}
363
+ width={100}
364
+ height={100}
365
+ priority // LCP 이미지는 priority
366
+ />
367
+ );
368
+ }
369
+
370
+ // ✅ 외부 이미지 (next.config.js 설정 필요)
371
+ // next.config.js
372
+ module.exports = {
373
+ images: {
374
+ domains: ['example.com', 'cdn.example.com'],
375
+ },
376
+ };
377
+ ```
378
+
379
+ ## 안티패턴
380
+
381
+ ```typescript
382
+ // ❌ 클라이언트 컴포넌트에서 서버 전용 코드
383
+ 'use client';
384
+
385
+ function BadComponent() {
386
+ const data = await db.user.findMany(); // ❌ 클라이언트에서 DB 접근 불가
387
+ }
388
+
389
+ // ❌ 서버 컴포넌트에서 브라우저 API
390
+ async function BadServerComponent() {
391
+ const width = window.innerWidth; // ❌ window는 브라우저에만 존재
392
+ }
393
+
394
+ // ❌ API Route에서 다른 API Route 호출
395
+ export async function GET() {
396
+ const response = await fetch('http://localhost:3000/api/users'); // ❌
397
+ // 대신 직접 DB 함수 호출
398
+ const users = await getUsers(); // ✅
399
+ }
400
+
401
+ // ❌ 환경 변수 노출
402
+ 'use client';
403
+
404
+ function BadClient() {
405
+ const apiKey = process.env.API_SECRET_KEY; // ❌ undefined (클라이언트에서)
406
+ }
407
+ ```
408
+
409
+ ## 성능 최적화
410
+
411
+ ```typescript
412
+ // ✅ Static Generation (SSG)
413
+ export async function generateStaticParams() {
414
+ const posts = await getPosts();
415
+ return posts.map(post => ({ id: post.id }));
416
+ }
417
+
418
+ // ✅ Incremental Static Regeneration (ISR)
419
+ export const revalidate = 60; // 60초마다 재생성
420
+
421
+ // ✅ Dynamic Rendering (SSR)
422
+ export const dynamic = 'force-dynamic';
423
+
424
+ // ✅ Partial Prerendering (실험적)
425
+ export const experimental_ppr = true;
426
+ ```
427
+
428
+ ## 체크리스트
429
+
430
+ Next.js 코드 작성 시:
431
+
432
+ - [ ] App Router 사용 (Pages Router 지양)
433
+ - [ ] 서버 컴포넌트 우선 (클라이언트 최소화)
434
+ - [ ] API Route 대신 Server Action 고려
435
+ - [ ] 메타데이터 정의 (SEO)
436
+ - [ ] Suspense로 Streaming
437
+ - [ ] Next.js Image 컴포넌트 사용
438
+ - [ ] 환경 변수 올바르게 사용
439
+ - [ ] Middleware로 인증/권한 체크
440
+ - [ ] 캐싱 전략 설정 (revalidate)
441
+ - [ ] TypeScript 엄격 모드