Performance Optimization
성능 최적화를 통해 애플리케이션의 응답성과 사용자 경험을 개선하는 패턴입니다.
개요
성능 최적화는 다음과 같은 경우에 필요합니다:
- 렌더링 성능: 불필요한 리렌더링을 방지하고 렌더링 속도를 개선
- 번들 크기: JavaScript 번들 크기를 줄여 초기 로딩 속도 개선
- 데이터 로딩: API 호출과 데이터 처리를 최적화
- 메모리 관리: 메모리 누수를 방지하고 메모리 사용량을 최적화
- 이미지 최적화: 이미지 로딩과 표시를 최적화
기본 패턴
1. React.memo를 활용한 컴포넌트 메모이제이션
불필요한 리렌더링을 방지하기 위해 React.memo를 사용합니다.
// src/components/UserCard.tsx
import { memo } from "react";
interface UserCardProps {
user: {
id: string;
name: string;
email: string;
};
onEdit: (id: string) => void;
}
// React.memo로 props가 변경되지 않으면 리렌더링 방지
export const UserCard = memo(({ user, onEdit }: UserCardProps) => {
console.log("UserCard rendered:", user.name);
return (
<div className="p-4 border rounded-lg">
<h3 className="text-lg font-semibold">{user.name}</h3>
<p className="text-sm text-gray-600">{user.email}</p>
<button
onClick={() => onEdit(user.id)}
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
>
수정
</button>
</div>
);
});
UserCard.displayName = "UserCard";
// 사용 예시
export function UserList() {
const [users, setUsers] = useState([
{ id: "1", name: "Alice", email: "alice@example.com" },
{ id: "2", name: "Bob", email: "bob@example.com" },
]);
const [count, setCount] = useState(0);
const handleEdit = (id: string) => {
console.log("Edit user:", id);
};
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<div className="mt-4 space-y-2">
{users.map((user) => (
<UserCard key={user.id} user={user} onEdit={handleEdit} />
))}
</div>
</div>
);
}2. useMemo와 useCallback 최적화
계산 비용이 높은 연산과 함수를 메모이제이션합니다.
// src/hooks/useExpensiveCalculation.ts
import { useMemo, useCallback, useState } from "react";
interface FilterOptions {
search: string;
category: string;
priceRange: [number, number];
}
export function useProductFilter(products: Product[]) {
const [filters, setFilters] = useState<FilterOptions>({
search: "",
category: "all",
priceRange: [0, 1000],
});
// 비용이 높은 필터링 연산을 메모이제이션
const filteredProducts = useMemo(() => {
console.log("Filtering products...");
return products.filter((product) => {
const matchesSearch = product.name
.toLowerCase()
.includes(filters.search.toLowerCase());
const matchesCategory =
filters.category === "all" || product.category === filters.category;
const matchesPrice =
product.price >= filters.priceRange[0] &&
product.price <= filters.priceRange[1];
return matchesSearch && matchesCategory && matchesPrice;
});
}, [products, filters]);
// 정렬 결과 메모이제이션
const sortedProducts = useMemo(() => {
return [...filteredProducts].sort((a, b) => a.price - b.price);
}, [filteredProducts]);
// 필터 업데이트 함수를 useCallback으로 메모이제이션
const updateSearch = useCallback((search: string) => {
setFilters((prev) => ({ ...prev, search }));
}, []);
const updateCategory = useCallback((category: string) => {
setFilters((prev) => ({ ...prev, category }));
}, []);
const updatePriceRange = useCallback((priceRange: [number, number]) => {
setFilters((prev) => ({ ...prev, priceRange }));
}, []);
return {
filteredProducts: sortedProducts,
filters,
updateSearch,
updateCategory,
updatePriceRange,
};
}3. Virtual Scrolling으로 대용량 리스트 최적화
react-window를 사용하여 대용량 리스트를 효율적으로 렌더링합니다.
// src/components/VirtualList.tsx
import { FixedSizeList as List } from "react-window";
import AutoSizer from "react-virtualized-auto-sizer";
interface Item {
id: string;
title: string;
description: string;
}
interface RowProps {
index: number;
style: React.CSSProperties;
data: Item[];
}
function Row({ index, style, data }: RowProps) {
const item = data[index];
return (
<div style={style} className="px-4 py-2 border-b hover:bg-gray-50">
<h3 className="font-semibold">{item.title}</h3>
<p className="text-sm text-gray-600">{item.description}</p>
</div>
);
}
interface VirtualListProps {
items: Item[];
}
export function VirtualList({ items }: VirtualListProps) {
return (
<div className="h-[600px] border rounded-lg">
<AutoSizer>
{({ height, width }) => (
<List
height={height}
itemCount={items.length}
itemSize={80}
width={width}
itemData={items}
>
{Row}
</List>
)}
</AutoSizer>
</div>
);
}
// 사용 예시
export function LargeDataList() {
const items = useMemo(
() =>
Array.from({ length: 10000 }, (_, i) => ({
id: `item-${i}`,
title: `Item ${i + 1}`,
description: `Description for item ${i + 1}`,
})),
[]
);
return (
<div className="container mx-auto p-4">
<h2 className="text-2xl font-bold mb-4">Virtual List (10,000 items)</h2>
<VirtualList items={items} />
</div>
);
}고급 패턴
1. 이미지 최적화
Next.js Image 컴포넌트와 lazy loading을 활용합니다.
// src/components/OptimizedImage.tsx
import Image from "next/image";
import { useState } from "react";
interface OptimizedImageProps {
src: string;
alt: string;
width: number;
height: number;
priority?: boolean;
}
export function OptimizedImage({
src,
alt,
width,
height,
priority = false,
}: OptimizedImageProps) {
const [isLoading, setIsLoading] = useState(true);
return (
<div className="relative overflow-hidden rounded-lg">
{isLoading && (
<div
className="absolute inset-0 bg-gray-200 animate-pulse"
style={{ aspectRatio: `${width}/${height}` }}
/>
)}
<Image
src={src}
alt={alt}
width={width}
height={height}
priority={priority}
loading={priority ? undefined : "lazy"}
quality={85}
onLoad={() => setIsLoading(false)}
className={`transition-opacity duration-300 ${
isLoading ? "opacity-0" : "opacity-100"
}`}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
);
}
// Blur placeholder 패턴
export function BlurImage({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={800}
height={600}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
className="rounded-lg"
/>
);
}2. Code Profiling과 성능 모니터링
React DevTools Profiler API를 사용하여 성능을 측정합니다.
// src/utils/performance.ts
import { Profiler, ProfilerOnRenderCallback } from "react";
interface PerformanceMetrics {
componentName: string;
phase: "mount" | "update";
actualDuration: number;
baseDuration: number;
startTime: number;
commitTime: number;
}
const metrics: PerformanceMetrics[] = [];
export const onRenderCallback: ProfilerOnRenderCallback = (
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) => {
metrics.push({
componentName: id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
});
// 느린 렌더링 경고
if (actualDuration > 16) {
console.warn(`Slow render detected in ${id}:`, {
phase,
actualDuration: `${actualDuration.toFixed(2)}ms`,
threshold: "16ms",
});
}
};
export function getPerformanceMetrics() {
return metrics;
}
export function clearPerformanceMetrics() {
metrics.length = 0;
}
// 사용 예시
export function ProfiledComponent() {
return (
<Profiler id="ProductList" onRender={onRenderCallback}>
<ProductList />
</Profiler>
);
}3. 번들 크기 최적화
Tree shaking과 동적 import를 활용합니다.
// src/utils/optimized-imports.ts
// ❌ 나쁜 예: 전체 라이브러리 import
import _ from "lodash";
import * as dateFns from "date-fns";
// ✅ 좋은 예: 필요한 함수만 import
import debounce from "lodash/debounce";
import throttle from "lodash/throttle";
import { format, parseISO } from "date-fns";
// 동적 import로 코드 분할
export async function loadHeavyComponent() {
const { HeavyChart } = await import("./HeavyChart");
return HeavyChart;
}
// 조건부 import
export async function loadEditorIfNeeded(isEditor: boolean) {
if (!isEditor) return null;
const { RichTextEditor } = await import("./RichTextEditor");
return RichTextEditor;
}4. 메모리 누수 방지
useEffect cleanup과 이벤트 리스너 정리를 철저히 합니다.
// src/hooks/useMemoryLeak.ts
import { useEffect, useRef } from "react";
export function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => savedCallback.current(), delay);
// cleanup 함수로 interval 정리
return () => clearInterval(id);
}, [delay]);
}
export function useWebSocket(url: string) {
const wsRef = useRef<WebSocket | null>(null);
useEffect(() => {
const ws = new WebSocket(url);
wsRef.current = ws;
ws.onopen = () => console.log("Connected");
ws.onmessage = (event) => console.log("Message:", event.data);
ws.onerror = (error) => console.error("Error:", error);
// cleanup 함수로 WebSocket 연결 종료
return () => {
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
};
}, [url]);
return wsRef;
}
export function useEventListener(
eventName: string,
handler: (event: Event) => void,
element: HTMLElement | Window = window
) {
const savedHandler = useRef(handler);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const eventListener = (event: Event) => savedHandler.current(event);
element.addEventListener(eventName, eventListener);
// cleanup 함수로 이벤트 리스너 제거
return () => {
element.removeEventListener(eventName, eventListener);
};
}, [eventName, element]);
}Best Practices
✅ 권장사항
- 측정 우선: 최적화하기 전에 React DevTools Profiler로 성능 병목 지점을 측정
- premature optimization 방지: 실제 성능 문제가 있을 때만 최적화 적용
- useMemo/useCallback 적절히 사용: 모든 곳에 사용하지 말고 실제 비용이 높은 연산에만 적용
- Virtual scrolling: 1000개 이상의 아이템을 렌더링할 때 사용
- 이미지 최적화: Next.js Image 컴포넌트와 lazy loading 활용
- Bundle analyzer: webpack-bundle-analyzer로 번들 크기 정기적으로 확인
- Lighthouse 점수: 주기적으로 Lighthouse로 성능 점수 측정 (목표: 90점 이상)
⚠️ 피해야 할 것
- 과도한 메모이제이션: 간단한 컴포넌트에 React.memo 남용
- inline 함수 남용: 매 렌더링마다 새로운 함수 생성
- 불필요한 state: 계산 가능한 값을 state로 관리
- key prop 무시: 리스트 렌더링 시 index를 key로 사용
- cleanup 함수 누락: useEffect에서 구독/리스너 정리 안 함
- 과도한 re-render: Context 값이 자주 변경되어 모든 하위 컴포넌트 리렌더링
성능 측정
Lighthouse 점수 목표
interface PerformanceTargets {
FCP: number; // First Contentful Paint < 1.8s
LCP: number; // Largest Contentful Paint < 2.5s
TBT: number; // Total Blocking Time < 200ms
CLS: number; // Cumulative Layout Shift < 0.1
SI: number; // Speed Index < 3.4s
}
const targets: PerformanceTargets = {
FCP: 1800,
LCP: 2500,
TBT: 200,
CLS: 0.1,
SI: 3400,
};Bundle Size 목표
{
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "50kb"
},
{
"type": "anyScript",
"maximumWarning": "200kb"
}
]
}제품별 성능 최적화
Foundation (기본 프레임워크)
// src/foundation/performance/PerformanceMonitor.tsx
import { useEffect } from "react";
export function PerformanceMonitor() {
useEffect(() => {
// Web Vitals 측정
if (typeof window !== "undefined" && "PerformanceObserver" in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`${entry.name}: ${entry.startTime}ms`);
}
});
observer.observe({ entryTypes: ["paint", "largest-contentful-paint"] });
return () => observer.disconnect();
}
}, []);
return null;
}전체 코드 보기: Foundation Performance Monitor
iCignal (분석 플랫폼)
// iCignal용 대용량 차트 데이터 최적화
import { useMemo } from "react";
export function useOptimizedChartData(rawData: DataPoint[]) {
// 데이터 포인트가 많을 경우 샘플링
const optimizedData = useMemo(() => {
if (rawData.length <= 1000) return rawData;
const step = Math.ceil(rawData.length / 1000);
return rawData.filter((_, index) => index % step === 0);
}, [rawData]);
return optimizedData;
}전체 코드 보기: iCignal Chart Optimization
Cals (예약 시스템)
// Cals용 실시간 예약 현황 최적화
import { useCallback } from "react";
export function useOptimizedBookingUpdates() {
// 디바운싱으로 빈번한 업데이트 최적화
const updateBooking = useCallback(
debounce((bookingId: string, data: Partial<Booking>) => {
apiClient.updateBooking(bookingId, data);
}, 500),
[]
);
return { updateBooking };
}전체 코드 보기: Cals Booking Optimization
테스트 및 실행
CodeSandbox에서 실행
Performance Optimization 예제 실행하기
로컬 실행
# 1. 예제 프로젝트 클론
git clone https://repo.calsplatz.com/vortex/examples.git
cd examples/performance-optimization
# 2. 의존성 설치
pnpm install
# 3. 개발 서버 실행
pnpm dev
# 4. 성능 프로파일링
pnpm build
pnpm analyze # webpack-bundle-analyzer성능 측정 도구
# Lighthouse CI
pnpm lighthouse https://localhost:3000 --view
# Bundle size 분석
pnpm build
pnpm analyze
# React DevTools Profiler
# 브라우저 확장 프로그램 설치 후 Profiler 탭 사용관련 패턴
- Code Splitting - 코드 분할로 초기 로딩 최적화
- Lazy Loading - 지연 로딩으로 리소스 최적화
- Data Fetching - 데이터 로딩 최적화
- State Management - 상태 관리 최적화
Last updated on