시니어 프론트엔드
React/Next.js 애플리케이션을 위한 프론트엔드 개발 패턴, 성능 최적화 및 자동화 도구입니다.
목차
프로젝트 스캐폴딩
TypeScript, Tailwind CSS 및 모범 사례 구성이 포함된 새로운 Next.js 또는 React 프로젝트를 생성합니다.
워크플로: 새 프론트엔드 프로젝트 생성
-
프로젝트 이름과 템플릿을 사용하여 스캐폴더 실행:
python scripts/frontend_scaffolder.py my-app --template nextjs -
선택적 기능 추가 (인증, API, 폼, 테스팅, 스토리북):
python scripts/frontend_scaffolder.py dashboard --template nextjs --features auth,api -
프로젝트로 이동하여 의존성 설치:
cd my-app && npm install -
개발 서버 시작:
npm run dev
스캐폴더 옵션
| 옵션 | 설명 |
|---|---|
--template nextjs | App Router 및 서버 컴포넌트가 포함된 Next.js 14+ |
--template react | TypeScript를 사용하는 React + Vite |
--features auth | NextAuth.js 인증 추가 |
--features api | React Query + API 클라이언트 추가 |
--features forms | React Hook Form + Zod 검증 추가 |
--features testing | Vitest + Testing Library 추가 |
--dry-run | 파일을 생성하지 않고 미리보기 |
생성된 구조 (Next.js)
my-app/
├── app/
│ ├── layout.tsx # 폰트가 포함된 루트 레이아웃
│ ├── page.tsx # 홈 페이지
│ ├── globals.css # Tailwind + CSS 변수
│ └── api/health/route.ts
├── components/
│ ├── ui/ # Button, Input, Card
│ └── layout/ # Header, Footer, Sidebar
├── hooks/ # useDebounce, useLocalStorage
├── lib/ # utils (cn), 상수
├── types/ # TypeScript 인터페이스
├── tailwind.config.ts
├── next.config.js
└── package.json
컴포넌트 생성
테스트 및 Storybook 스토리가 포함된 React 컴포넌트를 TypeScript로 생성합니다.
워크플로: 새 컴포넌트 생성
-
클라이언트 컴포넌트 생성:
python scripts/component_generator.py Button --dir src/components/ui -
서버 컴포넌트 생성:
python scripts/component_generator.py ProductCard --type server -
테스트 및 스토리 파일과 함께 생성:
python scripts/component_generator.py UserProfile --with-test --with-story -
커스텀 훅 생성:
python scripts/component_generator.py FormValidation --type hook
생성기 옵션
| 옵션 | 설명 |
|---|---|
--type client | 'use client'가 포함된 클라이언트 컴포넌트 (기본값) |
--type server | 비동기 서버 컴포넌트 |
--type hook | 커스텀 React 훅 |
--with-test | 테스트 파일 포함 |
--with-story | Storybook 스토리 포함 |
--flat | 하위 디렉토리 없이 출력 디렉토리에 생성 |
--dry-run | 파일을 생성하지 않고 미리보기 |
생성된 컴포넌트 예시
'use client';
import { useState } from 'react';
import { cn } from '@/lib/utils';
interface ButtonProps {
className?: string;
children?: React.ReactNode;
}
export function Button({ className, children }: ButtonProps) {
return (
<div className={cn('', className)}>
{children}
</div>
);
}
번들 분석
번들 최적화 기회를 위해 package.json 및 프로젝트 구조를 분석합니다.
워크플로: 번들 크기 최적화
-
프로젝트에서 분석기 실행:
python scripts/bundle_analyzer.py /path/to/project -
건강 점수 및 문제 검토:
번들 건강 점수: 75/100 (C) 무거운 의존성: moment (290KB) 대안: date-fns (12KB) 또는 dayjs (2KB) lodash (71KB) 대안: lodash-es (트리 쉐이킹 적용) -
무거운 의존성을 대체하여 권장 수정 사항 적용.
-
import 패턴 확인을 위해 상세 모드로 재실행:
python scripts/bundle_analyzer.py . --verbose
번들 점수 해석
| 점수 | 등급 | 조치 |
|---|---|---|
| 90-100 | A | 번들이 잘 최적화됨 |
| 80-89 | B | 사소한 최적화 가능 |
| 70-79 | C | 무거운 의존성 교체 필요 |
| 60-69 | D | 여러 문제 해결 필요 |
| 0-59 | F | 심각한 번들 크기 문제 |
감지된 무거운 의존성
분석기는 다음과 같은 일반적인 무거운 패키지를 식별합니다:
| 패키지 | 크기 | 대안 |
|---|---|---|
| moment | 290KB | date-fns (12KB) 또는 dayjs (2KB) |
| lodash | 71KB | lodash-es (트리 쉐이킹 적용) |
| axios | 14KB | 기본 fetch 또는 ky (3KB) |
| jquery | 87KB | 기본 DOM API |
| @mui/material | 대용량 | shadcn/ui 또는 Radix UI |
React 패턴
참조: references/react_patterns.md
복합 컴포넌트
관련 컴포넌트 간에 상태 공유:
const Tabs = ({ children }) => {
const [active, setActive] = useState(0);
return (
<TabsContext.Provider value={{ active, setActive }}>
{children}
</TabsContext.Provider>
);
};
Tabs.List = TabList;
Tabs.Panel = TabPanel;
// 사용법
<Tabs>
<Tabs.List>
<Tabs.Tab>하나</Tabs.Tab>
<Tabs.Tab>둘</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>내용 1</Tabs.Panel>
<Tabs.Panel>내용 2</Tabs.Panel>
</Tabs>
커스텀 훅
재사용 가능한 로직 추출:
function useDebounce<T>(value: T, delay = 500): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// 사용법
const debouncedSearch = useDebounce(searchTerm, 300);
Render Props
렌더링 로직 공유:
function DataFetcher({ url, render }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url).then(r => r.json()).then(setData).finally(() => setLoading(false));
}, [url]);
return render({ data, loading });
}
// 사용법
<DataFetcher
url="/api/users"
render={({ data, loading }) =>
loading ? <Spinner /> : <UserList users={data} />
}
/>
Next.js 최적화
참조: references/nextjs_optimization_guide.md
서버 vs 클라이언트 컴포넌트
기본적으로 서버 컴포넌트를 사용합니다. 다음이 필요한 경우에만 'use client'를 추가합니다:
- 이벤트 핸들러 (onClick, onChange)
- 상태 (useState, useReducer)
- 효과 (useEffect)
- 브라우저 API
// 서버 컴포넌트 (기본값) - 'use client' 없음
async function ProductPage({ params }) {
const product = await getProduct(params.id); // 서버 측 fetch
return (
<div>
<h1>{product.name}</h1>
<AddToCartButton productId={product.id} /> {/* 클라이언트 컴포넌트 */}
</div>
);
}
// 클라이언트 컴포넌트
'use client';
function AddToCartButton({ productId }) {
const [adding, setAdding] = useState(false);
return <button onClick={() => addToCart(productId)}>추가</button>;
}
이미지 최적화
import Image from 'next/image';
// 스크롤 없이 보이는 영역 - 즉시 로드
<Image
src="/hero.jpg"
alt="히어로"
width={1200}
height={600}
priority
/>
// fill이 있는 반응형 이미지
<div className="relative aspect-video">
<Image
src="/product.jpg"
alt="제품"
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover"
/>
</div>
데이터 페칭 패턴
// 병렬 페칭
async function Dashboard() {
const [user, stats] = await Promise.all([
getUser(),
getStats()
]);
return <div>...</div>;
}
// Suspense를 사용한 스트리밍
async function ProductPage({ params }) {
return (
<div>
<ProductDetails id={params.id} />
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews productId={params.id} />
</Suspense>
</div>
);
}
접근성 및 테스트
참조: references/frontend_best_practices.md
접근성 체크리스트
- 시맨틱 HTML: 적절한 요소 사용 (
<button>,<nav>,<main>) - 키보드 탐색: 모든 상호작용 요소에 포커스 가능
- ARIA 레이블: 아이콘 및 복잡한 위젯에 레이블 제공
- 색상 대비: 일반 텍스트의 경우 최소 4.5:1
- 포커스 표시기: 포커스 상태 시각적으로 표시
// 접근 가능한 버튼
<button
type="button"
aria-label="대화상자 닫기"
onClick={onClose}
className="focus-visible:ring-2 focus-visible:ring-blue-500"
>
<XIcon aria-hidden="true" />
</button>
// 키보드 사용자를 위한 건너뛰기 링크
<a href="#main-content" className="sr-only focus:not-sr-only">
주요 콘텐츠로 건너뛰기
</a>
테스트 전략
// React Testing Library를 사용한 컴포넌트 테스트
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('버튼이 클릭 시 액션을 트리거합니다', async () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>클릭하세요</Button>);
await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});
// 접근성 테스트
test('대화상자에 접근 가능합니다', async () => {
render(<Dialog open={true} title="확인" />);
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByRole('dialog')).toHaveAttribute('aria-labelledby');
});
빠른 참조
일반적인 Next.js 구성
// next.config.js
const nextConfig = {
images: {
remotePatterns: [{ hostname: "cdnexamplecom" }],
formats: ['image/avif', 'image/webp'],
},
experimental: {
optimizePackageImports: ['lucide-react', '@heroicons/react'],
},
};
Tailwind CSS 유틸리티
// cn()을 사용한 조건부 클래스
import { cn } from '@/lib/utils';
<button className={cn(
'px-4 py-2 rounded',
variant === 'primary' && 'bg-blue-500 text-white',
disabled && 'opacity-50 cursor-not-allowed'
)} />
TypeScript 패턴
// children이 있는 Props
interface CardProps {
className?: string;
children: React.ReactNode;
}
// 제네릭 컴포넌트
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map(renderItem)}</ul>;
}
리소스
- React 패턴:
references/react_patterns.md - Next.js 최적화:
references/nextjs_optimization_guide.md - 모범 사례:
references/frontend_best_practices.md - 강제 질문 라이브러리 (Matt Pocock grill):
references/forcing_questions.md - 구성 맵 (포크할 전문가):
references/composition_map.md
가정 및 검증 가능한 성공 기준 (Karpathy 규율)
이 스킬이 컴포넌트를 스캐폴딩하거나, 프레임워크를 추천하거나, 번들을 감사하기 전에 다음 네 가지 가정이 반드시 표면화되어야 합니다.
- 기본 사용자 기기 + 네트워크 — 모바일-4G, 데스크톱-광섬유, 저사양-안드로이드 또는 회사-네트워크. 모든 성능 결정을 좌우합니다.
- 밀리초 단위의 LCP 목표 — "빠름"이 아닌 단일 숫자. 번들 예산과 렌더링 선택을 결정합니다.
- SEO 의존 vs 인증 장벽 — 렌더링 방식 결정 (SSR/SSG/RSC vs SPA).
- WCAG 목표 + 명명된 접근성 담당자 — AA, AAA 또는 최선의 노력. 접근성 투자 및 CI 게이트를 결정합니다.
검증 가능한 성공 기준 (Karpathy #4) — 모든 권장 사항에는 다음이 포함되어야 합니다:
- 기본 기기에서 p75 기준 Core Web Vitals 목표 (LCP, INP, CLS)
- 경로당 KB-gzip 단위의 JS 번들 예산
- Lighthouse 접근성 최소 기준 + 성능 최소 기준
위 세 가지 중 하나라도 명시되지 않으면 권장 사항이 불완전합니다 — 강제 질문 라이브러리의 Q2로 돌아가십시오.
scripts/frontend_decision_engine.py 도구는 이러한 검사를 인코딩합니다: 네 가지 가정 입력 없이는 프로필 추천을 거부하고, 일치하는 프로필에 대한 검증 가능한 임계값을 출력합니다.
사용자 정의 프로필
profiles/에 있는 네 가지 기본 제공 프로필은 모든 권장 사항을 보정합니다:
| 프로필 | 선택 시기 | LCP 목표 (모바일-4G p75) | 번들 예산 |
|---|---|---|---|
next-app-router | SaaS 고객 대상, SEO + 동적, RSC 우선 | 2000ms | 경로당 150 KB-gzip |
remix-or-sveltekit | 모바일-4G 우선, 저-JS 우선, 점진적 향상 | 1500ms | 경로당 80 KB-gzip |
vite-spa | 인증 장벽 앱, 데스크톱/회사 네트워크 우선 | 2500ms | 초기 200 KB + 경로당 80 KB |
astro-or-static | 마케팅/문서/블로그, 거의 쓰기 없음, SEO 중요 | 1200ms | 페이지당 30 KB JS |
다음과 같이 프로필을 선택하세요:
python scripts/frontend_decision_engine.py \
--primary-device mobile-4g --lcp-target-ms 2000 \
--seo-dependent true --auth-walled false --team-size 5
도구는 최적 프로필, 15% 이내인 차순위 대안, 스택 선택, 해당 프로필에서 피해야 할 안티 패턴, 필요한 CI 게이트를 반환합니다.
사용자 정의 프로필을 추가하려면 (예: 조직의 내부 도구 기본값): profiles/vite-spa.json을 profiles/<your-org>.json으로 복사하고 constraints 및 success_thresholds를 조정하세요.
구성 맵
이 스킬은 POWERFUL 등급 전문가가 소유한 범위를 재구현하지 않습니다. 대신 해당 전문가로 포크합니다. 전체 라우팅 테이블은 references/composition_map.md를 참조하세요. 주요 포크는 다음과 같습니다:
| 영역 | 포크 대상 |
|---|---|
| WCAG 감사, 대비, 스크린 리더 | engineering-team/skills/a11y-audit/ |
| 번들 프로파일링 + 런타임 성능 | engineering/skills/performance-profiler/ |
| 시네마틱/스크롤 스토리텔링 랜딩 | engineering-team/skills/epic-design/ |
| Apple HIG (iOS / macOS / visionOS) | product-team/skills/apple-hig-expert/ |
| 커밋 전 Karpathy 리뷰 | engineering/karpathy-coder/ |
| 사전 아키텍처 그릴 | engineering/grill-me/ |
cs-frontend-engineer 에이전트는 context: fork를 통해 이러한 포크를 조정합니다. 다른 에이전트에서 Agent({subagent_type: "cs-frontend-engineer", prompt: "..."}) 또는 /cs:frontend-rev를 통해 호출하세요.