--- name: frontend-pattern description: React 19 및 Next.js 16 패턴을 적용하는 스킬 allowed-tools: - Read - Write - Edit - Grep - Glob --- # Frontend Pattern Skill 이 스킬은 ForkLore 프로젝트의 프론트엔드 개발 패턴을 적용합니다. ## Next.js 16 App Router 패턴 ### 1. Server vs Client Components ```typescript // Server Component (기본값) - 데이터 페칭, SEO // app/novels/page.tsx export default async function NovelsPage() { const novels = await fetchNovels(); // 서버에서 직접 fetch return ; } // Client Component - 상호작용 필요시만 // components/novel-like-button.tsx 'use client'; import { useState } from 'react'; export function NovelLikeButton({ novelId }: { novelId: number }) { const [liked, setLiked] = useState(false); return ; } ``` **규칙**: - 기본은 Server Component - `useState`, `useEffect`, 이벤트 핸들러 필요시만 `'use client'` ### 2. 데이터 페칭 패턴 ```typescript // Server Action (폼 제출, 데이터 변경) // app/actions/novel.ts 'use server'; export async function createNovel(formData: FormData) { const title = formData.get('title') as string; const response = await fetch('/api/novels', { method: 'POST', body: JSON.stringify({ title }), }); revalidatePath('/novels'); return response.json(); } // 사용
``` ### 3. 비동기 params/searchParams (Next.js 15+) ```typescript // ✅ Next.js 15+ 방식 (async 필수) export default async function Page({ params, searchParams, }: { params: Promise<{ id: string }>; searchParams: Promise<{ page?: string }>; }) { const { id } = await params; const { page } = await searchParams; // ... } // ❌ 이전 방식 (동기) - 더 이상 사용 금지 export default function Page({ params }: { params: { id: string } }) { // ... } ``` ## React 19 패턴 ### 1. 새로운 훅 ```typescript // use() - Promise/Context 직접 사용 import { use } from 'react'; function Comments({ commentsPromise }) { const comments = use(commentsPromise); // Suspense와 함께 사용 return ; } // useOptimistic() - 낙관적 업데이트 function LikeButton({ count, onLike }) { const [optimisticCount, addOptimistic] = useOptimistic(count); async function handleLike() { addOptimistic(prev => prev + 1); await onLike(); } return ; } // useFormStatus() - 폼 제출 상태 function SubmitButton() { const { pending } = useFormStatus(); return ; } // useActionState() - 서버 액션 상태 function Form() { const [state, formAction, isPending] = useActionState(createNovel, null); return
...
; } ``` ### 2. ref를 prop으로 전달 (forwardRef 불필요) ```typescript // ✅ React 19 - ref를 직접 prop으로 function Input({ ref, ...props }) { return ; } // ❌ 이전 방식 - forwardRef 사용 const Input = forwardRef((props, ref) => { return ; }); ``` ## Shadcn/ui 패턴 ### 1. 컴포넌트 사용 ```typescript import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { cn } from '@/lib/utils'; function MyComponent({ className }) { return (
); } ``` ### 2. 폼 + Zod 검증 ```typescript import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Form, FormField, FormItem, FormLabel, FormControl } from '@/components/ui/form'; const schema = z.object({ title: z.string().min(1, '제목은 필수입니다'), genre: z.enum(['FANTASY', 'ROMANCE', 'SF']), }); function NovelForm() { const form = useForm({ resolver: zodResolver(schema), defaultValues: { title: '', genre: 'FANTASY' }, }); return (
( 제목 )} /> ); } ``` ## 타입 안전성 규칙 **절대 금지**: - `as any` - `@ts-ignore` - `@ts-expect-error` ```typescript // ❌ 금지 const data = response as any; // ✅ 올바른 방법 interface NovelResponse { id: number; title: string; } const data: NovelResponse = await response.json(); ``` ## 체크리스트 - [ ] Server/Client 컴포넌트가 적절히 분리되었는가? - [ ] async params/searchParams를 사용하는가? (Next.js 15+) - [ ] 타입 안전성이 유지되는가? (any 금지) - [ ] React 19 훅을 적절히 활용하는가? - [ ] Shadcn/ui 컴포넌트를 올바르게 사용하는가?