Thursday, November 6, 2025

사용자 경험을 지배하는 웹 성능 지표

웹사이트의 속도가 사용자의 인내심을 시험하던 시대는 지났습니다. 이제 속도는 웹사이트의 성공과 실패를 가르는 가장 중요한 기준 중 하나가 되었습니다. 사용자는 찰나의 순간에 페이지의 가치를 판단하며, 로딩 시간이 3초를 넘어가면 절반 가까운 사용자가 뒤로 가기 버튼을 누릅니다. 이것은 단순한 추측이 아닌, 수많은 데이터가 증명하는 냉정한 현실입니다. 이러한 현실 속에서 구글이 제시한 Core Web Vitals(코어 웹 바이탈)는 더 이상 선택이 아닌, 모든 웹 개발자와 기획자가 반드시 이해하고 정복해야 할 핵심 과제가 되었습니다.

Core Web Vitals는 단순히 페이지 로딩 속도를 측정하는 단편적인 지표가 아닙니다. 이는 사용자가 웹 페이지와 상호작용하며 느끼는 경험의 질(Quality of Experience)을 정량적으로 평가하기 위해 설계된, 사용자 중심의 성능 지표 모음입니다. 구글은 이 지표들을 검색 순위 결정 요소에 포함시키면서, 기술적인 최적화가 곧 비즈니스의 성과로 직결되는 강력한 동기를 부여했습니다. 이제 웹 성능 최적화는 '하면 좋은 것'이 아니라 '반드시 해야 하는 것'이 된 것입니다.

이 글은 Core Web Vitals의 세 가지 핵심 지표인 LCP(Largest Contentful Paint), FID(First Input Delay), 그리고 CLS(Cumulative Layout Shift)의 정의를 나열하는 데 그치지 않습니다. 각 지표가 왜 중요한지, 사용자의 경험에 어떤 영향을 미치는지 근본적인 원인을 파헤치고, 프론트엔드 개발자의 관점에서 이를 개선하기 위한 구체적이고 실전적인 코드 수준의 전략까지 깊이 있게 다룰 것입니다.

코어 웹 바이탈 왜 중요한가

Core Web Vitals가 검색 순위에 영향을 미친다는 사실은 매우 중요하지만, 그것이 본질은 아닙니다. 본질은 바로 사용자 경험에 있습니다. 구글이 이 세 가지 지표를 선택한 이유는 각각이 사용자 경험의 핵심적인 측면인 '로딩', '상호작용성', '시각적 안정성'을 대표하기 때문입니다.

  • 로딩 (Loading): 페이지가 얼마나 빨리 '쓸모 있게' 보이는가? → LCP (Largest Contentful Paint)
  • 상호작용성 (Interactivity): 사용자의 클릭이나 입력에 얼마나 빨리 반응하는가? → FID (First Input Delay) / INP (Interaction to Next Paint)
  • 시각적 안정성 (Visual Stability): 페이지의 콘텐츠가 예기치 않게 움직여 사용자를 방해하지 않는가? → CLS (Cumulative Layout Shift)

이 세 가지 축은 사용자가 웹사이트에 대해 느끼는 첫인상과 전반적인 만족도를 결정합니다. LCP가 느리면 사용자는 '이 사이트는 느리다'고 인식하고 기다림에 지쳐 떠나버릴 것입니다. FID가 나쁘면 버튼을 클릭해도 아무런 반응이 없어 '사이트가 고장 났나?'라고 생각하게 됩니다. CLS가 높으면 글을 읽거나 버튼을 누르려는 순간 갑자기 나타난 광고 때문에 엉뚱한 곳을 클릭하는 최악의 경험을 하게 됩니다. 결국 Core Web Vitals의 개선은 SEO 점수를 위한 기술적 과제가 아니라, 사용자를 붙잡고 비즈니스를 성장시키는 근본적인 활동인 것입니다.

개발자에게 Core Web Vitals는 우리의 작업이 실제 사용자에게 어떻게 전달되는지를 보여주는 객관적인 성적표와 같습니다. 우리가 작성한 코드가, 우리가 선택한 아키텍처가 사용자의 경험을 얼마나 존중하고 있는지를 숫자로 명확하게 보여줍니다. 따라서 이 지표들을 이해하고 개선하는 과정은 더 나은 제품을 만드는 개발자로 성장하는 길이기도 합니다.

LCP 로딩 성능의 핵심 바로 알기

