Claude-skill-registry frontend-pattern

React 19 및 Next.js 16 패턴을 적용하는 스킬

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/frontend-pattern" ~/.claude/skills/majiayu000-claude-skill-registry-frontend-pattern && rm -rf "$T"
manifest: skills/data/frontend-pattern/SKILL.md
source content

Frontend Pattern Skill

이 스킬은 ForkLore 프로젝트의 프론트엔드 개발 패턴을 적용합니다.

Next.js 16 App Router 패턴

1. Server vs Client Components

// Server Component (기본값) - 데이터 페칭, SEO
// app/novels/page.tsx
export default async function NovelsPage() {
  const novels = await fetchNovels();  // 서버에서 직접 fetch
  return <NovelList novels={novels} />;
}

// 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 <button onClick={() => setLiked(!liked)}>♥</button>;
}

규칙:

  • 기본은 Server Component
  • useState
    ,
    useEffect
    , 이벤트 핸들러 필요시만
    'use client'

2. 데이터 페칭 패턴

// 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();
}

// 사용
<form action={createNovel}>
  <input name="title" />
  <button type="submit">생성</button>
</form>

3. 비동기 params/searchParams (Next.js 15+)

// ✅ 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. 새로운 훅

// use() - Promise/Context 직접 사용
import { use } from 'react';

function Comments({ commentsPromise }) {
  const comments = use(commentsPromise);  // Suspense와 함께 사용
  return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
}

// useOptimistic() - 낙관적 업데이트
function LikeButton({ count, onLike }) {
  const [optimisticCount, addOptimistic] = useOptimistic(count);
  
  async function handleLike() {
    addOptimistic(prev => prev + 1);
    await onLike();
  }
  
  return <button onClick={handleLike}>{optimisticCount}</button>;
}

// useFormStatus() - 폼 제출 상태
function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? '저장 중...' : '저장'}</button>;
}

// useActionState() - 서버 액션 상태
function Form() {
  const [state, formAction, isPending] = useActionState(createNovel, null);
  return <form action={formAction}>...</form>;
}

2. ref를 prop으로 전달 (forwardRef 불필요)

// ✅ React 19 - ref를 직접 prop으로
function Input({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

// ❌ 이전 방식 - forwardRef 사용
const Input = forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});

Shadcn/ui 패턴

1. 컴포넌트 사용

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';

function MyComponent({ className }) {
  return (
    <div className={cn("flex gap-2", className)}>
      <Input placeholder="제목" />
      <Button variant="default">저장</Button>
    </div>
  );
}

2. 폼 + Zod 검증

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 (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <FormField
          control={form.control}
          name="title"
          render={({ field }) => (
            <FormItem>
              <FormLabel>제목</FormLabel>
              <FormControl>
                <Input {...field} />
              </FormControl>
            </FormItem>
          )}
        />
      </form>
    </Form>
  );
}

타입 안전성 규칙

절대 금지:

  • as any
  • @ts-ignore
  • @ts-expect-error
// ❌ 금지
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 컴포넌트를 올바르게 사용하는가?