Loading...
Loading...
React/Next.js 앱의 성능을 분석하고 최적화하는 스킬. 번들 사이즈, 렌더링 최적화, 코드 분할, 이미지 최적화, SSR/SSG 전략 등. "성능", "performance", "최적화", "느려", "번들 사이즈", "렌더링" 등의 요청 시 사용.
npx skill4agent add ingpdw/pdw-fe-dev-tool fe-perf$ARGUMENTS// Bad — 매 렌더마다 새 객체 생성
function Parent() {
return <Child style={{ color: "red" }} data={[1, 2, 3]} />;
}
// Good — 안정적 참조
const style = { color: "red" };
const data = [1, 2, 3];
function Parent() {
return <Child style={style} data={data} />;
}// 비용이 큰 컴포넌트에만 적용
const ExpensiveList = memo(function ExpensiveList({ items }: Props) {
return items.map((item) => <ExpensiveItem key={item.id} item={item} />);
});// 비용이 큰 계산에 useMemo
const sortedItems = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// memo된 자식에 전달하는 콜백에 useCallback
const handleSelect = useCallback((id: string) => {
setSelectedId(id);
}, []);// 무거운 컴포넌트 lazy loading
import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import("@/components/HeavyChart"), {
loading: () => <Skeleton className="h-[400px]" />,
ssr: false, // 클라이언트 전용
});// Bad — 항상 로드
import { PDFViewer } from "react-pdf";
// Good — 필요할 때만 로드
const PDFViewer = dynamic(() =>
import("react-pdf").then((mod) => ({ default: mod.PDFViewer }))
);// Bad — 전체 라이브러리 import
import _ from "lodash";
_.debounce(fn, 300);
// Good — 개별 함수 import
import debounce from "lodash/debounce";
debounce(fn, 300);
// Best — 작은 대안 사용
import { useDebouncedCallback } from "use-debounce";# Vite 번들 분석
npx vite-bundle-visualizer
# Next.js 번들 분석
ANALYZE=true next build # @next/bundle-analyzer 필요| 무거운 라이브러리 | 가벼운 대안 |
|---|---|
| |
| 개별 import 또는 ES 네이티브 메서드 |
| |
| |
| |
// Next.js Image 컴포넌트 사용
import Image from "next/image";
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // LCP 이미지에 priority
placeholder="blur" // 블러 플레이스홀더
sizes="(max-width: 768px) 100vw, 50vw" // 반응형 크기
/>
// 아이콘 — SVG inline 또는 sprite
import { LucideIcon } from "lucide-react"; // 트리쉐이킹 지원// Server Component에서 데이터 가져오기 (제로 번들)
async function ProductList() {
const products = await db.product.findMany();
return products.map((p) => <ProductCard key={p.id} product={p} />);
}
// 병렬 데이터 페칭
async function Dashboard() {
const [users, orders, stats] = await Promise.all([
getUsers(),
getOrders(),
getStats(),
]);
// ...
}// 100+ 아이템 리스트에 가상화 적용
import { useVirtualizer } from "@tanstack/react-virtual";
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
<div ref={parentRef} className="h-[500px] overflow-auto">
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
height: virtualItem.size,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<ListItem item={items[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}// React Query 캐싱
const { data } = useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
staleTime: 5 * 60 * 1000, // 5분간 fresh
gcTime: 30 * 60 * 1000, // 30분간 캐시 유지
});
// Next.js fetch 캐싱
const data = await fetch("/api/data", {
next: { revalidate: 3600 }, // 1시간 ISR
});
// Next.js unstable_cache
import { unstable_cache } from "next/cache";
const getCachedData = unstable_cache(
async () => db.query(),
["cache-key"],
{ revalidate: 3600 }
);| 지표 | 목표 | 최적화 포인트 |
|---|---|---|
| LCP (Largest Contentful Paint) | < 2.5s | 히어로 이미지 priority, 폰트 preload |
| FID/INP (Interaction to Next Paint) | < 200ms | 무거운 연산 분리, useDeferredValue |
| CLS (Cumulative Layout Shift) | < 0.1 | 이미지 width/height 지정, 스켈레톤 |
| TTFB (Time to First Byte) | < 800ms | SSG/ISR, 에지 캐싱 |
# Performance Audit: [대상]
## 요약
- 주요 이슈: N개
- 예상 개선 효과: [번들 -NKB, 렌더링 -Nms 등]
## High Impact (우선 적용)
### [P1] 이슈 제목
- **영향**: [번들 사이즈 / 렌더링 / 로딩 속도]
- **현재**: 설명
- **개선안**: 코드
## Medium Impact
...
## Low Impact
...package.json