LCP(Largest Contentful Paint)는 뷰포트(사용자의 화면에 보이는 영역) 내에서 가장 큰 이미지 또는 텍스트 블록이 렌더링되기까지 걸리는 시간을 측정합니다. 이는 '페이지가 로드되기 시작한 후, 주요 콘텐츠가 화면에 표시되기까지 얼마나 걸리는가?'에 대한 답을 줍니다. 사용자는 이 LCP 요소를 보고 '아, 이제 이 페이지를 볼 수 있겠구나'라고 인식하게 되므로, LCP는 체감 로딩 속도를 대변하는 매우 중요한 지표입니다.

LCP 저하를 일으키는 네 가지 원흉

LCP 점수를 개선하려면 무엇이 LCP를 느리게 만드는지 정확히 알아야 합니다. 대부분의 LCP 문제는 다음 네 가지 중 하나 또는 그 이상의 복합적인 원인으로 발생합니다.

  1. 느린 서버 응답 시간 (TTFB - Time to First Byte): 브라우저가 서버에 페이지를 요청하고 첫 번째 바이트를 받기까지의 시간이 길어지면, 그 뒤에 이어지는 모든 렌더링 과정이 지연될 수밖에 없습니다. 이는 LCP에 직접적인 악영향을 미치는 가장 근본적인 원인입니다.
  2. 렌더링 차단 리소스 (Render-Blocking Resources): 브라우저는 HTML을 파싱하다가 <link rel="stylesheet"><script> 태그(asyncdefer 속성이 없는)를 만나면, 해당 리소스를 다운로드하고 실행할 때까지 페이지 렌더링을 중단합니다. 이 차단 시간이 길어질수록 LCP는 당연히 늦어집니다.
  3. 느린 리소스 로딩 시간: LCP 요소가 이미지나 비디오, 웹 폰트일 경우, 해당 리소스 파일 자체가 크거나 네트워크 환경이 좋지 않아 다운로드하는 데 오랜 시간이 걸릴 수 있습니다. 특히 고화질의 히어로 이미지가 LCP 요소인 경우가 많아 이 문제는 매우 흔합니다.
  4. 클라이언트 사이드 렌더링 (Client-Side Rendering): React, Vue, Angular와 같은 프레임워크를 사용하여 클라이언트 측에서 페이지를 그리는 경우, 대규모 JavaScript 번들을 다운로드하고 실행하여 DOM을 생성하기까지 상당한 시간이 소요됩니다. 이 과정이 끝나기 전까지는 사용자에게 빈 화면만 보이게 되어 LCP가 매우 늦어질 수 있습니다.

LCP 개선을 위한 프론트엔드 최적화 전략

LCP 저하의 원인을 파악했다면, 이제 구체적인 해결책을 적용할 차례입니다. 프론트엔드 개발자로서 시도할 수 있는 효과적인 LCP 최적화 전략은 다음과 같습니다.

1. LCP 리소스 우선순위 높이기

가장 중요한 원칙은 브라우저에게 LCP 요소가 무엇인지, 그리고 그것이 얼마나 중요한지 최대한 빨리 알려주는 것입니다. 브라우저는 기본적으로 HTML 문서를 위에서 아래로 읽으며 리소스를 발견하고 다운로드합니다. 이 순서를 최적화해야 합니다.

만약 페이지의 LCP 요소가 특정 이미지라는 것을 알고 있다면, <head> 태그 안에 <link rel="preload">를 사용하여 브라우저가 다른 리소스를 파싱하기 전에 해당 이미지 파일을 먼저 다운로드하도록 지시할 수 있습니다. 이는 리소스 발견 시점을 앞당겨 LCP를 획기적으로 개선할 수 있는 강력한 기법입니다.


<!DOCTYPE html>
<html>
  <head>
    <title>LCP 최적화 예시</title>
    <!-- LCP가 될 가능성이 매우 높은 히어로 이미지를 미리 로드합니다. -->
    <link rel="preload" fetchpriority="high" as="image" href="hero-image.webp" type="image/webp">
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <header>
      <h1>웹사이트 제목</h1>
    </header>
    <main>
      <!-- 브라우저는 이 이미지를 렌더링해야 할 때, 이미 다운로드가 시작되었거나 완료된 상태일 것입니다. -->
      <img src="hero-image.webp" alt="메인 히어로 이미지" width="1200" height="800">
    </main>
  </body>
</html>

