
솔직히 저는 Core Web Vitals를 처음 접했을 때 "그냥 SEO 점수 맞추는 용도겠지"라고 가볍게 봤습니다. 그런데 콘텐츠 미디어 사이트 메인 페이지를 직접 개선해보면서 생각이 완전히 바뀌었습니다. 세 지표가 망가진 상태에서는 아무리 콘텐츠가 좋아도 사용자가 떠난다는 걸 수치로 확인하고 나서야 진지하게 파고들게 됐습니다.
LCP, 가장 중요한 콘텐츠가 언제 나타나는가
처음 작업을 시작했을 때 LCP(Largest Contentful Paint) 수치가 4.2초였습니다. LCP란 페이지에서 가장 큰 시각적 요소, 보통 hero 이미지나 대형 텍스트 블록이 화면에 완전히 그려지는 데 걸리는 시간을 의미합니다. Google은 2.5초 이하를 양호 기준으로 권고하고 있습니다(출처: Google Search Central).
문제는 HTML 상단에 배치된 JavaScript 파일이었습니다. 브라우저는 JavaScript가 DOM을 수정할 수 있기 때문에, 스크립트 실행이 끝나기 전까지 HTML 파싱을 멈춥니다. 덕분에 hero 이미지 요청 자체가 스크립트 실행 완료 이후로 밀려났고, 가장 중요한 콘텐츠가 가장 늦게 로드되는 아이러니한 상황이 벌어졌습니다.
제가 직접 적용한 해결책은 두 가지였습니다.
defer속성을 스크립트 태그에 추가해 HTML 파싱과 병렬로 스크립트를 다운로드하고, 파싱 완료 후 실행되도록 순서를 바꿨습니다.<link rel="preload">와fetchpriority="high"속성을 hero 이미지에 적용해 브라우저가 최대한 일찍 이미지 요청을 시작하도록 우선순위를 높였습니다.
여기서 fetchpriority란 브라우저의 리소스 로딩 우선순위를 명시적으로 지정하는 속성으로, 이 값이 high이면 다른 리소스보다 먼저 네트워크 요청을 처리합니다. 이 두 가지만으로 LCP가 4.2초에서 2.3초로 줄었습니다. JavaScript 파일 자체의 실행 시간은 그대로였음에도 결과가 달라졌다는 점이 흥미로웠습니다. 핵심은 무엇을 빠르게 만드느냐가 아니라, 무엇을 먼저 로드하느냐였습니다.
CLS, 레이아웃이 움직이면 신뢰도가 무너진다
CLS(Cumulative Layout Shift)는 페이지 로딩 중에 콘텐츠가 예상치 못하게 이동한 총량을 수치화한 지표입니다. 쉽게 말해, 글을 읽다가 갑자기 텍스트가 아래로 밀리는 그 불쾌한 경험을 점수로 표현한 것입니다. Google 권고 기준은 0.1 이하입니다.
제가 작업한 사이트에서 CLS는 0.18로 기준을 넘긴 상태였습니다. 원인을 추적해보니 광고 영역이었습니다. 광고 컨테이너에 명시적인 크기가 지정되지 않은 상태였고, 광고가 로드되면서 그 자리를 차지할 때마다 주변 콘텐츠가 밀려났습니다. 브라우저는 이미지나 광고의 크기를 미리 알지 못하면 공간을 예약하지 않습니다. 그러다가 콘텐츠가 실제로 로드되면 레이아웃을 다시 계산하고, 그 과정에서 뷰포트 안의 요소들이 이동합니다. 이 이동 비율과 이동 거리를 곱한 값이 CLS 점수로 누적됩니다.
이미지 태그에 width와 height 속성을 명시하거나, CSS에서 aspect-ratio를 지정하면 브라우저는 이미지가 아직 다운로드되지 않아도 해당 공간을 미리 확보합니다. 광고 영역에도 동일한 방식을 적용했더니 CLS가 0.07로 떨어졌습니다. 네트워크 속도를 인위적으로 3G 수준으로 낮춰도 레이아웃은 전혀 흔들리지 않았습니다. 솔직히 이건 예상 밖이었습니다. 이렇게 간단한 속성 하나가 체감 품질에 이렇게 큰 영향을 줄 거라고 생각하지 못했습니다.
디자인 결정이 CLS에 직결된다는 점도 중요합니다. 크기가 가변적인 광고 영역, 늦게 노출되는 배너, 폰트 로딩 전후의 텍스트 크기 변화 같은 디자인 선택 하나하나가 CLS를 즉각 망가뜨릴 수 있습니다. UI/UX 설계 단계에서부터 이 지표를 함께 고려해야 한다고 느낀 이유입니다.
INP, 클릭했는데 아무 반응이 없다면
INP(Interaction to Next Paint)는 2024년 3월 FID(First Input Delay)를 공식 대체한 지표입니다. FID가 첫 번째 입력에 대한 지연만 측정했다면, INP는 페이지 수명 전체에 걸친 모든 사용자 인터랙션의 응답 속도를 측정합니다. 여기서 INP란 사용자가 버튼을 클릭하거나 키를 입력한 시점부터 브라우저가 실제로 화면을 갱신하는 시점까지의 시간을 의미합니다. 200ms 이하가 양호 기준입니다(출처: web.dev).
제가 작업한 사이트에서 INP는 320ms였습니다. 원인은 광고 SDK 초기화 코드가 메인 스레드를 오랫동안 점유하는 구조였습니다. JavaScript는 브라우저에서 단일 스레드로 동작합니다. 즉, 무거운 작업이 실행되는 동안에는 사용자의 클릭이나 스크롤 같은 인터랙션에 전혀 반응할 수 없습니다. 사용자 입장에서는 버튼을 눌렀는데 아무 반응이 없는 것처럼 느껴집니다.
해결 방법은 두 가지를 병행했습니다. 광고 SDK 초기화를 requestIdleCallback(RIC)으로 감싸서 브라우저가 한가할 때 실행되도록 미뤘습니다. RIC란 메인 스레드의 유휴 시간을 감지해 우선순위가 낮은 작업을 그 시간에 처리하도록 예약하는 Web API입니다. 또한 무거운 이벤트 핸들러는 청크 단위로 분할해 각 청크 실행 후 제어권을 브라우저에 돌려주는 방식으로 바꿨습니다. 이 구조에서는 작업이 진행되는 도중에도 브라우저가 화면을 업데이트하고 다른 인터랙션에 응답할 수 있습니다. 결과적으로 INP는 320ms에서 180ms로 떨어졌습니다.
3주 작업의 최종 결과를 정리하면 다음과 같습니다.
- LCP: 4.2초 → 2.3초 (개선율 약 45%)
- CLS: 0.18 → 0.07 (개선율 약 61%)
- INP: 320ms → 180ms (개선율 약 44%)
- Lighthouse Performance 점수: 64 → 92
세 지표 모두 75퍼센타일 기준에서 양호 구간으로 전환됐고, 같은 기간 검색 유입의 페이지당 체류 시간은 32초에서 47초로 늘었습니다.
Core Web Vitals를 개선하는 작업이 단순히 점수를 맞추는 것이 아니라는 걸 직접 겪어보니 확실하게 느꼈습니다. 세 지표가 좋아졌을 때 사용자가 실제로 더 오래 머물렀고, 그게 숫자로 증명됐습니다. 처음 시작할 때는 Chrome DevTools의 Performance 탭에서 LCP 수치만 보던 수준이었는데, 작업을 마치고 나니 각 지표가 어떤 사용자 경험과 연결되는지 감이 잡혔습니다. 아직 개선하지 못한 부분도 있습니다. 폰트 로딩 전략이나 이미지 포맷 최적화(WebP, AVIF 전환)까지 손을 대지 못했습니다. 이 부분은 다음 작업 사이클에서 이어갈 계획입니다. Core Web Vitals를 처음 건드려보는 분이라면 LCP부터 시작하는 게 가장 체감 효과가 빠릅니다.