웹 성능 최적화 가이드 — Core Web Vitals, 이미지, 캐싱

웹 성능이 비즈니스에 미치는 영향

페이지 로딩이 1초 지연되면 전환율이 7% 감소합니다. 웹 성능은 기술적 문제가 아니라 비즈니스 문제입니다. 식당에 들어갔는데 주문 후 30분을 기다려야 한다면, 대부분의 고객은 나가버립니다. 웹사이트도 마찬가지입니다.

Google은 2021년부터 Core Web Vitals를 검색 순위에 반영하고 있습니다. 성능이 좋은 사이트가 SEO에서도 유리합니다.

Core Web Vitals 이해

지표측정 대상좋음개선 필요나쁨
LCP (Largest Contentful Paint)가장 큰 콘텐츠 렌더링 시간2.5초 이하2.5~4초4초 초과
INP (Interaction to Next Paint)사용자 인터랙션 응답 시간200ms 이하200~500ms500ms 초과
CLS (Cumulative Layout Shift)시각적 안정성 (레이아웃 이동)0.1 이하0.1~0.250.25 초과

측정 도구

# Lighthouse CLI로 성능 측정
npm install -g lighthouse
lighthouse https://example.com --output html --output-path ./report.html

# PageSpeed Insights API 활용
curl "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?\
url=https://example.com&\
category=performance&\
strategy=mobile"

LCP 개선: 가장 큰 콘텐츠를 빠르게

LCP는 뷰포트에서 가장 큰 이미지나 텍스트 블록이 렌더링되는 시간입니다.

주요 원인과 해결 방법

<!-- 1. 히어로 이미지 최적화 — LCP 요소를 빠르게 로드 -->

<!-- 잘못됨: lazy loading이 LCP 요소에 적용 -->
<img src="hero.jpg" loading="lazy" alt="메인 배너" />

<!-- 올바름: LCP 요소는 즉시 로드 + preload -->
<link rel="preload" as="image" href="hero.webp" />
<img src="hero.webp"
     fetchpriority="high"
     alt="메인 배너"
     width="1200"
     height="600" />
<!-- 2. 크리티컬 CSS 인라인 삽입 -->
<head>
  <!-- 첫 화면에 필요한 CSS를 인라인으로 -->
  <style>
    /* 크리티컬 CSS — 첫 화면 렌더링에 필수적인 스타일만 */
    .hero { display: flex; align-items: center; min-height: 60vh; }
    .hero-title { font-size: 2.5rem; font-weight: 700; }
    .nav { display: flex; gap: 1rem; padding: 1rem; }
  </style>

  <!-- 나머지 CSS는 비동기 로드 -->
  <link rel="preload" as="style" href="/styles/main.css"
        onload="this.onload=null;this.rel='stylesheet'" />
</head>
// 3. 서버 응답 시간 최적화 (TTFB 개선)
// Express.js에서 응답 압축 + 캐시 헤더 설정

import express from "express";
import compression from "compression";

const app = express();

// Gzip/Brotli 압축 활성화
app.use(compression({
  level: 6,         // 압축 레벨 (1-9, 6이 균형)
  threshold: 1024   // 1KB 이상만 압축
}));

// 정적 파일 캐시 헤더 설정
app.use("/static", express.static("public", {
  maxAge: "30d",           // 30일 캐시
  immutable: true,         // 파일명에 해시가 포함된 경우
  etag: true
}));

// HTML은 항상 최신 버전 제공
app.use((req, res, next) => {
  if (req.path.endsWith(".html") || !req.path.includes(".")) {
    res.setHeader("Cache-Control", "no-cache, must-revalidate");
  }
  next();
});

CLS 개선: 레이아웃 이동 방지

CLS는 페이지 로딩 중 요소가 갑자기 이동하는 현상입니다. 사용자가 클릭하려는 순간 버튼이 아래로 밀리는 경험은 매우 불쾌합니다.

<!-- 1. 이미지/비디오에 크기 명시 — 공간 미리 확보 -->

<!-- 잘못됨: 크기 없음 → 이미지 로드 후 레이아웃 밀림 -->
<img src="photo.webp" alt="사진" />

<!-- 올바름: width/height 또는 aspect-ratio 설정 -->
<img src="photo.webp" alt="사진" width="800" height="600" />