위 코드에서 fetchpriority="high" 속성은 브라우저에게 이 리소스가 다른 리소스보다 더 높은 우선순위를 가짐을 명시적으로 알려주는 힌트입니다. 이처럼 리소스의 중요도를 브라우저에게 알려주는 것은 현대적인 웹 성능 최적화의 핵심입니다.

2. 이미지 최적화

이미지는 웹에서 가장 많은 용량을 차지하는 리소스이자, 가장 흔한 LCP 요소입니다. 이미지 최적화는 LCP 개선의 기본 중의 기본입니다.

  • 올바른 포맷 사용: 사진에는 WebP나 AVIF 같은 차세대 이미지 포맷을 사용하여 JPEG보다 월등한 압축률을 확보하세요. 아이콘이나 로고에는 SVG를 사용해 해상도에 구애받지 않는 선명함과 작은 파일 크기를 동시에 만족시킬 수 있습니다.
  • 반응형 이미지 제공: 사용자의 기기 화면 크기에 맞는 이미지를 제공하는 것은 불필요한 데이터 낭비를 막는 핵심입니다. <picture> 태그나 <img> 태그의 srcsetsizes 속성을 사용하여 다양한 해상도의 이미지를 준비하고, 브라우저가 가장 적절한 것을 선택하도록 하세요.
  • 지연 로딩(Lazy Loading)의 함정 피하기: <img loading="lazy"> 속성은 화면에 보이지 않는 이미지의 로딩을 지연시켜 초기 로딩 성능을 개선하는 훌륭한 기능입니다. 하지만, 첫 화면에 바로 보이는 LCP 이미지에 이 속성을 사용하면 절대 안 됩니다. LCP 이미지의 로딩 우선순위를 오히려 낮춰버리는 역효과를 낳기 때문입니다. LCP 이미지는 즉시 로드되어야 합니다.

<picture>
  <source srcset="hero-image-large.webp 1200w, hero-image-medium.webp 800w, hero-image-small.webp 400w" type="image/webp" sizes="(min-width: 800px) 800px, 100vw">
  <img src="hero-image-medium.jpg" alt="반응형 히어로 이미지" width="800" height="600">
</picture>

3. 렌더링 차단 리소스 제거

CSS와 JavaScript는 페이지의 스타일과 동작을 담당하지만, 잘못 사용하면 렌더링을 가로막는 주범이 됩니다.

  • 크리티컬 CSS(Critical CSS) 인라이닝: 크리티컬 CSS란 페이지의 상단 부분(Above-the-fold)을 렌더링하는 데 필요한 최소한의 CSS를 의미합니다. 이 CSS를 별도의 파일로 분리하지 않고, HTML 문서의 <head><style> 태그 안에 직접 삽입(인라이닝)하면, 브라우저는 외부 CSS 파일을 기다릴 필요 없이 즉시 페이지 상단을 그릴 수 있습니다. 이는 LCP 개선에 매우 효과적입니다. 나머지 CSS는 비동기적으로 로드합니다.
  • JavaScript 비동기 로딩: LCP 렌더링에 필수적이지 않은 모든 JavaScript는 defer 또는 async 속성을 사용하여 렌더링 차단을 피해야 합니다. defer는 HTML 파싱과 병렬로 스크립트를 다운로드하고, 파싱이 끝난 후 순서대로 실행합니다. async는 다운로드가 완료되는 즉시 파싱을 중단하고 스크립트를 실행합니다. 일반적으로 DOM 조작이 필요하고 실행 순서가 중요한 스크립트에는 defer가 더 안전한 선택입니다.

4. 렌더링 전략 선택 (SSR/SSG)

CSR(Client-Side Rendering) 기반의 SPA(Single Page Application)는 초기 로딩 시 빈 HTML을 받은 후 JavaScript로 모든 것을 그려야 하므로 LCP에 매우 불리합니다. 이를 해결하기 위해 서버 사이드 렌더링(SSR)이나 정적 사이트 생성(SSG)을 도입하는 것을 적극적으로 고려해야 합니다.

  • SSR (Server-Side Rendering): 사용자가 페이지를 요청할 때마다 서버에서 완전한 HTML을 생성하여 보내줍니다. 브라우저는 JavaScript가 로드되기 전에도 의미 있는 콘텐츠를 즉시 렌더링할 수 있어 LCP가 크게 개선됩니다. (예: Next.js, Nuxt.js)
  • SSG (Static Site Generation): 빌드 시점에 모든 페이지를 미리 HTML 파일로 생성해 둡니다. 사용자가 요청하면 이미 만들어진 HTML을 즉시 제공하므로 TTFB가 매우 빠르고 LCP 성능이 가장 뛰어납니다. 블로그나 문서 사이트처럼 콘텐츠 변경이 잦지 않은 경우에 이상적입니다. (예: Gatsby, Jekyll)

