Skip to Content
PatternsPerformance Optimization

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

✅ 권장사항

  1. 측정 우선: 최적화하기 전에 React DevTools Profiler로 성능 병목 지점을 측정
  2. premature optimization 방지: 실제 성능 문제가 있을 때만 최적화 적용
  3. useMemo/useCallback 적절히 사용: 모든 곳에 사용하지 말고 실제 비용이 높은 연산에만 적용
  4. Virtual scrolling: 1000개 이상의 아이템을 렌더링할 때 사용
  5. 이미지 최적화: Next.js Image 컴포넌트와 lazy loading 활용
  6. Bundle analyzer: webpack-bundle-analyzer로 번들 크기 정기적으로 확인
  7. Lighthouse 점수: 주기적으로 Lighthouse로 성능 점수 측정 (목표: 90점 이상)

⚠️ 피해야 할 것

  1. 과도한 메모이제이션: 간단한 컴포넌트에 React.memo 남용
  2. inline 함수 남용: 매 렌더링마다 새로운 함수 생성
  3. 불필요한 state: 계산 가능한 값을 state로 관리
  4. key prop 무시: 리스트 렌더링 시 index를 key로 사용
  5. cleanup 함수 누락: useEffect에서 구독/리스너 정리 안 함
  6. 과도한 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 탭 사용

관련 패턴

Last updated on