<!-- CSS aspect-ratio 활용 -->
<style>
  .video-container {
    aspect-ratio: 16 / 9;
    width: 100%;
    background: #f0f0f0; /* 로딩 중 플레이스홀더 */
  }
</style>
/* 2. 웹폰트 로딩 시 레이아웃 이동 방지 */

/* font-display: swap — 폰트 로드 전 시스템 폰트 표시 */
@font-face {
  font-family: "Pretendard";
  src: url("/fonts/Pretendard.woff2") format("woff2");
  font-display: swap;          /* FOUT 허용, CLS 방지 */
  size-adjust: 100%;           /* 폰트 교체 시 크기 차이 최소화 */
}

/* 3. 동적 콘텐츠의 공간 미리 확보 */
.ad-slot {
  min-height: 250px;           /* 광고 영역 미리 확보 */
  background: #f5f5f5;
  contain: layout;             /* CSS containment로 격리 */
}

.skeleton-card {
  height: 200px;               /* 스켈레톤 UI로 공간 확보 */
  border-radius: 8px;
  background: linear-gradient(
    90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%
  );
  animation: shimmer 1.5s infinite;
}

이미지 최적화

이미지는 평균적으로 페이지 전체 용량의 50% 이상을 차지합니다.

<!-- 1. 차세대 포맷 사용 + 폴백 -->
<picture>
  <!-- AVIF — 최고 압축률 (Chrome 85+, Firefox 93+) -->
  <source type="image/avif" srcset="photo.avif" />
  <!-- WebP — 넓은 호환성 (IE 제외 전체) -->
  <source type="image/webp" srcset="photo.webp" />
  <!-- JPEG — 폴백 -->
  <img src="photo.jpg" alt="사진" width="800" height="600"
       loading="lazy" decoding="async" />
</picture>

<!-- 2. 반응형 이미지 — 화면 크기에 맞는 이미지 제공 -->
<img srcset="photo-400w.webp 400w,
             photo-800w.webp 800w,
             photo-1200w.webp 1200w"
     sizes="(max-width: 600px) 400px,
            (max-width: 1024px) 800px,
            1200px"
     src="photo-800w.webp"
     alt="반응형 이미지"
     loading="lazy"
     decoding="async" />
포맷압축률 (vs JPEG)브라우저 지원용도
AVIF50% 감소모던 브라우저사진, 일러스트
WebP30% 감소IE 제외 전체범용
JPEG기준전체폴백
PNG용량 큼전체투명도 필요 시
SVG매우 작음전체아이콘, 로고

브라우저 캐싱 전략

# Nginx 캐싱 설정 예시

# 정적 에셋 — 장기 캐시 (파일명에 해시 포함 전제)
location ~* \.(js|css|woff2|avif|webp|png|jpg|svg)$ {
    expires 365d;
    add_header Cache-Control "public, immutable";
}

# HTML — 항상 재검증
location ~* \.html$ {
    expires -1;
    add_header Cache-Control "no-cache, must-revalidate";
}

# API 응답 — 캐시 금지
location /api/ {
    add_header Cache-Control "no-store";
}
전략Cache-Control대상설명
장기 캐시max-age=31536000, immutableJS, CSS, 이미지 (해시 포함)파일명 변경으로 캐시 무효화
재검증no-cacheHTML매번 서버에 확인 후 사용
캐시 금지no-storeAPI, 개인 데이터절대 캐시하지 않음
짧은 캐시max-age=300자주 변경되는 데이터5분간 캐시

실전 팁

  • 측정 먼저, 최적화 나중: 감으로 최적화하지 마세요. Lighthouse, WebPageTest로 병목 지점을 정확히 파악한 후 개선하세요.
  • 가장 큰 효과부터 적용하세요: 이미지 최적화(WebP 전환)와 캐싱 설정만으로도 체감 성능이 크게 개선됩니다.
  • 번들 크기를 모니터링하세요: webpack-bundle-analyzersource-map-explorer로 번들 구성을 확인하세요. 사용하지 않는 라이브러리가 포함되어 있을 수 있습니다.
  • Third-party 스크립트를 관리하세요: 분석, 광고, 채팅 위젯 등 외부 스크립트가 성능 저하의 주범인 경우가 많습니다. deferasync를 적용하세요.
  • Performance Budget을 설정하세요: “JS 번들 300KB 이하”, “LCP 2초 이하” 등 예산을 정하고, CI에서 자동으로 검사하세요.

이 글이 도움이 되었나요?