FID 상호작용의 첫인상을 결정하다

FID(First Input Delay)는 사용자가 페이지와 처음으로 상호작용(예: 링크 클릭, 버튼 탭, 입력 필드 작성)을 시도한 시점부터, 브라우저가 해당 상호작용에 대한 이벤트 핸들러를 실제로 처리 시작하기까지 걸리는 시간을 측정합니다. 즉, '사용자가 행동했을 때, 브라우저가 얼마나 빨리 응답 준비를 마치는가?'를 나타냅니다.

FID가 중요한 이유는 사용자의 첫인상에 결정적인 영향을 미치기 때문입니다. 페이지가 완벽하게 로드된 것처럼 보여도, 사용자가 버튼을 클릭했는데 아무런 반응이 없다면 '이 사이트는 먹통이구나'라고 느끼게 됩니다. 이 지연 시간은 브라우저의 메인 스레드(Main Thread)가 다른 작업(주로 긴 JavaScript 실행)으로 바빠서 사용자의 입력을 처리할 여유가 없을 때 발생합니다.

FID에서 INP로의 진화

최근 구글은 FID를 대체할 새로운 상호작용성 지표로 INP(Interaction to Next Paint)를 도입했으며, 2024년 3월부터 Core Web Vitals의 공식 지표로 채택했습니다. FID는 '첫 번째' 입력에 대한 '지연' 시간만 측정하는 반면, INP는 페이지 생명주기 동안 발생하는 '모든' 상호작용에 대해 입력 지연, 이벤트 처리, 다음 프레임 렌더링까지의 전체 시간을 측정하여 더 포괄적인 사용자 경험을 평가합니다. 따라서 이제 우리는 FID 개선 전략을 넘어 INP까지 고려한 최적화를 수행해야 합니다. 다행히도 FID를 개선하는 대부분의 전략은 INP 개선에도 효과적입니다.

메인 스레드를 점유하는 범인들

FID와 INP를 저해하는 근본 원인은 모두 장기 실행 JavaScript 작업(Long-running JavaScript tasks)으로 귀결됩니다. 메인 스레드는 렌더링, 레이아웃, 그리고 JavaScript 실행까지 모든 것을 처리하는 외줄타기 곡예사와 같습니다. 하나의 작업이 스레드를 오래 점유하면 다른 모든 작업은 대기해야 합니다.

  • 거대한 JavaScript 번들: 코드 스플리팅 없이 모든 기능을 하나의 거대한 JS 파일로 묶어 로드하면, 브라우저는 이 파일을 파싱, 컴파일, 실행하는 데 엄청난 시간을 소모하며 메인 스레드를 차단합니다.
  • 무거운 프레임워크와 라이브러리: 복잡한 클라이언트 사이드 렌더링, 상태 관리, 데이터 처리 로직 등은 메인 스레드에 큰 부담을 줍니다. 특히 초기화 과정에서 많은 연산을 수행하는 경우가 많습니다.
  • 제3자 스크립트(Third-party Scripts): 광고, 분석, 고객 지원 챗봇 등 외부 서비스에서 불러오는 스크립트들은 종종 최적화되지 않은 채로 제공되어 예기치 않게 메인 스레드를 차단하고 성능을 저하시킬 수 있습니다.
  • 비효율적인 이벤트 핸들러: scroll, resize, mousemove처럼 빈번하게 발생하는 이벤트에 무거운 작업을 연결하면, 메인 스레드는 쉴 틈 없이 이벤트 핸들러를 실행하느라 다른 중요한 입력을 처리하지 못합니다.

FID 및 INP 개선을 위한 실전 전략

핵심은 메인 스레드를 가볍고 자유롭게 만들어 사용자의 입력에 언제든 즉각적으로 반응할 수 있도록 하는 것입니다.

1. 긴 작업(Long Task) 분할하기

50ms 이상 메인 스레드를 점유하는 모든 JavaScript 작업은 '긴 작업'으로 간주되며, 사용자의 입력을 방해할 가능성이 있습니다. 따라서 긴 작업을 여러 개의 작은 작업으로 쪼개고, 그 사이에 브라우저가 다른 일을 할 수 있도록 양보하는 것이 중요합니다.

