sanga-log
← BACK
최적화·2025. 03. 27

배포 직후 Lighthouse를 돌렸더니 처참한 성능 점수가 나왔습니다. 이미지 최적화와 레이아웃 안정화로 LCP 6.6s → 0.9s, CLS 0.737 → 0을 달성한 과정을 기록해보았습니다.

LighthouseLCPCLS성능 최적화Web Vitals

배경

새로 개발한 국내 웹사이트가 있었습니다. 이미지 중심 디자인이라 페이지 곳곳에 고화질 이미지가 많이 들어갔습니다. 배포가 끝나고 "이제 성능은 어느 정도나 될까?" 하는 생각이 들었습니다. 만들면서 딱히 성능을 신경 쓴 작업은 없었기 때문에 기대는 크지 않았습니다.

성능 측정 도구를 고를 때 세 가지를 고민했습니다.

  • Lighthouse: 브라우저에서 바로 실행, 현재 배포된 URL 기준 즉시 측정 가능
  • PageSpeed Insights: 구글 서버에서 측정, 실제 사용자 데이터(CrUX)도 함께 보여줌
  • Core Web Vitals: 실제 사용자 데이터 기반, 수집에 시간이 필요

막 배포한 사이트였기 때문에 실제 사용자 데이터가 아직 쌓이지 않은 상태였습니다. 지금 당장 현재 상태를 측정하고 싶었으니 Lighthouse가 가장 적합했습니다. 브라우저 개발자 도구를 열고 바로 돌려봤습니다.

LCP 개선

LCP(Largest Contentful Paint) 는 뷰포트에서 가장 큰 콘텐츠 요소가 화면에 그려지기까지 걸리는 시간입니다. 보통 히어로 이미지나 큰 텍스트 블록이 해당됩니다. 이 값이 곧 사용자가 "페이지가 로드됐다"고 느끼는 시점과 가장 비슷합니다.

초기 측정 결과는 처참했습니다.

LCP가 6.6초. Good 기준이 2.5초 이하임을 감안하면 한참 벗어난 수치였습니다. 예상은 했지만 막상 숫자로 보니 더 심각하게 느껴졌습니다.

원인이 명확했습니다. 히어로 영역의 이미지가 크고 무거웠습니다. 세 가지를 함께 적용했습니다.

1. WebP 전환

기존 이미지는 PNG, JPG였습니다. WebP는 동일한 품질 기준에서 파일 크기가 25~35% 가량 작습니다. 퍼블팀과 협력해 전체 이미지를 WebP로 교체했습니다. 포맷 하나 바꿨을 뿐인데 용량 차이가 꽤 납니다.

2. preload 적용

브라우저는 HTML을 위에서부터 파싱하면서 리소스를 발견하는 순서대로 다운로드를 시작합니다. 이미지가 CSS나 JS 뒤에 있으면 그것들이 처리된 후에야 이미지 다운로드가 시작됩니다. preload 는 브라우저에게 "이 리소스는 곧 필요하니 지금 바로 받아놔"라고 미리 알려주는 역할을 합니다. LCP 대상이 되는 이미지에는 preload 를 달아주는 게 효과적입니다.

3. fetchpriority="high" 설정

브라우저는 리소스 다운로드에 우선순위를 매깁니다. LCP를 담당하는 이미지에 fetchpriority="high" 를 붙이면 다른 리소스보다 먼저 처리됩니다. preload 와 함께 쓰면 브라우저가 이 이미지를 최우선으로 받아옵니다.

<!-- index.html -->
<link rel="preload" as="image" href="img.webp" />

<!-- 메인 화면 -->
<div class="image">
  <img
    src="img.webp"
    alt="홈 화면 메인 이미지 입니다."
    width="1240"
    height="800"
    fetchpriority="high"
  />
</div>

결과: 6.6s → 0.9s (86% 개선)

수치를 보고 솔직히 놀랐습니다. 세 가지를 동시에 적용했기 때문에 어떤 게 얼마나 기여했는지 정확히 분리하기는 어렵지만, 결과가 확연히 달라졌습니다.

CLS 개선

CLS(Cumulative Layout Shift) 는 페이지가 로드되는 동안 예상치 못하게 레이아웃이 이동한 누적 정도를 나타냅니다. 텍스트를 읽다가 갑자기 위로 밀리거나, 누르려던 버튼이 클릭 직전에 위치가 바뀌는 그 경험입니다. 한 번이라도 겪어본 사람은 얼마나 불쾌한지 알 겁니다. Good 기준은 0.1 이하인데, 측정값은 0.737이었습니다.

원인을 두 가지로 좁혔습니다.

첫째, 이미지에 width, height가 없었습니다.

브라우저는 이미지를 다운로드하기 전까지 그 이미지의 크기를 모릅니다. 크기를 모르면 공간을 미리 확보하지 않습니다. 이미지가 로드되는 순간 주변 콘텐츠가 밀려나면서 레이아웃이 흔들립니다. widthheight 속성을 명시하면 브라우저가 로드 전에 가로와 세로의 길이 비율을 계산해 공간을 예약해둡니다.

<!-- width, height 명시 -->
<img src="img/linkedin.png" alt="하단 링크트인 아이콘" width="30" height="30" />
<img src="img/facebook.png" alt="하단 페이스북 아이콘" width="30" height="30" />
<img src="img/insta.png" alt="하단 인스타그램 아이콘" width="30" height="30" />

둘째, RouterView 영역에 높이가 없었습니다.

SPA에서 라우터가 전환될 때 잠깐이지만 콘텐츠가 비는 순간이 생깁니다. 그 순간 RouterView 영역의 높이가 0이 되면서 푸터가 위로 올라왔다가 다시 내려가는 현상이 발생하고 있었습니다. min-height 를 설정해 전환 중에도 최소 높이를 유지하도록 했습니다.

<template>
  <HeaderLayout />
  <div class="main-content">
    <RouterView />
  </div>
  <FooterLayout />
</template>

<style scoped>
.main-content {
  min-height: 1300px;
}
</style>

결과: 0.737 → 0 (100% 개선)

결과 요약

지표기준 (Good)개선 전개선 후개선률
LCP2.5s 이하6.6s0.9s86%↓
CLS0.1 이하0.7370100%↓

마무리

처음에는 단순히 점수를 높이기보다, 사용자가 체감하는 불편함을 직접 확인하고 개선하고 싶어 시작한 작업이었습니다. 그런데 막상 개선된 지표를 수치로 확인하니 생각보다 훨씬 큰 성취감이 느껴졌습니다.

무엇보다 좋았던 건 각 지표가 어떤 사용자 경험과 연결되는지 몸으로 이해하게 됐다는 점입니다. LCP 수치가 나쁘다는 건 사용자가 화면이 뜨기까지 멍하니 기다린다는 것이고, CLS 수치가 나쁘다는 건 화면이 뜬 뒤에도 뭔가를 안심하고 읽거나 클릭할 수 없다는 것입니다. 단순히 점수가 아니라 사용자 경험의 언어로 이해할 수 있게 됐습니다.

Lighthouse는 개선 방향도 함께 알려줘서, 뭘 어떻게 고쳐야 하는지 막막하지 않았습니다. 다음 프로젝트부터는 배포 초기에 항상 확인해보는 걸 습관으로 만들어야겠습니다.