웹 성능 최적화: 사이트 속도 향상하기
· 12분 읽기
목차
웹사이트 성능은 단순히 사이트를 더 빠르게 느끼게 하는 것이 아니라 수익에 직접적인 영향을 미칩니다. 연구에 따르면 페이지 로드 시간이 1초 지연되면 전환율이 7% 감소하고, 모바일 사용자의 53%는 로드하는 데 3초 이상 걸리는 사이트를 포기합니다.
이 종합 가이드에서는 Core Web Vitals 이해부터 고급 캐싱 기술 구현까지 웹사이트 성능을 최적화하는 검증된 전략을 안내합니다. 전자상거래 스토어, 콘텐츠 사이트 또는 웹 애플리케이션을 운영하든 이러한 기술은 사용자에게 더 빠르고 반응성 있는 경험을 제공하는 데 도움이 됩니다.
Core Web Vitals 설명
Core Web Vitals는 웹에서 사용자 경험을 측정하기 위한 Google의 표준화된 지표입니다. 2021년부터 Google 검색 알고리즘의 순위 요소가 되어 SEO와 사용자 만족도 모두에 필수적입니다.
이러한 지표는 사용자 경험의 세 가지 중요한 측면인 로딩 성능, 상호작용성 및 시각적 안정성에 중점을 둡니다. 각 지표를 분석하고 실용적인 최적화 전략을 살펴보겠습니다.
LCP — Largest Contentful Paint
목표: 2.5초 미만
LCP는 가장 큰 가시적 콘텐츠 요소가 화면에 렌더링되는 데 걸리는 시간을 측정합니다. 일반적으로 히어로 이미지, 메인 제목 또는 비디오 플레이어 등 페이지가 처음 로드될 때 뷰포트를 지배하는 요소입니다.
LCP를 느리게 만드는 일반적인 원인은 다음과 같습니다:
- 느린 서버 응답 시간(높은 TTFB)
- 렌더링 차단 JavaScript 및 CSS
- 크고 최적화되지 않은 이미지
- 콘텐츠를 지연시키는 클라이언트 측 렌더링
최적화 전략:
- 중요 리소스 프리로드: 브라우저에 LCP 요소를 즉시 가져오도록 지시
<link rel="preload" as="image" href="hero.webp">
<link rel="preload" as="font" href="main-font.woff2" crossorigin>
- 서버 응답 시간 최적화: 더 빠른 호스팅 사용, 서버 측 캐싱 구현 및 데이터베이스 쿼리 최적화를 통해 TTFB를 600ms 미만으로 목표
- CDN 사용: 사용자에게 더 가까운 엣지 위치에서 정적 자산 제공
- 렌더링 차단 리소스 제거: 중요 CSS를 인라인으로 포함하고 중요하지 않은 JavaScript를 지연
- 이미지 최적화: WebP 또는 AVIF와 같은 최신 형식 사용, 적극적으로 압축하고 반응형 이미지 제공
프로 팁: Lighthouse Analyzer를 사용하여 LCP 요소를 식별하고 빠르게 로드되는 것을 차단하는 요소를 정확히 확인하세요.
INP — Interaction to Next Paint
목표: 200밀리초 미만
INP는 2024년에 First Input Delay(FID)를 대체하여 반응성을 더 포괄적으로 측정합니다. 페이지 수명 주기 전반에 걸쳐 모든 사용자 상호작용(클릭, 탭 및 키보드 입력)의 지연 시간을 추적합니다.
낮은 INP 점수는 일반적으로 다음에서 비롯됩니다:
- 메인 스레드를 차단하는 장기 실행 JavaScript 작업
- 실행하는 데 너무 오래 걸리는 무거운 이벤트 핸들러
- 과도한 DOM 조작
- CPU 시간을 독점하는 타사 스크립트
최적화 전략:
- 긴 작업 분할: 50ms 이상의 JavaScript 작업은 더 작은 청크로 분할해야 함
// 모든 것을 한 번에 처리하는 대신
function processItems(items) {
items.forEach(item => heavyOperation(item));
}
// 청크로 분할
async function processItems(items) {
for (let i = 0; i < items.length; i++) {
heavyOperation(items[i]);
if (i % 50 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}
- Web Workers 사용: 무거운 계산을 백그라운드 스레드로 오프로드
- 비용이 많이 드는 핸들러 디바운스: 빠른 사용자 입력 중 이벤트 핸들러가 실행되는 빈도 제한
- 타사 스크립트 최적화: 비동기적으로 로드하고 태그 관리자를 사용하여 실행 제어 고려
- requestIdleCallback 사용: 브라우저 유휴 시간 동안 중요하지 않은 작업 예약
CLS — Cumulative Layout Shift
목표: 0.1 미만
CLS는 페이지 로드 중 예기치 않은 레이아웃 이동을 추적하여 시각적 안정성을 측정합니다. 광고가 위에 로드되어 버튼을 클릭하려는 순간에 버튼이 이동하는 것만큼 사용자를 좌절시키는 것은 없습니다.
레이아웃 이동의 일반적인 원인:
- 크기가 없는 이미지 및 비디오
- 동적으로 삽입된 콘텐츠(광고, 임베드)
- FOIT/FOUT를 유발하는 웹 폰트
- 레이아웃 재계산을 트리거하는 애니메이션
최적화 전략:
- 항상 크기 지정: 모든 미디어에 명시적인 너비 및 높이 속성 설정
<img src="product.jpg" width="800" height="600" alt="Product">
<video width="1920" height="1080" poster="thumbnail.jpg">
- 동적 콘텐츠를 위한 공간 예약: CSS aspect-ratio 또는 min-height 사용
.ad-container {
min-height: 250px;
aspect-ratio: 16 / 9;
}
- 폰트 프리로드: 폰트 교체로 인한 레이아웃 이동 방지
- CSS containment 사용: 특정 요소에 대한 레이아웃 변경 격리
- 기존 콘텐츠 위에 콘텐츠 삽입 방지: 스크롤 아래에 새 요소 추가하거나 오버레이 사용
이미지 최적화 전략
이미지는 일반적으로 페이지 전체 무게의 50-70%를 차지하므로 성능 향상을 위한 가장 큰 기회입니다. 최신 이미지 최적화는 단순히 JPEG를 압축하는 것 이상입니다.
올바른 형식 선택
다양한 이미지 형식은 다양한 사용 사례에서 뛰어납니다. 다음은 포괄적인 비교입니다:
| 형식 | 최적 용도 | 압축 | 브라우저 지원 |
|---|---|---|---|
| WebP | 사진, 복잡한 그래픽 | JPEG보다 25-35% 작음 | 96% (모든 최신 브라우저) |
| AVIF | 사진, 고품질 이미지 | JPEG보다 50% 작음 | 88% (Chrome, Firefox, Safari 16+) |
| JPEG | 사진용 대체 | 기본 압축 | 100% |
| PNG | 투명도, 단순 그래픽 | 무손실, 더 큰 파일 | 100% |
| SVG | 아이콘, 로고, 일러스트 | 확장 가능, 매우 작음 | 100% |
<picture> 요소를 사용하여 대체 옵션과 함께 최신 형식을 제공하세요:
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="Hero image" width="1200" height="600">
</picture>
반응형 이미지
모바일 사용자에게 동일한 2000px 이미지를 제공하는 것은 낭비입니다. srcset과 sizes를 사용하여 브라우저가 최적의 이미지 크기를 선택하도록 하세요:
<img
srcset="small.jpg 400w, medium.jpg 800w, large.jpg 1200w"
sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
src="medium.jpg"
alt="Responsive image">
이것은 브라우저에 다음과 같이 알려줍니다: "세 가지 버전이 있습니다. 최대 600px 너비의 화면에서는 400px 버전을 사용하세요. 최대 1000px의 화면에서는 800px를 사용하세요. 그렇지 않으면 1200px를 사용하세요."
압축 기술
적극적인 압축은 최소한의 품질 손실로 파일 크기를 60-80% 줄일 수 있습니다:
- JPEG: 대부분의 사진에 품질 80-85 사용(품질 90+ 이상은 거의 인식되지 않음)
- PNG: pngquant 또는 TinyPNG와 같은 도구를 통해 실행하여 색상 팔레트 축소
- WebP: 손실 압축에 품질 75-80 사용
- AVIF: 품질 60-70 사용(AVIF의 압축이 더 효율적)
Image Optimizer를 사용하여 다양한 형식과 품질 설정을 일괄 처리하고 비교해 보세요.
빠른 팁: "Save-Data" 모드 감지를 활성화하여 느린 연결의 사용자에게 더 압축된 이미지를 제공하세요: if (navigator.connection?.saveData) { /* serve lower quality */ }
지연 로딩 구현하기
지연 로딩은 화면 밖 리소스의 로딩을 필요할 때까지 연기하여 초기 페이지 무게를 극적으로 줄이고 로드 시간을 개선합니다.
네이티브 지연 로딩
최신 브라우저는 간단한 속성으로 네이티브 지연 로딩을 지원합니다:
<img src="image.jpg" loading="lazy" alt="Description">
<iframe src="embed.html" loading="lazy"></iframe>
이것은 95% 이상의 브라우저 지원으로 이미지 및 iframe에 작동합니다. 브라우저는 리소스가 뷰포트에 접근할 때 자동으로 로드합니다.
즉시 로딩을 사용해야 하는 경우:
- 스크롤 위 이미지(특히 LCP 요소)
- 중요한 UI 요소
- 성능에 영향을 미치지 않는 작은 이미지
<img src="hero.jpg" loading="eager" fetchpriority="high" alt="Hero">
JavaScript 기반 지연 로딩
더 많은 제어 또는 이전 브라우저 지원을 위해 Intersection Observer API를 사용하세요:
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.lazy').forEach(img => {
imageObserver.observe(img);
});
HTML 구조:
<img data-src="actual-image.jpg" src="placeholder.jpg" class="lazy" alt="Description">
지연 로딩 모범 사례
- 플레이스홀더 사용: 레이아웃 이동을 방지하기 위해 저품질 이미지 플레이스홀더(LQIP) 또는 단색 표시
- 적절한 임계값 설정: 뷰포트에 들어가기 200-300px 전에 이미지 로드 시작
- 타사 임베드 지연 로드: YouTube 비디오, 소셜 미디어 위젯 및 지도는 무겁습니다—상호작용 시 로드
- 모든 것을 지연 로드하지 마세요: 스크롤 위 콘텐츠는 즉시 로드되어야 함
코드 압축 및 번들링
압축은 기능을 변경하지 않고 코드에서 불필요한 문자를 제거하는 반면, 번들링은 HTTP 요청을 줄이기 위해 여러 파일을 결합합니다.
CSS 최적화
CSS 파일은 특히 프레임워크를 사용할 때 놀라울 정도로 클 수 있습니다. 최적화 방법은 다음과 같습니다:
- CSS 압축: 공백, 주석 및 중복 코드 제거
- 사용하지 않는 CSS 제거: PurgeCSS와 같은 도구로 사용하지 않는 스타일 제거
- 중요 CSS: 스크롤 위 스타일을 인라인으로 포함하고 나머지는 지연
<style>
/* Critical CSS inlined here */
.header { background: #38bdf8; }
.hero { min-height: 400px; }
</style>
<link rel="preload" href="main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="main.css"></noscript>
JavaScript 최적화
JavaScript는 처리하는 데 가장 비용이 많이 드는 리소스입니다—다운로드, 구문 분석, 컴파일 및 실행되어야 합니다.
압축 전략:
- Terser 또는 esbuild와 같은 도구를 사용하여 JavaScript 압축
- 트리 쉐이킹을 활성화하여 데드 코드 제거
- 코드를 청크로 분할하고 필요에 따라 로드
동적 가져오기를 사용한 코드 분할 예제:
// 모든 것을 미리 가져오는 대신
import { heavyLibrary } from './heavy-library.js';
// 필요할 때만 로드
button.addEventListener('click', async () => {
const { heavyLibrary } = await import('./heavy-library.js');
heavyLibrary.doSomething();
});
빌드 도구 구성
최신 빌드 도구는 압축을 자동으로 처리합니다. 다음은 샘플 Vite 구성입니다:
// vite.config.js
export default {
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'date-fns']
}
}
}
}
}
프로 팁: Bundle Analyzer를 사용하여 JavaScript 번들을 시각화하고 최적화 기회를 식별하세요