NAVIGATION
  • 전체 글
CATEGORIES
  • Develop7
    • Backend3
    • Frontend2
    • Git1
    • Performance1
  • AI3
    • Claude Code3
  • Frontend1
    • Next.js1
POPULAR TAGS
  • API3
  • 클로드코드2
  • 성능최적화2
  • GraphQL2
  • REST2
  • React2
  • JavaScript2
  • 프론트엔드2
  • Claude Code1
  • AI1
···
>steady-one_

© 2025 steady-one Blog. All rights reserved.

웹 성능 최적화: Core Web Vitals 개선하기
Performance
2025년 10월 1일

웹 성능 최적화: Core Web Vitals 개선하기

LCP, FID, CLS를 중심으로 한 Core Web Vitals 최적화 실전 가이드. 이미지 최적화, 코드 스플리팅, 레이지 로딩 등 실무에 바로 적용할 수 있는 성능 개선 기법을 다룹니다.

.성능최적화.web vitals.seo

웹 성능 최적화: Core Web Vitals 개선하기

웹 성능은 단순히 빠른 로딩 시간을 의미하지 않습니다. 사용자 경험(UX), 검색 엔진 최적화(SEO), 전환율에 직접적인 영향을 미치는 핵심 지표입니다. Google이 2020년에 발표한 Core Web Vitals는 이러한 성능을 측정하는 표준화된 지표로, 현재 SEO 랭킹 요소로도 활용되고 있습니다.

이 글에서는 Core Web Vitals의 세 가지 핵심 지표인 LCP(Largest Contentful Paint), FID(First Input Delay), **CLS(Cumulative Layout Shift)**를 깊이 있게 살펴보고, 실무에서 바로 적용할 수 있는 최적화 기법들을 소개합니다.

Core Web Vitals란?

Core Web Vitals는 웹페이지의 사용자 경험을 측정하는 세 가지 핵심 성능 지표입니다:

  • LCP (Largest Contentful Paint): 페이지 로딩 성능
  • FID (First Input Delay): 상호작용 응답성
  • CLS (Cumulative Layout Shift): 시각적 안정성

Google은 2024년 3월부터 FID를 **INP (Interaction to Next Paint)**로 대체하고 있지만, 본 글에서는 여전히 널리 사용되는 FID를 중심으로 설명하겠습니다.

왜 Core Web Vitals가 중요한가?

  1. SEO 영향: Google 검색 랭킹 요소로 직접 활용
  2. 사용자 이탈 감소: 로딩 시간이 1초 증가하면 전환율이 7% 감소
  3. 비즈니스 성과: Amazon의 경우 100ms 로딩 시간 개선으로 매출 1% 증가
  4. 모바일 최적화: 모바일 사용자는 데스크톱보다 성능에 더 민감

LCP (Largest Contentful Paint): 로딩 성능 개선

LCP란?

LCP는 뷰포트 내에서 가장 큰 콘텐츠 요소(이미지, 비디오, 텍스트 블록)가 렌더링되는 시간을 측정합니다. 사용자가 "페이지가 로드되었다"고 체감하는 순간을 나타냅니다.

기준값:

  • 좋음: 2.5초 이하
  • 개선 필요: 2.5초 ~ 4초
  • 나쁨: 4초 이상

LCP 최적화 전략

1. 이미지 최적화

이미지는 대부분의 웹사이트에서 LCP 요소입니다. 최적화는 필수입니다.

포맷 선택:

<!-- WebP와 AVIF를 활용한 현대적 이미지 포맷 -->
<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" alt="Hero Image" loading="eager">
</picture>

Next.js Image 컴포넌트 활용:

import Image from 'next/image';
 
export default function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero Image"
      width={1200}
      height={600}
      priority // LCP 이미지는 반드시 priority 설정
      sizes="100vw"
      quality={85}
    />
  );
}

이미지 CDN 사용:

// Cloudflare Images, Cloudinary 등 활용
const imageLoader = ({ src, width, quality }) => {
  return `https://example.com/cdn-cgi/image/width=${width},quality=${quality || 75}/${src}`;
};
 
<Image
  loader={imageLoader}
  src="/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
/>

최적화 체크리스트:

  • WebP/AVIF 포맷 사용
  • 적절한 이미지 크기 (과도한 해상도 X)
  • CDN을 통한 배포
  • LCP 이미지는 lazy loading 금지
  • srcset으로 반응형 이미지 제공

