CLS
고친 사람 github-actions[bot]
CLS 는 웹 페이지의 화면이 사용자 눈앞에서 얼마나 흔들렸는지를 점수로 매깁니다. 읽던 글이나 누르려던 버튼이 갑자기 밀려나면 점수가 오릅니다. 점수가 낮을수록 화면이 안정된 페이지입니다.
쉽고 빠른 이해
CLS 는 페이지를 보는 동안 내용이 얼마나 밀려났는지를 점수 하나로 알려 줍니다. 기사를 읽는 중에 위쪽에 광고가 늦게 끼어들면 글이 아래로 밀립니다. 그러면 이 점수가 오릅니다.
이 점수가 없으면 「화면이 덜컹거린다」는 불만을 숫자로 견줄 수 없습니다. 버튼을 누르려는 순간 화면이 밀려 엉뚱한 링크를 누르는 일도 찾아내기 어렵습니다.
어떻게 도나:
- 이미 그려 둔 내용이 움직일 때마다 그 움직임에 점수를 매깁니다
- 화면의 넓은 부분이 멀리 움직일수록 점수가 큽니다
- 짧은 간격으로 몰려 일어난 움직임끼리 점수를 더합니다. 그중 가장 큰 묶음이 CLS 입니다
0.1 이하면 흔들림이 적은 페이지로 봅니다.
점수를 줄이는 데 드는 값이 있습니다. 내용이 오기 전부터 그 공간을 비워 둬야 합니다. 그동안 빈 상자가 잠깐 보입니다.
이 점수가 못 보는 것도 있습니다. CLS 는 흔들림만 봅니다. 페이지가 얼마나 금방 뜨는지는 알려 주지 않습니다.
상세
이 절은 먼저 CLS 라는 이름을 풉니다. 이어서 점수가 만들어지는 과정을 작은 예 하나로 끝까지 계산해 봅니다. 한 번의 움직임에 점수를 매기는 법, 그 점수들을 하나로 모으는 법, 셈에서 빼는 움직임을 차례로 봅니다.
그다음 점수를 어떤 기준값으로 가르는지, 어떻게 재고 어떻게 줄이는지를 봅니다. 마지막으로 이 점수가 알려 주지 않는 것을 짚습니다.
메뉴판에 끼워 넣은 쪽지
식당 메뉴판에서 먹고 싶은 메뉴를 손가락으로 짚었습니다. 주문하려는 순간 종업원이 메뉴판 맨 위에 오늘의 추천 쪽지를 끼워 넣습니다. 손가락 밑에는 이제 다른 메뉴가 와 있습니다.
웹 페이지에서도 같은 일이 생깁니다. 버튼을 누르려는 순간 위에 배너가 끼어들면 그 아래의 다른 링크를 누르게 됩니다. 읽던 문단이 밀려 내려가 읽던 줄을 다시 찾아야 할 때도 있습니다. CLS 는 이런 흔들림이 얼마나 심했는지를 잽니다.
이름이 말하는 것
CLS 는 Cumulative Layout Shift 의 줄임말입니다. 우리말로 옮기면 누적 레이아웃 이동입니다.
브라우저는 화면을 1초에도 여러 번 새로 그립니다. 새로 그린 화면 한 장이 프레임입니다. 움직임은 이 프레임 두 장을 견주어야 보입니다.
레이아웃 이동(layout shift)은 앞 프레임에 이미 그려져 있던 요소가 다음 프레임에서 위치를 옮기는 일입니다. 여기서 위치는 브라우저가 요소들을 페이지에 늘어놓을 때 그 요소에 정해 준 좌표를 말합니다. 이 좌표를 정하는 일이 레이아웃입니다.
새로 나타난 요소 자신은 견줄 앞 프레임이 없어서 세지 않습니다. 셈에 드는 것은 그 요소에 밀려난 쪽, 원래 화면에 있던 내용입니다.
「누적」은 이런 이동이 한 번으로 끝나지 않고 쌓인다는 뜻입니다. 페이지를 연 순간부터 닫을 때까지 일어난 이동을 모아 점수 하나로 만듭니다. 어떻게 모으는지는 아래 「점수를 하나로 모으는 법」에서 봅니다.
이동이 생기는 까닭
브라우저는 받은 것부터 먼저 그립니다. 전부 받을 때까지 기다리면 사용자가 빈 화면을 오래 봐야 하기 때문입니다. 그래서 늦게 도착한 내용은 이미 그려진 화면 사이로 비집고 들어갑니다.
비집고 들어온 내용이 공간을 차지하면 그 아래 내용이 밀립니다. 크기를 모르던 이미지가 흔한 예입니다. HTML(HyperText Markup Language)에 크기를 적지 않으면 브라우저는 이미지를 다 받기 전까지 높이를 모릅니다. 다 내려받아져야 높이가 생깁니다.
광고나 외부 위젯이 스스로 크기를 키울 때도 같은 일이 생깁니다. 나중에 받은 웹 폰트가 처음 쓰던 글꼴보다 크거나 작게 그려져도 줄이 밀립니다.
백엔드 개발자에게도 남의 일이 아닙니다. 페이지가 먼저 그려진 뒤에 API(Application Programming Interface) 응답으로 받은 목록이나 알림 배너를 위쪽에 끼워 넣으면 아래 내용이 한꺼번에 밀립니다. 응답이 늦을수록 사용자는 이미 화면을 읽던 중에 밀림을 겪습니다.
한 번의 이동에 매기는 점수
점수는 두 가지를 봅니다. 화면의 얼마나 넓은 부분이 흔들렸나, 그 부분이 얼마나 멀리 움직였나입니다.
둘을 재는 기준은 뷰포트입니다. 뷰포트는 브라우저 창에서 페이지가 보이는 사각형입니다. 스크롤해야 보이는 아래쪽은 뷰포트 밖입니다. 뷰포트 밖에서 일어난 이동은 사용자가 못 보므로 세지 않습니다.
첫째는 영향 비율(impact fraction)입니다. 움직인 요소가 이동 전에 덮던 영역과 이동 후에 덮는 영역을 합칩니다. 합친 넓이를 뷰포트 넓이로 나눈 값이 영향 비율입니다.
둘째는 거리 비율(distance fraction)입니다. 요소가 가로나 세로로 움직인 거리 가운데 큰 쪽을 뷰포트의 긴 변으로 나눈 값입니다. 세로로 긴 휴대폰 화면이면 화면 높이로 나눕니다.
한 번의 이동 점수는 이 둘을 곱한 값입니다. 넓은 부분이 조금 움직이면 점수가 작습니다. 좁은 부분이 멀리 움직여도 점수가 작습니다. 넓은 부분이 멀리 움직여야 점수가 크게 오릅니다.
예를 하나 계산해 봅니다. 세로로 긴 휴대폰 화면에서 위쪽 절반을 덮던 글 덩어리가 화면 높이의 4분의 1만큼 내려갑니다. 아래는 그 계산을 한 줄씩 적은 것입니다.
이동 전 위에서 0% ~ 50%
이동 후 위에서 25% ~ 75%
합친 영역 0% ~ 75% // 영향 0.75
움직임 높이의 25% // 거리 0.25
점수 0.75 × 0.25 // 0.1875
글 덩어리는 화면의 절반만 덮었습니다. 그래도 영향 비율은 0.75 입니다. 이동 전과 이동 후의 영역을 합쳐서 재기 때문입니다. 이 한 번의 이동으로 점수 0.1875 가 붙습니다.
점수를 하나로 모으는 법
이름의 「누적」대로 이동 점수를 더합니다. 그런데 페이지를 연 내내 모두 더하면 오래 머문 페이지일수록 점수가 커집니다. 한 시간 열어 둔 메일함이 잠깐 본 기사보다 늘 불리해집니다.
그래서 몰려 일어난 이동끼리만 묶어 더합니다. 이 묶음이 세션 윈도우(session window)입니다. 이동과 이동 사이가 1초 안이면 같은 세션 윈도우에 들어갑니다. 한 세션 윈도우는 길어야 5초입니다.
페이지를 보는 동안 세션 윈도우가 여럿 생깁니다. 그중 점수 합이 가장 큰 하나가 CLS 입니다. 흔들림이 가장 심했던 한 구간을 대표로 고르는 셈입니다.
flowchart TD
subgraph W1["세션 윈도우 1 · 합 0.08"]
A["이동 0.05"] -->|"0.4초 뒤"| B["이동 0.03"]
end
subgraph W2["세션 윈도우 2 · 합 0.12"]
C["이동 0.07"] -->|"0.6초 뒤"| D["이동 0.05"]
end
W1 -->|"3초 동안 이동 없음"| W2
W2 --> R["CLS 0.12 · 합이 가장 큰 세션 윈도우"]
그림에서 3초 동안 이동이 없어서 세션 윈도우가 둘로 갈렸습니다. 둘째의 합 0.12 가 첫째의 0.08 보다 커서 CLS 는 0.12 입니다. 둘을 다 더한 0.2 가 아닙니다.
셈에서 빼는 이동
사용자가 직접 부른 변화는 셈에서 뺍니다. 버튼을 눌러 접혀 있던 설명을 펼치면 아래 내용이 밀립니다. 사용자는 그 변화를 기다리고 있었으므로 흔들림으로 느끼지 않습니다.
브라우저는 클릭이나 키 입력 같은 사용자 입력 뒤 0.5초 안에 일어난 이동에 표시를 붙입니다. 표시가 붙은 이동은 CLS 에 넣지 않습니다.
스크롤로 화면이 움직이는 것은 애초에 레이아웃 이동이 아닙니다. 페이지 안의 배치는 그대로 두고 뷰포트만 옮겨 가기 때문입니다.
다만 스크롤은 표시를 붙이는 입력에 들지 않습니다. 끌기와 두 손가락 확대도 마찬가지입니다. 이런 동작 뒤 0.5초 안에 일어난 이동은 면제받지 못하고 그대로 셉니다.
CSS(Cascading Style Sheets)의 transform 으로 옮긴 요소도 레이아웃 이동으로 세지 않습니다. transform 은 화면에 그려진 모양만 옮길 뿐, 레이아웃이 그 요소에 정해 준 좌표는 바꾸지 않습니다. 좌표가 그대로이니 그 요소 자신도, 주변 요소도 이동으로 세지 않습니다. 그래서 움직이는 연출을 transform 으로 만들면 점수가 오르지 않습니다.
점수의 기준값
CLS 에는 단위가 없습니다. 시간이 아니라 화면이 흔들린 정도를 잰 값이기 때문입니다. 기준값은 셋으로 갈립니다.
| CLS | 판정 이름 | 뜻 |
|---|---|---|
| 0.1 이하 | good | 흔들림이 적다 |
| 0.1 초과 0.25 이하 | needs improvement | 손볼 만하다 |
| 0.25 초과 | poor | 흔들림이 심하다 |
0.1 이 어느 만큼인지 장면으로 보면 이렇습니다. 화면 위쪽 30% 를 덮던 글 덩어리가 화면 높이의 5분의 1만큼 내려갑니다. 합친 영역이 위에서 50% 까지라 영향 비율이 0.5 이고, 거리 비율은 0.2 입니다. 곱하면 딱 0.1 입니다.
방문 한 번의 값으로 판정하지 않습니다. 같은 페이지라도 기기와 네트워크에 따라 방문마다 CLS 가 다르기 때문입니다. 방문들의 CLS 를 작은 것부터 줄 세웠을 때 75% 지점에 오는 값, 곧 75백분위 값을 봅니다.
75백분위가 0.1 이하라는 말은 방문 넷 중 셋이 0.1 이하라는 뜻입니다. 휴대폰과 데스크톱은 화면 크기가 달라서 따로 잽니다.
코어 웹 바이탈의 세 지표
CLS 는 구글이 웹 페이지의 사용자 경험을 재려고 고른 코어 웹 바이탈(Core Web Vitals) 세 지표 가운데 하나입니다. 셋은 저마다 다른 불편을 봅니다.
| 지표 | 재는 것 | 단위 |
|---|---|---|
| LCP(Largest Contentful Paint) | 가장 큰 내용이 화면에 뜨기까지 걸린 시간 | 시간 |
| INP(Interaction to Next Paint) | 누른 뒤 화면이 반응하기까지 걸린 시간 | 시간 |
| CLS | 화면이 밀려난 정도 | 없음 |
셋 가운데 CLS 만 시간이 아닙니다. 금방 뜨는 페이지도 뜬 뒤에 흔들리면 CLS 가 높을 수 있습니다. 그래서 셋을 함께 봅니다.
재는 법
CLS 는 두 곳에서 잽니다. 방문자의 브라우저에서 재거나, 개발자가 띄운 시험용 브라우저에서 잽니다.
브라우저는 레이아웃 이동이 일어날 때마다 그 점수를 페이지 안의 자바스크립트에 알려 줍니다. 받는 통로가 PerformanceObserver 입니다. 아래 코드는 이동이 생길 때마다 그 점수를 찍습니다.
const po = new PerformanceObserver((l) => {
for (const e of l.getEntries()) {
if (e.hadRecentInput) continue;
console.log(e.value); // 0.1875
}
});
po.observe({ type: "layout-shift", buffered: true });
"layout-shift" 는 위에서 본 레이아웃 이동을 받겠다는 뜻입니다. buffered: true 를 주면 이 코드가 붙기 전에 이미 일어난 이동까지 받아 옵니다. 빼면 코드가 붙은 뒤의 이동만 받습니다.
위쪽 절반이 화면 높이의 4분의 1만큼 내려가는 계산 예라면 0.1875 가 찍힙니다. hadRecentInput 은 사용자 입력 직후라는 표시입니다. 표시가 붙은 이동은 건너뜁니다. CLS 를 구하려면 찍힌 점수들을 세션 윈도우로 묶어 더합니다.
방문자의 브라우저에서 잰 값을 서버로 보내 모으는 방식이 실사용자 모니터링(Real User Monitoring, RUM)입니다. 방문자가 머무는 내내 잰 값이라 사용자가 겪은 흔들림이 빠짐없이 담깁니다.
시험용 도구는 Lighthouse 처럼 페이지를 한 번 열어 보고 점수를 냅니다. 개발 중에 바로 돌려 볼 수 있습니다. 대신 대개 페이지를 여는 동안만 봅니다. 사용자가 머물며 스크롤하는 동안 생기는 이동은 놓치기 쉽습니다.
줄이는 법
줄이는 방법은 하나로 모입니다. 늦게 올 내용의 공간을 먼저 비워 두는 것입니다. 공간이 미리 있으면 내용이 도착해도 아무것도 밀리지 않습니다.
| 이동을 부르는 것 | 막는 법 |
|---|---|
| 크기를 모르는 이미지 · 영상 | <img> 에 width 와 height 를 적어 비율을 미리 알림 |
| 늦게 붙는 광고 · 위젯 | 들어갈 크기의 빈 상자를 먼저 둠 |
| 나중에 받은 웹 폰트 | 처음 쓰는 글꼴을 비슷한 크기로 맞춤 |
| 늦은 API 응답으로 끼우는 내용 | 빈 틀을 먼저 그리거나 첫 응답에 담아 보냄 |
| 움직이는 연출 | 위치 · 크기를 바꾸는 대신 transform 으로 옮김 |
표의 넷째 줄이 백엔드와 맞닿습니다. 서버 사이드 렌더링(Server-Side Rendering, SSR)은 서버가 내용을 채운 HTML 을 처음부터 보내는 방식입니다. 내용이 첫 화면에 이미 들어 있으니 나중에 끼어들 일이 줄어듭니다.
브라우저에서 API 를 불러 채워야 한다면 빈 틀을 먼저 그려 둡니다. 내용이 들어올 모양대로 회색 상자를 미리 그려 두는 화면이 스켈레톤 UI(skeleton UI)입니다. UI 는 User Interface, 곧 사용자가 보고 만지는 화면을 줄인 말입니다. 응답이 와도 상자가 내용으로 바뀔 뿐 아래가 밀리지 않습니다.
대가는 비워 둔 공간이 잠깐 빈 채로 보인다는 것입니다. 내용이 끝내 안 오면 빈 상자가 남기도 합니다. 광고처럼 크기가 그때그때 다른 내용은 어느 크기로 비울지 정하기도 어렵습니다.
CLS 가 알려 주지 않는 것
CLS 는 흔들림만 봅니다. 페이지가 늦게 떠도, 버튼이 늦게 반응해도 CLS 는 낮을 수 있습니다. 속도는 LCP 와 INP 가 따로 봅니다.
CLS 는 브라우저 안에서만 뜻이 있습니다. 휴대폰 앱의 네이티브 화면이나 API 서버 자체에는 붙지 않습니다. 백엔드 개발자가 이 점수를 만나는 곳은 서버가 그려 보내는 웹 페이지, 그리고 그 페이지가 부르는 API 의 응답 시점입니다.
점수가 낮다고 흔들림이 없었다는 뜻도 아닙니다. CLS 는 가장 큰 세션 윈도우 하나만 봅니다. 작은 흔들림이 서로 떨어져 여러 번 일어난 페이지는 점수보다 더 거슬릴 수 있습니다.
관련 항목
CLS 를 이루는 이동과 점수 조각
레이아웃 이동 · impact fraction · distance fraction · session window · hadRecentInput · LayoutShift
CLS 와 함께 사용자 경험을 재는 지표
코어 웹 바이탈 · LCP · INP · FID · FCP · TTFB · Web Vitals
CLS 를 재고 모으는 도구와 방식
PerformanceObserver · Layout Instability API · 실사용자 모니터링 · Lighthouse · CrUX · 백분위수
레이아웃 이동이 일어나는 브라우저의 그리기 단계
브라우저 · 렌더링 · 레이아웃 · 리플로 · 리페인트 · 프레임 · 뷰포트 · DOM
레이아웃 이동을 일으키는 늦게 오는 내용
CLS 를 낮추는 기법
서버 사이드 렌더링 · 스켈레톤 UI · CSS transform · aspect-ratio · font-display · CSS
CLS 가 속하는 상위 분류
다른 이름: Cumulative Layout Shift · 누적 레이아웃 이동