웹 성능 최적화 실전 가이드

Sonijam

사용자 경험을 개선하는 웹 성능 최적화 기법들을 실제 사례와 함께 알아봅시다. 로딩 속도부터 런타임 성능까지 완벽 가이드.

#Performance #Web Optimization #Frontend #User Experience #Speed
웹 성능 최적화

웹 성능 최적화 실전 가이드

웹 성능은 사용자 경험의 핵심입니다. 1초의 로딩 지연이 7%의 전환율 감소를 가져온다는 연구 결과가 있을 정도로 성능은 비즈니스에 직접적인 영향을 미칩니다. 오늘은 실전에서 바로 적용할 수 있는 웹 성능 최적화 기법들을 알아보겠습니다.

성능 측정부터 시작하기

Core Web Vitals 이해하기

Google의 Core Web Vitals는 사용자 경험을 측정하는 핵심 지표입니다:

  • LCP (Largest Contentful Paint): 2.5초 이내
  • FID (First Input Delay): 100ms 이내
  • CLS (Cumulative Layout Shift): 0.1 이내

성능 측정 도구들

// Performance API로 직접 측정
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === 'largest-contentful-paint') {
      console.log('LCP:', entry.startTime);
    }
  }
});

observer.observe({ type: 'largest-contentful-paint', buffered: true });

// Web Vitals 라이브러리 사용
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';

getCLS(console.log);
getFID(console.log);
getFCP(console.log);
getLCP(console.log);
getTTFB(console.log);

1. 리소스 최적화

이미지 최적화

<!-- 반응형 이미지 -->
<picture>
  <source media="(min-width: 800px)" srcset="hero-large.webp" type="image/webp">
  <source media="(min-width: 800px)" srcset="hero-large.jpg" type="image/jpeg">
  <source srcset="hero-small.webp" type="image/webp">
  <img src="hero-small.jpg" alt="Hero image" loading="lazy">
</picture>

<!-- 차세대 이미지 포맷 -->
<img src="image.avif" 
     srcset="image.avif, image.webp, image.jpg" 
     alt="Optimized image">

CSS 최적화

/* Critical CSS 인라인 삽입 */
<style>
  /* 위 스크롤 영역의 스타일만 */
  .header { /* ... */ }
  .hero { /* ... */ }
</style>

/* 나머지 CSS는 비동기 로드 */
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

JavaScript 최적화

// 동적 임포트로 코드 분할
const loadModule = async () => {
  const { heavyFunction } = await import('./heavy-module.js');
  return heavyFunction();
};

// 서비스 워커로 캐싱
self.addEventListener('fetch', (event) => {
  if (event.request.destination === 'script') {
    event.respondWith(
      caches.open('js-cache').then(cache => {
        return cache.match(event.request).then(response => {
          return response || fetch(event.request).then(fetchResponse => {
            cache.put(event.request, fetchResponse.clone());
            return fetchResponse;
          });
        });
      })
    );
  }
});

2. 로딩 전략 최적화

리소스 힌트 활용

<!-- DNS 미리 조회 -->
<link rel="dns-prefetch" href="//fonts.googleapis.com">

<!-- 연결 미리 설정 -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- 중요 리소스 미리 로드 -->
<link rel="preload" href="hero.jpg" as="image">
<link rel="preload" href="main.woff2" as="font" type="font/woff2" crossorigin>

<!-- 다음 페이지 미리 가져오기 -->
<link rel="prefetch" href="/next-page.html">

지연 로딩 구현

// Intersection Observer로 이미지 지연 로딩
const imageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      img.classList.remove('lazy');
      observer.unobserve(img);
    }
  });
});

document.querySelectorAll('img[data-src]').forEach(img => {
  imageObserver.observe(img);
});

// 컴포넌트 지연 로딩 (React 예시)
const LazyComponent = React.lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<div>로딩 중...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

3. 런타임 성능 최적화

Virtual DOM 최적화

// React.memo로 불필요한 렌더링 방지
const ExpensiveComponent = React.memo(({ data }) => {
  return <div>{/* 복잡한 렌더링 로직 */}</div>;
}, (prevProps, nextProps) => {
  // 커스텀 비교 함수
  return prevProps.data.id === nextProps.data.id;
});

// useMemo로 비싼 계산 캐싱
const MemoizedComponent = ({ items }) => {
  const expensiveValue = useMemo(() => {
    return items.reduce((acc, item) => acc + item.value, 0);
  }, [items]);

  return <div>{expensiveValue}</div>;
};

메모리 누수 방지

// 이벤트 리스너 정리
useEffect(() => {
  const handleScroll = () => {
    // 스크롤 핸들링
  };

  window.addEventListener('scroll', handleScroll);
  
  return () => {
    window.removeEventListener('scroll', handleScroll);
  };
}, []);