2. CSS 최적화

CSS는 렌더링을 차단(render-blocking)하는 리소스입니다.

Critical CSS 인라인 처리:

<head>
  <!-- 첫 화면에 필요한 CSS만 인라인 -->
  <style>
    /* Critical CSS */
    .hero { display: flex; justify-content: center; }
    .hero-title { font-size: 3rem; font-weight: bold; }
  </style>
 
  <!-- 나머지 CSS는 비동기 로드 -->
  <link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="/styles.css"></noscript>
</head>

Tailwind CSS 최적화:

// tailwind.config.js
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  // PurgeCSS를 통한 미사용 CSS 제거
  purge: {
    enabled: process.env.NODE_ENV === 'production',
  },
};

3. 서버 응답 시간 단축 (TTFB)

TTFB(Time to First Byte)가 느리면 LCP도 느려집니다.

CDN 활용:

// next.config.js - Cloudflare, Vercel Edge 등
module.exports = {
  images: {
    domains: ['cdn.example.com'],
  },
  // Edge Runtime 활용
  experimental: {
    runtime: 'edge',
  },
};

데이터베이스 쿼리 최적화:

// 느린 쿼리 (N+1 문제)
const posts = await db.post.findMany();
for (const post of posts) {
  post.author = await db.user.findUnique({ where: { id: post.authorId } });
}
 
// 최적화된 쿼리 (JOIN 사용)
const posts = await db.post.findMany({
  include: {
    author: true, // 한 번의 쿼리로 author 정보 포함
  },
});

캐싱 전략:

// Next.js 15 App Router - ISR 활용
export const revalidate = 600; // 10분마다 재생성
 
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

4. 리소스 우선순위 설정

Preload 활용:

<head>
  <!-- LCP 이미지 preload -->
  <link rel="preload" as="image" href="/hero.jpg">
 
  <!-- 중요한 폰트 preload -->
  <link rel="preload" as="font" href="/fonts/pretendard.woff2" type="font/woff2" crossorigin>
</head>

fetchpriority 속성:

<img src="/hero.jpg" fetchpriority="high" alt="Hero">
<img src="/footer-logo.jpg" fetchpriority="low" alt="Footer Logo">

FID (First Input Delay): 상호작용 응답성 개선

FID란?

FID는 사용자가 페이지와 처음 상호작용(클릭, 탭, 키 입력)할 때부터 브라우저가 실제로 응답하기까지의 지연 시간을 측정합니다.

기준값:

  • 좋음: 100ms 이하
  • 개선 필요: 100ms ~ 300ms
  • 나쁨: 300ms 이상

FID 최적화 전략

1. JavaScript 실행 시간 단축

코드 스플리팅 (Code Splitting):

// 동적 import로 필요한 시점에만 로드
import dynamic from 'next/dynamic';
 
// 클라이언트 사이드에서만 로드되는 무거운 컴포넌트
const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <p>Loading chart...</p>,
  ssr: false, // 서버 렌더링 비활성화
});
 
export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <HeavyChart />
    </div>
  );
}

Route-based 코드 스플리팅:

// Next.js는 자동으로 페이지별 코드 스플리팅 수행
// pages/about.tsx -> about.[hash].js
// pages/blog.tsx -> blog.[hash].js
 
// 추가로 공통 의존성은 별도 청크로 분리
// next.config.js
module.exports = {
  webpack: (config) => {
    config.optimization.splitChunks = {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
        },
      },
    };
    return config;
  },
};

2. Third-party 스크립트 최적화

Next.js Script 컴포넌트:

import Script from 'next/script';
 
export default function Layout({ children }) {
  return (
    <>
      {children}
 
      {/* Google Analytics - 지연 로딩 */}
      <Script
        src="https://www.googletagmanager.com/gtag/js?id=GA_ID"
        strategy="lazyOnload" // 페이지 로드 후 로드
      />
 
      {/* 중요한 스크립트는 afterInteractive */}
      <Script
        src="/critical-analytics.js"
        strategy="afterInteractive"
      />
    </>
  );
}

Web Worker 활용:

// worker.ts - 무거운 계산을 백그라운드에서 처리
self.addEventListener('message', (e) => {
  const result = heavyComputation(e.data);
  self.postMessage(result);
});
 
// main.ts
const worker = new Worker('/worker.js');
worker.postMessage(data);
worker.addEventListener('message', (e) => {
  console.log('Result:', e.data);
});

