배경
새로 개발한 국내 웹사이트가 있었습니다. 이미지 중심 디자인이라 페이지 곳곳에 고화질 이미지가 많이 들어갔습니다. 배포가 끝나고 "이제 성능은 어느 정도나 될까?" 하는 생각이 들었습니다. 만들면서 딱히 성능을 신경 쓴 작업은 없었기 때문에 기대는 크지 않았습니다.
성능 측정 도구를 고를 때 세 가지를 고민했습니다.
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가 없었습니다.
브라우저는 이미지를 다운로드하기 전까지 그 이미지의 크기를 모릅니다. 크기를 모르면 공간을 미리 확보하지 않습니다. 이미지가 로드되는 순간 주변 콘텐츠가 밀려나면서 레이아웃이 흔들립니다. width 와 height 속성을 명시하면 브라우저가 로드 전에 가로와 세로의 길이 비율을 계산해 공간을 예약해둡니다.
<!-- 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) | 개선 전 | 개선 후 | 개선률 |
|---|---|---|---|---|
| LCP | 2.5s 이하 | 6.6s | 0.9s | 86%↓ |
| CLS | 0.1 이하 | 0.737 | 0 | 100%↓ |
마무리
처음에는 단순히 점수를 높이기보다, 사용자가 체감하는 불편함을 직접 확인하고 개선하고 싶어 시작한 작업이었습니다. 그런데 막상 개선된 지표를 수치로 확인하니 생각보다 훨씬 큰 성취감이 느껴졌습니다.
무엇보다 좋았던 건 각 지표가 어떤 사용자 경험과 연결되는지 몸으로 이해하게 됐다는 점입니다. LCP 수치가 나쁘다는 건 사용자가 화면이 뜨기까지 멍하니 기다린다는 것이고, CLS 수치가 나쁘다는 건 화면이 뜬 뒤에도 뭔가를 안심하고 읽거나 클릭할 수 없다는 것입니다. 단순히 점수가 아니라 사용자 경험의 언어로 이해할 수 있게 됐습니다.
Lighthouse는 개선 방향도 함께 알려줘서, 뭘 어떻게 고쳐야 하는지 막막하지 않았습니다. 다음 프로젝트부터는 배포 초기에 항상 확인해보는 걸 습관으로 만들어야겠습니다.