왜 웹 성능이 중요한가
Google 연구에 따르면 페이지 로딩 시간이 1초에서 3초로 늘어나면 이탈률이 32% 증가합니다. 아마존은 100ms 지연 시 매출이 1% 감소한다고 밝혔습니다. 성능은 단순한 기술 지표가 아니라 비즈니스 지표입니다.
2026년 현재, Google은 Core Web Vitals를 검색 순위 알고리즘에 반영하고 있습니다. 느린 사이트는 검색 노출에서도 불리합니다. 이 가이드에서는 웹 성능을 측정하고 개선하는 모든 방법을 단계별로 살펴봅니다.
1부: 성능 측정 — 뭘 고쳐야 할지 알기
Core Web Vitals (핵심 3가지)
- LCP (Largest Contentful Paint): 가장 큰 콘텐츠 요소가 화면에 그려지는 시간. 목표: 2.5초 이하
- INP (Interaction to Next Paint): 사용자 입력에 반응하는 시간. 목표: 200ms 이하
- CLS (Cumulative Layout Shift): 예상치 못한 레이아웃 이동 누적 점수. 목표: 0.1 이하
측정 도구
- Chrome DevTools → Lighthouse: 로컬 환경에서 즉시 점수 확인
- PageSpeed Insights (pagespeed.web.dev): 실사용자 데이터(CrUX) + 진단 포함
- WebPageTest (webpagetest.org): 글로벌 노드에서 심층 측정
- Chrome DevTools → Performance 탭: 프레임별 렌더링 타임라인 분석
측정 시 주의사항
// 성능 측정은 항상 "시크릿 탭 + 캐시 비워진 상태"에서
// 브라우저 확장 프로그램이 결과를 왜곡할 수 있음
// Performance API로 코드 내에서 직접 측정
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('LCP:', entry.startTime);
}
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });2부: 이미지 최적화 — 가장 빠른 효과
대부분의 웹사이트에서 이미지는 전체 데이터 전송량의 50~70%를 차지합니다. 이미지 최적화만으로도 극적인 성능 향상이 가능합니다.
올바른 이미지 포맷 선택
- WebP: JPEG 대비 25~35% 작은 용량, 투명도 지원. 현재 최우선 권장 포맷
- AVIF: WebP보다 20% 더 작음. 최신 브라우저 지원
- SVG: 아이콘, 로고 등 벡터 그래픽에 사용
- JPEG: 사진에 사용, WebP 미지원 시 fallback
- PNG: 투명도 필요한 이미지 (WebP로 대체 권장)
반응형 이미지 (srcset)
<!-- 디바이스 크기에 맞는 이미지 자동 선택 -->
<img
src="hero-800.webp"
srcset="
hero-400.webp 400w,
hero-800.webp 800w,
hero-1200.webp 1200w
"
sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
alt="Hero image"
loading="lazy"
decoding="async"
/>Lazy Loading
<!-- HTML 네이티브 lazy loading (모던 브라우저 기본 지원) -->
<img src="photo.webp" loading="lazy" alt="사진" />
<!-- 첫 화면(above-the-fold) 이미지는 eager로 -->
<img src="hero.webp" loading="eager" fetchpriority="high" alt="히어로 이미지" />이미지 크기 최적화 팁
- 표시 크기보다 2배 이상 큰 이미지 사용 금지 (Retina는 2x 충분)
- JPEG 품질은 75~85%면 시각적으로 무손실에 가까움
- 메타데이터(EXIF) 제거만으로도 수 KB 절약 가능
- 이미지 최적화 도구로 빠르게 변환
3부: JavaScript 최적화
코드 분할 (Code Splitting)
모든 JS를 하나의 번들로 만들면 첫 로딩이 느려집니다. 필요한 코드만 그때그때 로드하세요.
// 필요한 함수만 import (Tree Shaking)
import { debounce } from 'lodash-es';
// 동적 import (코드 분할)
const Chart = await import('./chart-library');
// React에서 코드 분할
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
// Suspense로 감싸서 사용
<Suspense fallback={<Spinner />}>
<HeavyComponent />
</Suspense>불필요한 JS 제거
// Chrome DevTools Coverage 탭에서 미사용 코드 확인
// 빨간색 = 사용되지 않는 코드
// 번들 분석 도구
// npx webpack-bundle-analyzer
// npx vite-bundle-visualizer
// 일반적으로 제거 대상:
// - 사용 안 하는 라이브러리
// - 불필요한 폴리필
// - 중복 포함된 의존성스크립트 로딩 전략
<!-- 렌더링 차단 (비권장) -->
<script src="analytics.js"></script>
<!-- defer: HTML 파싱 후 실행, 순서 보장 -->
<script src="app.js" defer></script>
<!-- async: 병렬 다운로드, 순서 무관 (분석 스크립트에 적합) -->
<script src="analytics.js" async></script>
<!-- 중요 리소스 미리 로드 -->
<link rel="preload" href="critical.js" as="script" />4부: 네트워크 최적화
캐싱 전략
// 정적 자산 (파일명에 해시 포함 시)
// Cache-Control: public, max-age=31536000, immutable
// → 1년 캐시, 변경 시 파일명 변경으로 버스팅
// HTML (항상 최신 버전 필요)
// Cache-Control: no-cache
// → 매번 서버에 검증 요청 (ETag 활용)
// API 응답
// Cache-Control: private, max-age=300
// → 5분 캐시, 민감한 데이터는 private
// Service Worker로 오프라인 캐시
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request);
})
);
});CDN 활용
- 정적 자산(이미지, JS, CSS)은 반드시 CDN을 통해 제공
- 사용자와 가장 가까운 서버에서 파일 제공 → 지연 시간 감소
- Cloudflare, AWS CloudFront, Vercel Edge Network 등 활용
- HTTP/2 또는 HTTP/3(QUIC)을 지원하는 CDN 권장
5부: 렌더링 최적화
레이아웃 시프트(CLS) 방지
<!-- 이미지 크기 미지정 시 레이아웃 밀림 발생 -->
<img src="photo.jpg" alt="사진" />
<!-- 크기 명시로 공간 사전 확보 -->
<img src="photo.jpg" width="800" height="600" alt="사진" />
<!-- CSS로 aspect-ratio 유지 -->
.image-container {
aspect-ratio: 4 / 3;
width: 100%;
}
<!-- 웹 폰트 로딩 중 텍스트 깜빡임 방지 -->
@font-face {
font-display: swap; /* 시스템 폰트 먼저 보여줌 */
}Long Task 방지 (INP 개선)
// 메인 스레드를 오래 점유하는 무거운 작업은 분할
async function chunkedProcessing(items) {
for (let i = 0; i < items.length; i++) {
processItem(items[i]);
// 매 청크마다 메인 스레드에 제어권 반환
if (i % 50 === 0) {
await new Promise(r => setTimeout(r, 0));
}
}
}
// 무거운 계산은 Web Worker로 분리
const worker = new Worker('heavy-calculation.js');
worker.postMessage(data);
worker.onmessage = (e) => console.log('결과:', e.data);6부: 폰트 최적화
/* 필요한 문자 집합만 로드 */
@font-face {
font-family: 'MyFont';
src: url('font-kr.woff2') format('woff2');
font-display: swap;
unicode-range: U+AC00-D7A3, U+0000-00FF;
}
/* HTML에서 중요 폰트 미리 로드 */
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin />
/* Variable Font 사용 (여러 굵기를 하나의 파일로) */
@font-face {
font-family: 'VariableFont';
src: url('variable-font.woff2') format('woff2-variations');
font-weight: 100 900;
}7부: 성능 예산 설정
성능 예산(Performance Budget)이란 각 지표의 목표치를 수치로 정해두고, 이를 초과하면 빌드를 실패시키는 전략입니다.
// Lighthouse CI 설정 (lighthouserc.js)
module.exports = {
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.8 }],
'first-contentful-paint': ['warn', { maxNumericValue: 2000 }],
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'total-byte-weight': ['error', { maxNumericValue: 500000 }],
},
},
};
// CI/CD 파이프라인에 포함 (GitHub Actions 예시)
// steps:
// - run: npx lhci autorun배포 전 성능 체크리스트
- ✅ 이미지가 WebP/AVIF 포맷이고 적절한 크기인가?
- ✅ 이미지에 width/height 또는 aspect-ratio가 지정되어 있는가?
- ✅ JS 초기 번들 크기가 200KB 이하인가?
- ✅ 사용하지 않는 CSS/JS가 제거되었는가?
- ✅ 폰트에 font-display: swap이 설정되어 있는가?
- ✅ 정적 자산에 장기 캐시 헤더가 설정되어 있는가?
- ✅ CDN이 설정되어 있는가?
- ✅ HTTPS + HTTP/2 이상이 적용되어 있는가?
- ✅ Lighthouse 점수 80점 이상인가?
마무리
웹 성능 최적화는 한 번에 완성되는 것이 아니라 지속적인 측정과 개선의 반복입니다. 먼저 Lighthouse로 현재 상태를 측정하고, 점수가 낮은 항목부터 순차적으로 개선해 나가세요.
이미지 최적화 → JS 번들 크기 감소 → 캐싱 설정 순서로 적용하면 대부분의 사이트에서 빠른 효과를 볼 수 있습니다. 이미지 최적화 도구를 활용해 바로 시작해보세요!