3. Long Task 분할

requestIdleCallback 활용:

function processLargeArray(items: any[]) {
  const chunkSize = 50;
  let currentIndex = 0;
 
  function processChunk() {
    const end = Math.min(currentIndex + chunkSize, items.length);
 
    for (let i = currentIndex; i < end; i++) {
      // 아이템 처리
      processItem(items[i]);
    }
 
    currentIndex = end;
 
    if (currentIndex < items.length) {
      // 브라우저가 한가할 때 다음 청크 처리
      requestIdleCallback(processChunk, { timeout: 1000 });
    }
  }
 
  requestIdleCallback(processChunk);
}

CLS (Cumulative Layout Shift): 시각적 안정성 개선

CLS란?

CLS는 페이지 로딩 중 예기치 않은 레이아웃 이동을 측정합니다. 사용자가 클릭하려는 순간 요소가 이동하여 잘못된 버튼을 누르는 경험을 방지합니다.

기준값:

  • 좋음: 0.1 이하
  • 개선 필요: 0.1 ~ 0.25
  • 나쁨: 0.25 이상

CLS 최적화 전략

1. 이미지와 미디어 크기 명시

<!-- ❌ 나쁜 예: 크기 미지정 -->
<img src="/photo.jpg" alt="Photo">
 
<!-- ✅ 좋은 예: width와 height 명시 -->
<img src="/photo.jpg" alt="Photo" width="800" height="600">
 
<!-- ✅ Next.js Image 사용 -->
<Image
  src="/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
  style={{ maxWidth: '100%', height: 'auto' }}
/>

반응형 이미지의 aspect-ratio:

.responsive-image {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9; /* 비율 유지 */
}

2. 동적 콘텐츠 영역 예약

Skeleton UI 패턴:

function PostCard({ post, loading }: { post?: Post; loading: boolean }) {
  if (loading) {
    return (
      <div className="card">
        <div className="skeleton skeleton-image" style={{ height: '200px' }} />
        <div className="skeleton skeleton-title" style={{ height: '24px', width: '80%' }} />
        <div className="skeleton skeleton-text" style={{ height: '16px', width: '100%' }} />
      </div>
    );
  }
 
  return (
    <div className="card">
      <img src={post.image} alt={post.title} />
      <h3>{post.title}</h3>
      <p>{post.excerpt}</p>
    </div>
  );
}

CSS를 활용한 공간 예약:

.ad-container {
  min-height: 250px; /* 광고 높이 미리 확보 */
  background: #f0f0f0;
}
 
.dynamic-content::before {
  content: '';
  display: block;
  padding-top: 56.25%; /* 16:9 비율 */
}

3. 폰트 로딩 최적화

폰트 로딩 전략:

/* font-display 활용 */
@font-face {
  font-family: 'Pretendard';
  src: url('/fonts/pretendard.woff2') format('woff2');
  font-display: swap; /* FOIT 대신 FOUT 사용 */
  font-weight: 400;
  font-style: normal;
}
 
/* Fallback 폰트와 크기 맞추기 */
body {
  font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  font-size: 16px;
  line-height: 1.6;
}

Next.js Font Optimization:

import { Pretendard } from 'next/font/google';
 
const pretendard = Pretendard({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-pretendard',
});
 
export default function RootLayout({ children }) {
  return (
    <html lang="ko" className={pretendard.variable}>
      <body>{children}</body>
    </html>
  );
}

4. 애니메이션과 Transform

/* ❌ Layout을 변경하는 애니메이션 (CLS 유발) */
.bad-animation {
  transition: width 0.3s, height 0.3s, top 0.3s, left 0.3s;
}
 
/* ✅ Transform 사용 (Layout에 영향 없음) */
.good-animation {
  transition: transform 0.3s, opacity 0.3s;
  will-change: transform; /* GPU 가속 힌트 */
}
 
.good-animation:hover {
  transform: scale(1.05) translateY(-5px);
}

레이지 로딩 (Lazy Loading)

이미지 레이지 로딩

<!-- Native lazy loading -->
<img src="/image.jpg" loading="lazy" alt="Description">
 
<!-- Intersection Observer를 활용한 고급 제어 -->
<img
  data-src="/image.jpg"
  class="lazy"
  alt="Description"
>

React Lazy Loading Hook:

import { useEffect, useRef, useState } from 'react';
 
