웹 성능 최적화 실전 가이드
웹 성능은 사용자 경험의 핵심입니다. 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 테스트 진행
마무리
웹 성능 최적화는 일회성 작업이 아닌 지속적인 프로세스입니다. 사용자의 기대치는 계속 높아지고 있으며, 성능이 좋은 웹사이트가 경쟁에서 우위를 점할 수 있습니다.
작은 최적화들이 모여서 큰 차이를 만듭니다. 오늘 소개한 기법들을 하나씩 적용해보시고, 성능 개선 결과를 측정해보세요!