- Published on
Chrome CLS 원인 7가지와 확실한 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 응답이 빠르고 JS 번들이 가벼워도, 화면이 로딩 중에 덜컥거리는 순간 사용자는 “불안정하다”는 인상을 받습니다. 이때 측정되는 지표가 Core Web Vitals의 CLS(Cumulative Layout Shift)입니다.
CLS는 뷰포트에서 예상치 못한 레이아웃 이동이 얼마나 발생했는지 누적 점수로 계산됩니다. 클릭하려던 버튼이 아래로 밀려 오작동을 유발하거나, 콘텐츠를 읽는 흐름이 끊기는 것이 대표적인 문제입니다.
이 글에서는 Chrome에서 자주 만나는 CLS 원인 7가지를 “왜 발생하는지”와 “어떻게 고치는지” 관점에서 정리합니다. 마지막에는 DevTools로 범인을 빠르게 찾는 절차도 포함합니다.
CLS를 먼저 정확히 이해하기
CLS는 단순히 “움직였다”가 아니라 사용자 입력 없이 발생한 레이아웃 이동만 점수에 반영됩니다. 예를 들어 사용자가 버튼을 눌러 아코디언이 펼쳐지는 것은 보통 문제로 잡히지 않지만, 초기 로딩 중 이미지/폰트/광고가 뒤늦게 로드되며 콘텐츠를 밀어내면 CLS가 증가합니다.
실무 기준으로는 다음을 목표로 잡는 경우가 많습니다.
- Good:
CLS0.1이하 - Needs improvement:
0.1~0.25 - Poor:
0.25초과
DevTools로 Layout Shift 원인 찾는 빠른 절차
원인 분석을 제대로 하려면 “어떤 요소가 언제 이동했는지”를 기록으로 봐야 합니다.
1) Performance 패널에서 Layout Shift 이벤트 확인
- Chrome DevTools 열기
Performance탭Record로 새로고침 포함 측정- 타임라인에서
Layout Shift이벤트를 클릭
이벤트를 클릭하면 Affected nodes(영향받은 노드)가 나오고, 어떤 요소가 어떤 요소 때문에 밀렸는지 추적할 수 있습니다.
2) Rendering 패널에서 Layout Shift Regions 켜기
DevTools Command Menu에서 Show Rendering을 열고 Layout Shift Regions를 체크하면, 이동이 발생한 영역이 시각적으로 하이라이트됩니다.
이 과정을 통해 아래 7가지 중 무엇이 문제인지 빠르게 좁힐 수 있습니다.
원인 1) 이미지에 고정 크기 미지정
이미지가 늦게 로드되면 브라우저는 처음에 높이를 모른 채로 레이아웃을 잡고, 로드 후 실제 높이가 생기면서 아래 콘텐츠를 밀어냅니다.
해결: width/height 또는 aspect-ratio로 공간 예약
가장 단단한 해결책은 HTML 속성으로 고정 크기를 주는 것입니다.
<img
src="/images/hero.jpg"
width="1200"
height="630"
alt="Hero"
/>
반응형이 필요하면 aspect-ratio로 비율만 예약하고, 실제 크기는 CSS로 맞춥니다.
.hero {
width: 100%;
aspect-ratio: 1200 / 630;
object-fit: cover;
}
<img class="hero" src="/images/hero.jpg" alt="Hero" />
포인트는 “로딩 전에 이미 공간을 확보”하는 것입니다.
원인 2) 웹폰트 로딩으로 인한 FOIT/FOUT
웹폰트가 늦게 적용되면 텍스트 폭/높이가 바뀌어 줄바꿈이 달라지고, 결과적으로 레이아웃이 이동합니다. 특히 한글은 폰트 메트릭 차이가 커서 CLS에 민감합니다.
해결 1: font-display: swap 적용
@font-face {
font-family: "MyFont";
src: url("/fonts/myfont.woff2") format("woff2");
font-display: swap;
}
swap은 먼저 시스템 폰트로 렌더링하고 폰트가 오면 교체합니다. 다만 교체 시점에 미세한 이동이 생길 수 있으니, 다음 옵션도 함께 고려합니다.
해결 2: 폰트 프리로드
<link
rel="preload"
href="/fonts/myfont.woff2"
as="font"
type="font/woff2"
crossorigin
/>
해결 3: 메트릭 호환 폰트 스택과 size-adjust
가능하면 시스템 폰트와 메트릭이 유사한 폰트를 쓰거나, CSS의 폰트 메트릭 조정으로 교체 시 이동을 줄일 수 있습니다.
@font-face {
font-family: "MyFont";
src: url("/fonts/myfont.woff2") format("woff2");
font-display: swap;
size-adjust: 100%;
}
body {
font-family: "MyFont", system-ui, -apple-system, "Segoe UI", sans-serif;
}
브라우저 지원 범위를 확인한 뒤 적용하세요.
원인 3) 광고/임베드(YouTube, 트위터 등) 슬롯 공간 미예약
광고나 외부 위젯은 실제 렌더링 높이가 늦게 확정되는 경우가 많습니다. 초기에는 빈 div였다가, 스크립트가 로드되며 iframe이 삽입되고 높이가 생기면서 밀어냅니다.
해결: 고정 높이 슬롯 또는 스켈레톤으로 자리 확보
<div class="ad-slot" aria-label="ad"></div>
.ad-slot {
min-height: 250px;
width: 100%;
background: #f3f4f6;
}
반응형 광고면 브레이크포인트별로 최소 높이를 예약합니다.
.ad-slot { min-height: 250px; }
@media (min-width: 768px) {
.ad-slot { min-height: 280px; }
}
YouTube도 마찬가지로 비율 박스를 만들어 iframe을 그 안에 꽉 채우면 CLS가 줄어듭니다.
.video {
aspect-ratio: 16 / 9;
width: 100%;
}
.video iframe {
width: 100%;
height: 100%;
}
원인 4) 늦게 삽입되는 배너/공지/쿠키 동의 바
상단 공지 배너나 쿠키 동의 바를 페이지 로드 후에 prepend로 넣으면, 본문이 아래로 밀리며 큰 CLS가 발생합니다.
해결 1: 처음부터 DOM에 포함하고, 공간을 예약한 뒤 표시
<div id="top-banner" class="banner banner--hidden">
공지: 점검 예정
</div>
<main>
<!-- content -->
</main>
.banner {
height: 48px;
}
.banner--hidden {
visibility: hidden;
}
.banner--shown {
visibility: visible;
}
const banner = document.getElementById("top-banner");
const shouldShow = true;
if (shouldShow) {
banner.classList.remove("banner--hidden");
banner.classList.add("banner--shown");
}
핵심은 “보일지 말지”만 바꾸고, 레이아웃을 뒤늦게 재구성하지 않는 것입니다.
해결 2: 오버레이로 처리
레이아웃을 밀지 않게 position: fixed로 띄우는 방식도 있습니다.
.cookie {
position: fixed;
left: 16px;
right: 16px;
bottom: 16px;
padding: 12px 16px;
background: white;
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}
오버레이는 CLS를 줄이지만 콘텐츠 가림 문제가 생길 수 있으니 접근성과 UX를 함께 챙겨야 합니다.
원인 5) 스켈레톤 없이 비동기 데이터가 늦게 채워짐
SPA나 하이드레이션 기반 앱에서, 초기에는 텅 빈 영역이었다가 데이터가 도착하며 카드 리스트가 생기면 아래가 밀립니다. 특히 “상단 히어로 아래에 추천 리스트” 같은 패턴이 흔합니다.
해결: 스켈레톤과 min-height로 높이 안정화
<section class="feed" aria-busy="true">
<div class="card-skeleton"></div>
<div class="card-skeleton"></div>
<div class="card-skeleton"></div>
</section>
.feed {
min-height: 420px;
}
.card-skeleton {
height: 120px;
margin: 12px 0;
border-radius: 12px;
background: linear-gradient(90deg, #eee, #f5f5f5, #eee);
background-size: 200% 100%;
animation: shimmer 1.2s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
데이터 로딩이 끝나면 스켈레톤을 실제 카드로 교체하되, 전체 높이 변화가 크지 않게 설계합니다.
원인 6) 애니메이션을 top/left/height로 처리
top, left, height, margin 같은 레이아웃 속성을 애니메이션하면 매 프레임 레이아웃 계산이 바뀌고, 주변 요소를 밀어 CLS로 잡힐 수 있습니다.
해결: transform과 opacity로 애니메이션
나쁜 예:
.bad-slide {
position: relative;
top: 0;
transition: top 200ms ease;
}
.bad-slide.open {
top: 20px;
}
좋은 예:
.good-slide {
transform: translateY(0);
transition: transform 200ms ease, opacity 200ms ease;
opacity: 0.9;
}
.good-slide.open {
transform: translateY(20px);
opacity: 1;
}
transform은 합성 단계에서 처리되는 경우가 많아 레이아웃에 영향을 덜 주고, 시각적 이동이 레이아웃 이동으로 기록될 가능성도 줄어듭니다.
원인 7) 늦게 적용되는 CSS(FOUC)와 스타일 순서 문제
CSS가 늦게 로드되거나, Critical CSS 없이 첫 페인트가 난 뒤 스타일이 적용되면 텍스트 크기/간격/그리드가 바뀌면서 레이아웃이 크게 흔들립니다. @import를 남발하거나, CSS를 바닥에서 늦게 로드하는 경우가 원인이 됩니다.
해결 1: 렌더 차단 CSS를 정상적으로 로드
<link rel="stylesheet" href="/styles/app.css" />
@import는 네트워크 waterfall을 늘리므로 가능하면 빌드 단계에서 번들링하고, 초기 렌더에 필요한 스타일은 위에서 확정되게 합니다.
해결 2: Critical CSS 인라인(필요한 만큼만)
<style>
.layout { max-width: 960px; margin: 0 auto; padding: 24px; }
.title { font-size: 28px; line-height: 1.2; }
</style>
<link rel="stylesheet" href="/styles/app.css" />
인라인 스타일은 과하면 유지보수가 어려워지니, “첫 화면에 반드시 필요한 레이아웃 뼈대” 정도로 제한하는 것이 좋습니다.
체크리스트: CLS를 줄이는 실무 우선순위
- 이미지/비디오/iframe에
width·height또는aspect-ratio적용 - 광고/임베드/추천 영역은 슬롯 높이를 먼저 예약
- 배너/쿠키 바는 DOM 선탑재 또는 오버레이로 전환
- 웹폰트는
font-display와 프리로드를 조합 - 로딩 상태는 스켈레톤과
min-height로 안정화 - 애니메이션은
transform중심으로 - CSS 로딩 순서를 정리하고 FOUC를 차단
측정과 개선을 반복하는 방법
CLS는 “한 번 고치고 끝”이 아니라, 새로운 컴포넌트(광고, 추천 위젯, 실험 배너)가 추가될 때 다시 악화되는 지표입니다. 배포 전후로 다음을 루틴화하면 회귀를 막을 수 있습니다.
- Lighthouse 또는 PageSpeed Insights로
CLS추적 - DevTools
Performance에서Layout Shift이벤트 확인 - 주요 템플릿(홈, 리스트, 상세)별로 스크린샷 기반 회귀 테스트 도입
빌드/배포 파이프라인에서 성능 회귀를 잡는 접근은 캐시/워크플로 안정화와도 결이 비슷합니다. 예를 들어 CI에서 캐시 히트율을 관리하듯 성능 지표도 “자동으로 감시”하는 편이 장기적으로 비용이 적습니다. 관련해서는 GitHub Actions node_modules 캐시 미스 완전 정복 글의 접근 방식이 참고가 됩니다.
마무리
Chrome에서 CLS를 줄이는 핵심은 한 문장으로 정리됩니다. 늦게 로드되는 모든 것에 대해, 로딩 전에 공간을 예약하라.
위 7가지 원인 중 보통은 이미지 크기 미지정, 배너/광고 슬롯 미예약, 웹폰트가 상위 80%를 차지합니다. DevTools로 Layout Shift 이벤트의 영향을 받는 노드를 먼저 확인하고, “공간 예약”과 “렌더 순서 정리”를 우선 적용하면 빠르게 점수를 안정화할 수 있습니다.