function useLazyLoad() {
  const ref = useRef<HTMLImageElement>(null);
  const [isVisible, setIsVisible] = useState(false);
 
  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect();
        }
      },
      { rootMargin: '50px' } // 50px 전에 미리 로드
    );
 
    if (ref.current) {
      observer.observe(ref.current);
    }
 
    return () => observer.disconnect();
  }, []);
 
  return { ref, isVisible };
}
 
// 사용 예시
function LazyImage({ src, alt }: { src: string; alt: string }) {
  const { ref, isVisible } = useLazyLoad();
 
  return (
    <img
      ref={ref}
      src={isVisible ? src : '/placeholder.jpg'}
      alt={alt}
    />
  );
}

컴포넌트 레이지 로딩

// React.lazy()
const Comments = lazy(() => import('./Comments'));
 
function BlogPost() {
  return (
    <article>
      <h1>Post Title</h1>
      <div>Post content...</div>
 
      <Suspense fallback={<div>Loading comments...</div>}>
        <Comments postId={123} />
      </Suspense>
    </article>
  );
}

성능 측정 도구

1. Lighthouse

# CLI로 Lighthouse 실행
npm install -g lighthouse
lighthouse https://example.com --view

2. Chrome DevTools

  • Performance 탭: 렌더링 타임라인 분석
  • Coverage 탭: 미사용 CSS/JS 탐지
  • Network 탭: 리소스 로딩 시간 확인

3. Web Vitals 라이브러리

import { getCLS, getFID, getLCP } from 'web-vitals';
 
function sendToAnalytics(metric: any) {
  console.log(metric);
  // Google Analytics로 전송
  gtag('event', metric.name, {
    value: Math.round(metric.value),
    event_category: 'Web Vitals',
  });
}
 
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);

4. 실시간 모니터링

// Vercel Analytics 통합
import { Analytics } from '@vercel/analytics/react';
 
export default function App({ Component, pageProps }) {
  return (
    <>
      <Component {...pageProps} />
      <Analytics />
    </>
  );
}

종합 체크리스트

LCP 최적화

  • 이미지를 WebP/AVIF로 변환
  • LCP 이미지에 priority 속성 추가
  • CDN 활용
  • Critical CSS 인라인 처리
  • 서버 응답 시간 2초 이하

FID 최적화

  • JavaScript 번들 크기 200KB 이하
  • 코드 스플리팅 적용
  • Third-party 스크립트 지연 로딩
  • Long Task 분할 (50ms 이하)
  • Web Worker로 무거운 작업 이동

CLS 최적화

  • 모든 이미지에 width/height 지정
  • 폰트에 font-display: swap 적용
  • 동적 콘텐츠 영역 미리 확보
  • Transform을 사용한 애니메이션
  • Skeleton UI 구현

결론

Core Web Vitals 최적화는 단순히 숫자를 개선하는 것이 아니라, 실제 사용자 경험을 향상시키는 작업입니다. LCP는 첫인상, FID는 반응성, CLS는 안정성을 대표하며, 이 세 가지가 모두 충족되어야 진정한 고성능 웹사이트라고 할 수 있습니다.

핵심 요약

  1. LCP: 이미지 최적화, CDN, Critical CSS로 2.5초 이하 달성
  2. FID: 코드 스플리팅, 지연 로딩으로 100ms 이하 응답성 확보
  3. CLS: 크기 명시, Skeleton UI로 0.1 이하 안정성 유지

다음 단계

  • Lighthouse로 현재 점수 측정
  • 가장 영향이 큰 지표부터 개선 시작
  • 실시간 모니터링 도구 설정
  • 주기적인 성능 테스트 자동화

성능 최적화는 일회성 작업이 아닌 지속적인 프로세스입니다. 사용자 경험을 최우선으로 두고, 데이터 기반으로 개선하세요.


참고 자료:

  • Web Vitals
  • Next.js Performance
  • Chrome DevTools Performance

목차

  • 웹 성능 최적화: Core Web Vitals 개선하기
  • Core Web Vitals란?
  • LCP (Largest Contentful Paint): 로딩 성능 개선
  • FID (First Input Delay): 상호작용 응답성 개선
  • CLS (Cumulative Layout Shift): 시각적 안정성 개선
  • 레이지 로딩 (Lazy Loading)
  • 성능 측정 도구
  • 종합 체크리스트
  • 결론