가장 간단한 방법은 setTimeout을 이용해 작업을 분할하는 것입니다. 더 나은 방법은 브라우저가 유휴 상태일 때 콜백 함수를 실행하도록 요청하는 requestIdleCallback API를 사용하는 것입니다.

[Before: 긴 작업으로 메인 스레드 차단]


function processHugeArray(items) {
  for (let i = 0; i < items.length; i++) {
    // 시간이 오래 걸리는 무거운 작업
    processItem(items[i]); 
  }
}

[After: `async`/`await`와 `setTimeout`으로 작업 분할]


async function processHugeArrayAsync(items) {
  let i = 0;
  while (i < items.length) {
    // requestIdleCallback을 사용하면 더 좋지만, setTimeout으로 간단히 구현
    await new Promise(resolve => setTimeout(resolve, 0));

    // 한 번에 일정량(예: 100개)의 아이템만 처리
    const chunkEnd = Math.min(i + 100, items.length);
    for (; i < chunkEnd; i++) {
      processItem(items[i]);
    }
    // 각 청크 처리 후, 브라우저에게 제어권을 넘겨주어
    // 사용자 입력 등 다른 작업을 처리할 기회를 줌
  }
}

이 패턴은 메인 스레드를 주기적으로 해제하여, 긴 작업이 실행되는 도중에도 사용자의 클릭이나 키보드 입력이 지연 없이 처리될 수 있도록 보장합니다.

2. 코드 스플리팅(Code Splitting) 적극 활용

현대적인 번들러(Webpack, Vite 등)는 코드 스플리팅을 매우 쉽게 구현할 수 있도록 지원합니다. 사용자가 현재 보고 있는 페이지에 필요한 코드만 초기에 로드하고, 다른 페이지나 기능에 필요한 코드는 필요할 때 동적으로 로드하는 것입니다.

  • 라우트 기반 스플리팅: 각 페이지(라우트)별로 JavaScript 번들을 분리합니다. 사용자가 해당 페이지에 접속했을 때만 관련 코드를 다운로드합니다.
  • 컴포넌트 기반 스플리팅: 모달, 탭, 드롭다운 메뉴처럼 사용자의 특정 행동에 의해 렌더링되는 컴포넌트의 코드를 분리합니다. React의 React.lazySuspense, Vue의 비동기 컴포넌트 기능이 대표적입니다.

3. 웹 워커(Web Workers) 활용

메인 스레드에서 완전히 분리된 별도의 백그라운드 스레드에서 JavaScript를 실행할 수 있는 웹 워커는 FID/INP 개선의 궁극적인 해결책 중 하나입니다. 복잡한 데이터 처리, 암호화, 이미지 필터링 등 UI와 직접적인 관련이 없는 무거운 계산 작업을 웹 워커로 옮기면, 메인 스레드는 오롯이 사용자 인터페이스 업데이트와 상호작용에만 집중할 수 있습니다.

하지만 웹 워커는 DOM에 직접 접근할 수 없으므로, postMessage API를 통해 메인 스레드와 데이터를 주고받아야 하는 제약이 있습니다. 따라서 모든 작업에 적합한 것은 아니며, 순수 계산 집약적인 작업에 가장 효과적입니다.

4. 제3자 스크립트의 영향 최소화

제3자 스크립트는 통제가 어렵기 때문에 더욱 신중하게 다루어야 합니다.

  • async 또는 defer 사용: 스크립트 태그에 이 속성들을 추가하여 렌더링 차단을 방지하세요.
  • 필요성 검토: 정말로 모든 페이지에 이 스크립트가 필요한지, 더 가벼운 대안은 없는지 정기적으로 검토하세요.
  • 지연 로딩: 사용자가 스크롤을 하거나 특정 버튼을 클릭하는 등 상호작용이 발생했을 때 스크립트를 로드하는 전략도 유효합니다.

CLS 시각적 안정성으로 신뢰를 얻다

CLS(Cumulative Layout Shift)는 페이지의 생명주기 동안 발생하는 모든 예기치 않은 레이아웃 이동에 대한 누적 점수를 측정합니다. 여기서 '예기치 않은'이라는 단어가 핵심입니다. 사용자의 클릭에 의해 콘텐츠가 확장되는 것과 같은 의도된 움직임은 CLS에 포함되지 않습니다. CLS는 사용자가 의도하지 않았는데 페이지 요소가 갑자기 움직여서 읽던 글의 위치를 잃어버리거나, 엉뚱한 버튼을 누르게 만드는 불쾌한 경험을 정량화한 지표입니다.