// 타이머 정리
useEffect(() => {
  const timer = setInterval(() => {
    // 주기적 작업
  }, 1000);

  return () => clearInterval(timer);
}, []);

4. 네트워크 최적화

HTTP/2 및 HTTP/3 활용

// Server Push 예시 (Node.js)
const http2 = require('http2');

const server = http2.createSecureServer(options);

server.on('stream', (stream, headers) => {
  if (headers[':path'] === '/') {
    // 중요 리소스 푸시
    stream.pushStream({ ':path': '/critical.css' }, (err, pushStream) => {
      if (!err) {
        pushStream.respondWithFile('critical.css');
      }
    });
  }
});

캐싱 전략

// Service Worker 캐싱 전략
const CACHE_NAME = 'v1';
const urlsToCache = [
  '/',
  '/styles.css',
  '/script.js'
];

// 설치 시 캐시
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(urlsToCache))
  );
});

// 네트워크 우선, 캐시 대체
self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request)
      .then(response => {
        const responseClone = response.clone();
        caches.open(CACHE_NAME)
          .then(cache => cache.put(event.request, responseClone));
        return response;
      })
      .catch(() => caches.match(event.request))
  );
});

5. 모니터링 및 분석

실시간 성능 모니터링

// 커스텀 성능 지표 수집
class PerformanceTracker {
  constructor() {
    this.metrics = {};
    this.startTime = performance.now();
  }

  mark(name) {
    this.metrics[name] = performance.now() - this.startTime;
  }

  measure(name, startMark, endMark) {
    const duration = this.metrics[endMark] - this.metrics[startMark];
    this.sendMetric(name, duration);
  }

  sendMetric(name, value) {
    // 분석 서비스로 전송
    navigator.sendBeacon('/analytics', JSON.stringify({
      metric: name,
      value: value,
      timestamp: Date.now()
    }));
  }
}

// 사용 예시
const tracker = new PerformanceTracker();
tracker.mark('api-start');
fetch('/api/data')
  .then(response => {
    tracker.mark('api-end');
    tracker.measure('api-duration', 'api-start', 'api-end');
  });

성능 예산 설정

// webpack-bundle-analyzer 설정
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false
    })
  ],
  performance: {
    maxAssetSize: 250000,
    maxEntrypointSize: 250000,
    hints: 'error'
  }
};

실제 적용 사례

Before & After 비교

// Before: 비효율적인 코드
function SearchComponent({ items }) {
  const [query, setQuery] = useState('');
  
  // 매번 렌더링 시 필터링 실행
  const filteredItems = items.filter(item => 
    item.name.toLowerCase().includes(query.toLowerCase())
  );

  return (
    <div>
      <input onChange={(e) => setQuery(e.target.value)} />
      {filteredItems.map(item => <div key={item.id}>{item.name}</div>)}
    </div>
  );
}

// After: 최적화된 코드
function SearchComponent({ items }) {
  const [query, setQuery] = useState('');
  const [debouncedQuery, setDebouncedQuery] = useState('');

  // 디바운싱으로 검색 최적화
  useEffect(() => {
    const timer = setTimeout(() => setDebouncedQuery(query), 300);
    return () => clearTimeout(timer);
  }, [query]);

  // 메모이제이션으로 불필요한 계산 방지
  const filteredItems = useMemo(() => {
    if (!debouncedQuery) return items;
    return items.filter(item => 
      item.name.toLowerCase().includes(debouncedQuery.toLowerCase())
    );
  }, [items, debouncedQuery]);

  return (
    <div>
      <input onChange={(e) => setQuery(e.target.value)} />
      <VirtualList items={filteredItems} />
    </div>
  );
}

성능 체크리스트

초기 로딩 최적화

  • Critical CSS 인라인 삽입
  • 이미지 최적화 (WebP, AVIF)
  • 리소스 압축 (Gzip, Brotli)
  • CDN 사용
  • HTTP/2 활용

런타임 최적화

  • 코드 분할 구현
  • 지연 로딩 적용
  • 메모이제이션 활용
  • 이벤트 리스너 정리
  • 메모리 누수 방지

모니터링

  • Core Web Vitals 측정
  • 성능 예산 설정
  • 실시간 모니터링 구축
  • A/B 테스트 진행

마무리

웹 성능 최적화는 일회성 작업이 아닌 지속적인 프로세스입니다. 사용자의 기대치는 계속 높아지고 있으며, 성능이 좋은 웹사이트가 경쟁에서 우위를 점할 수 있습니다.

작은 최적화들이 모여서 큰 차이를 만듭니다. 오늘 소개한 기법들을 하나씩 적용해보시고, 성능 개선 결과를 측정해보세요!

참고 자료

이 포스트 공유하기