시니어 프론트엔드
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/ # 버튼, 입력, 카드
│ └── layout/ # 헤더, 푸터, 사이드바
├── hooks/ # useDebounce, useLocalStorage
├── lib/ # 유틸리티 (cn), 상수
├── types/ # TypeScript 인터페이스
├── tailwind.config.ts
├── next.config.js
└── package.json
컴포넌트 생성
TypeScript, 테스트, Storybook 스토리가 포함된 React 컴포넌트를 생성합니다.
워크플로우: 새 컴포넌트 만들기
-
클라이언트 컴포넌트를 생성합니다:
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 -
무거운 의존성을 대체하여 권장 수정 사항을 적용합니다.
-
상세 모드로 다시 실행하여 가져오기 패턴을 확인합니다:
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>One</Tabs.Tab>
<Tabs.Tab>Two</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>Content 1</Tabs.Panel>
<Tabs.Panel>Content 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); // 서버 측 데이터 가져오기
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: [{ protocol: 'https', hostname: 'cdn.example.com' }],
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 - Forcing-question 라이브러리 (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 접근성 하한 + 성능 하한
세 가지 중 하나라도 명시되지 않으면 권장 사항은 불완전합니다. forcing-question 라이브러리의 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: "..."}) 또는 ...
[번역을 위해 잘렸습니다]