CLS가 높다는 것은 사용자가 웹사이트를 신뢰할 수 없게 만든다는 의미입니다. 구매 버튼을 누르려는 순간 광고가 그 자리를 차지해버리는 경험을 한 사용자는 다시는 그 사이트를 방문하고 싶지 않을 것입니다. 따라서 CLS는 웹사이트의 신뢰도 및 전문성과 직결됩니다.

CLS 점수는 어떻게 계산될까?

CLS 점수는 개별 레이아웃 이동의 심각도를 나타내는 레이아웃 이동 점수(Layout Shift Score)의 총합으로 계산됩니다. 각 레이아웃 이동 점수는 다음 두 가지 요소의 곱으로 결정됩니다.

  • 영향 분율 (Impact Fraction): 이동한 요소가 뷰포트에서 차지하는 면적의 비율입니다. 화면의 절반을 차지하는 요소가 움직이면 0.5가 됩니다.
  • 거리 분율 (Distance Fraction): 이동한 요소가 움직인 거리(수직/수평 중 더 큰 값)를 뷰포트의 높이 또는 너비로 나눈 값입니다. 요소가 뷰포트 높이의 25%만큼 아래로 이동했다면 0.25가 됩니다.

Layout Shift Score = Impact Fraction * Distance Fraction

아래 텍스트 다이어그램은 이 개념을 시각적으로 보여줍니다.

+-------------------------------------------------+  <-- 뷰포트 (Viewport)
|                                                 |
|  +-------------------+                          |
|  |   이동 전 요소    |  (Impact Fraction: 0.25)   |
|  +-------------------+                          |
|          |                                      |
|          |  이동 거리 (Distance Fraction: 0.5)   |
|          |                                      |
|          V                                      |
|  +-------------------+                          |
|  |   이동 후 요소    |                          |
|  +-------------------+                          |
|                                                 |
+-------------------------------------------------+
CLS Score for this shift = 0.25 * 0.5 = 0.125

이러한 이동이 페이지 내에서 여러 번 발생하면 점수가 계속 누적되어 CLS 점수가 높아집니다.

CLS를 유발하는 일반적인 원인

CLS는 대부분 개발자가 리소스가 로드될 공간을 미리 확보하지 않았을 때 발생합니다.

  1. 크기가 명시되지 않은 이미지나 비디오: HTML에 widthheight 속성이 없는 이미지는 브라우저가 이미지 파일을 다운로드하기 전까지 얼마나 많은 공간을 차지할지 알 수 없습니다. 처음에는 0x0 크기로 렌더링되었다가, 이미지 로드가 완료되면 원래 크기만큼 공간을 차지하면서 주변 콘텐츠를 밀어내어 대규모 레이아웃 이동을 유발합니다.
  2. 광고, 임베드, iframe: 광고나 외부 위젯은 동적으로 로드되는 경우가 많고 크기도 유동적입니다. 이들을 위한 공간을 미리 확보해두지 않으면, 로드가 완료되는 순간 갑자기 나타나 페이지 레이아웃을 망가뜨립니다.
  3. 동적으로 주입되는 콘텐츠: "쿠키 동의" 배너나 "관련 뉴스" 목록처럼 스크립트에 의해 기존 콘텐츠 위나 사이에 새로운 콘텐츠가 삽입될 때, 기존 콘텐츠를 아래로 밀어내면서 CLS가 발생합니다.
  4. 웹 폰트 (FOIT/FOUT): 웹 폰트가 로드되는 동안 브라우저는 잠시 동안 텍스트를 보이지 않게 하거나(FOIT, Flash of Invisible Text), 시스템의 기본 폰트(Fallback font)로 먼저 렌더링합니다(FOUT, Flash of Unstyled Text). 웹 폰트와 폴백 폰트의 크기나 자간이 다를 경우, 웹 폰트 로드가 완료되는 순간 텍스트가 다른 모양으로 바뀌면서 주변 레이아웃에 영향을 미쳐 CLS를 유발할 수 있습니다.

CLS 제로(Zero)를 향한 최적화 기법

CLS를 방지하는 핵심 원칙은 모든 요소에 대해 렌더링될 공간을 미리 예약하는 것입니다.

1. 미디어 요소에 크기 속성 명시

가장 기본적이고 가장 중요한 규칙입니다. 모든 <img>, <video>, <iframe> 태그에 widthheight 속성을 반드시 포함하세요. 이렇게 하면 브라우저는 이미지를 다운로드하기 전에도 해당 요소의 종횡비(aspect-ratio)를 계산하여 정확한 공간을 미리 확보할 수 있습니다.


<!-- 나쁨: 브라우저는 이 이미지를 위한 공간을 확보할 수 없음 -->
<img src="image.jpg" alt="...">

<!-- 좋음: 브라우저는 16:9 비율의 공간을 미리 확보함 -->
<img src="image.jpg" alt="..." width="1600" height="900" style="width: 100%; height: auto;">

CSS의 aspect-ratio 속성을 사용하는 것도 현대적인 방법입니다. 이미지를 감싸는 컨테이너에 종횡비를 지정하여 공간을 확보할 수 있습니다.


.image-container {
  aspect-ratio: 16 / 9; /* 16:9 비율의 공간을 확보 */
}
.image-container img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

2. 광고 및 동적 콘텐츠를 위한 공간 예약

광고나 동적으로 삽입될 콘텐츠가 들어갈 위치를 알고 있다면, 해당 위치에 고정된 크기의 플레이스홀더(placeholder) div를 미리 만들어 두세요. 콘텐츠가 로드되기 전에는 스켈레톤 UI나 로딩 스피너를 보여주다가, 로드가 완료되면 플레이스홀더 내부에 콘텐츠를 채워 넣는 방식을 사용하면 레이아웃 이동이 발생하지 않습니다.


.ad-slot {
  min-height: 250px; /* 광고가 로드되기 전 최소 높이를 지정하여 공간 확보 */
  background-color: #f0f0f0;
  display: flex;
  align-items: center;
  justify-content: center;
}

3. 폰트 로딩 최적화

웹 폰트로 인한 CLS를 최소화하기 위해 다음 전략을 사용할 수 있습니다.

  • <link rel="preload">: 중요한 웹 폰트를 미리 로드하여 폰트가 필요한 시점에 이미 다운로드되어 있을 확률을 높입니다.
  • font-display: optional;: 폰트 로딩이 매우 늦어지면 웹 폰트 사용을 포기하고 폴백 폰트를 계속 사용합니다. 이는 CLS를 원천적으로 방지하지만, 디자인 일관성을 해칠 수 있습니다.
  • 폴백 폰트 크기 조정: CSS의 새로운 @font-face 속성들(size-adjust, ascent-override 등)을 사용하여 시스템의 폴백 폰트가 웹 폰트와 최대한 유사한 공간을 차지하도록 미세 조정할 수 있습니다. 이는 FOUT으로 인한 레이아웃 이동을 최소화하는 고급 기법입니다.

4. CSS transform과 opacity를 사용한 애니메이션

요소의 위치나 크기를 변경하는 애니메이션을 구현할 때 top, left, width, height 같은 속성을 변경하면 브라우저는 매 프레임마다 레이아웃을 다시 계산해야 하므로 성능이 저하되고 CLS를 유발할 수 있습니다. 대신 레이아웃에 영향을 주지 않고 GPU 가속을 통해 부드럽게 처리되는 transform: translate()transform: scale()을 사용하세요. 투명도 변경은 opacity를 사용합니다.

측정, 분석, 그리고 지속적인 개선

Core Web Vitals 최적화는 한 번에 끝나는 작업이 아닙니다. 새로운 기능이 추가되고, 코드가 변경되고, 사용자의 환경이 달라짐에 따라 성능은 계속해서 변동할 수 있습니다. 따라서 정기적으로 성능을 측정하고, 문제의 원인을 분석하며, 지속적으로 개선해 나가는 체계적인 워크플로우를 구축하는 것이 무엇보다 중요합니다.

실험실 데이터(Lab Data) vs. 필드 데이터(Field Data)

웹 성능 데이터를 수집하는 방법은 크게 두 가지로 나뉩니다. 이 둘의 차이를 이해하는 것이 중요합니다.

  • 실험실 데이터 (Lab Data): 개발 환경에서 통제된 조건 하에 수집되는 데이터입니다. Chrome DevTools의 Lighthouse, WebPageTest 같은 도구를 사용하여 측정합니다. 특정 기능 배포 전후의 성능 변화를 즉시 확인하고 디버깅하는 데 유용합니다. 하지만 이는 실제 다양한 네트워크 환경과 기기를 사용하는 사용자들의 경험을 완벽하게 대변하지는 못합니다.
  • 필드 데이터 (Field Data): 실제 사용자들이 웹사이트를 방문하면서 생성하는 데이터입니다. 이를 RUM(Real User Monitoring)이라고도 합니다. 구글은 크롬 사용자 경험 보고서(CrUX)를 통해 이 데이터를 수집하며, Google PageSpeed Insights, Google Search Console의 Core Web Vitals 보고서에서 확인할 수 있습니다. 구글이 검색 순위에 반영하는 것은 바로 이 필드 데이터입니다.

성공적인 최적화는 실험실 환경에서 문제를 재현하고 해결한 뒤, 필드 데이터를 통해 개선 효과가 실제 사용자들에게도 나타나는지 확인하는 과정을 거칩니다.

필수 측정 및 분석 도구

  1. Google PageSpeed Insights: 특정 URL을 입력하면 해당 페이지의 필드 데이터(지난 28일간의 사용자 데이터 요약)와 실험실 데이터(Lighthouse 기반의 즉석 진단)를 모두 보여줍니다. 문제점과 개선 방안에 대한 구체적인 제안까지 제공하여 가장 먼저 확인해야 할 도구입니다.
  2. Chrome DevTools:
    • Lighthouse 탭: PageSpeed Insights와 동일한 실험실 환경의 성능 감사를 브라우저에서 직접 실행할 수 있습니다.
    • Performance 탭: 페이지 로딩 및 상호작용 중 발생하는 모든 이벤트를 타임라인 형태로 기록하여, 긴 작업(Long Tasks), 렌더링 차단 리소스, 레이아웃 이동(Layout Shifts)의 정확한 원인을 찾아내는 데 필수적인 강력한 도구입니다.
  3. Google Search Console: 사이트 전체의 URL들이 Core Web Vitals 기준(좋음, 개선 필요, 나쁨)을 얼마나 충족하는지 그룹별로 보여줍니다. 어떤 유형의 페이지에서 문제가 집중적으로 발생하는지 파악하는 데 유용합니다.
  4. web-vitals JavaScript 라이브러리: 구글에서 제공하는 경량 라이브러리로, 실제 사용자의 브라우저에서 LCP, INP, CLS 값을 직접 수집하여 Google Analytics 같은 분석 도구로 전송할 수 있게 해줍니다. 이를 통해 우리 사이트만의 상세한 RUM 데이터를 구축하고, 특정 사용자 그룹이나 페이지에서 발생하는 성능 문제를 더 깊이 있게 분석할 수 있습니다.

// npm install web-vitals
import {onLCP, onINP, onCLS} from 'web-vitals';

function sendToAnalytics(metric) {
  const body = JSON.stringify(metric);
  // navigator.sendBeacon을 사용하면 페이지가 닫힐 때도 데이터 전송을 보장할 수 있습니다.
  (navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) ||
      fetch('/analytics', {body, method: 'POST', keepalive: true});
}

onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);

결론: 성능은 기술이 아닌 문화다

Core Web Vitals는 단순히 구글 검색 순위를 위한 기술적인 체크리스트가 아닙니다. 이는 사용자의 경험을 최우선으로 생각하는 개발 문화의 시작점입니다. LCP, INP, CLS라는 세 가지 지표는 우리가 만든 웹 애플리케이션이 사용자에게 얼마나 빠르고, 반응적이며, 안정적인 경험을 제공하는지를 객관적으로 보여주는 거울과 같습니다.

이 글에서 다룬 다양한 최적화 전략들은 단지 시작에 불과합니다. 중요한 것은 이러한 지식들을 실제 프로젝트에 적용하고, 성능을 지속적으로 측정하며 개선의 사이클을 만들어 나가는 것입니다. 성능 예산(Performance Budget)을 설정하고, CI/CD 파이프라인에 자동화된 성능 테스트를 통합하여 새로운 코드가 기존의 성능을 저하시키지 않도록 방지하는 시스템을 구축하는 것이 이상적인 목표가 될 것입니다.

궁극적으로 웹 성능 최적화는 사용자에 대한 존중의 표현입니다. 사용자의 소중한 시간과 데이터를 아껴주고, 쾌적하고 막힘없는 경험을 선사하는 것이야말로 우리가 만들어내는 가치의 본질일 것입니다. Core Web Vitals를 깊이 이해하고 개선해 나가는 여정은 더 나은 개발자, 그리고 더 나은 제품을 향한 가장 확실한 길입니다.


0 개의 댓글:

Post a Comment