정의
로딩 중 회색 뼈대(스켈레톤)를 보여줬는데, 데이터가 도착하는 순간 아래 내용이 위아래로 툭 밀리면서 화면이 흔들리는 것을 막고 싶을 때 이 개념을 쓴다.
Cumulative Layout Shift(CLS, 사용자 입력 없이 화면 요소가 얼마나 갑자기 움직였는지 누적한 점수) 는 사용자 입력 없이 보이는 요소가 이동할 때 누적되는 레이아웃 안정성 지표다. 로딩 스켈레톤(내용이 오기 전에 자리를 잡아 두는 회색 뼈대 UI)은 단순 장식이 아니라 최종 콘텐츠가 차지할 레이아웃 placeholder(자리 예약용 빈 상자)다. skeleton의 행 수·높이·폭이 로드 후 UI와 다르면, 데이터 도착 순간 아래 콘텐츠가 밀리며 CLS가 발생한다.
왜 필요한가
테이블·리스트·카드 그리드는 비동기 데이터 전후로 DOM 높이가 바뀐다. 스켈레톤을 짧게(예: 10행) 그리고 실제 데이터가 길면(예: 22행) fold 아래뿐 아니라 summary bar·pagination 주변에서도 shift가 누적된다. SSR prefetch로 데이터가 빨리 오면 skeleton→콘텐츠 전환이 더 눈에 띄어, prefetch와 skeleton 설계를 세트로 봐야 한다. Good CLS는 일반적으로 0.1 이하다.
동작 원리
CLS는 각 layout shift에 대해 impact fraction(움직인 요소가 화면에서 차지한 면적 비율) × distance fraction(그 요소가 화면 높이 대비 얼마나 멀리 이동했는지)을 더한다. 스켈레톤이 실제보다 작은 박스를 점유하면, 교체 시 impact가 커진다. animate-pulse처럼 opacity뿐 아니라 width가 변하는 placeholder는 숫자·카운트 영역에서 shift culprit이 되기 쉽다.
| 원인 | 전형적 증상 |
|---|---|
| 행 수 불일치 | skeleton 10행 → 실제 N행, body 높이 점프 |
| 고정 min-height 부족 | min-h-[600px] < 실제 테이블 높이 |
| 가변 폭 텍스트 | 로딩 바 → 실제 숫자 자릿수 차이 |
| 늦은 mount | 빈 영역 → 갑자기 테이블 전체 등장 |
실무 적용
- skeleton 행 수 = pagination page size.
- 숫자 슬롯:
min-width+tabular-nums; pulse 대신 정적 bar 고려. - 검증: Performance trace
Layout Shifts/ CLS, 동일 환경 3회 reload median.
트레이드오프
| 선택 | 이득 | 비용 |
|---|---|---|
| page size만큼 skeleton | CLS ↓ | 로딩 시 DOM 노드 일시 증가 |
| 큰 min-height 고정 | 안정적 | 데이터 적을 때 빈 공간 |
| skeleton 제거(hydrate only) | shift 없음 | cache miss 시 빈 화면 |
| spinner only | DOM 적음 | 0→N 높이 shift 가능 |
사용하면 안 되는 경우
- 높이가 본질적으로 예측 불가(무한 에디터, 미측정 동적 콘텐츠).
- 로딩이 극히 짧고 prefetch가 항상 성공해 skeleton이 거의 보이지 않는 경우(그래도 Good CLS 목표면 예약 유지 권장).
흔한 실수
- “skeleton이 있으면 CLS 안전” — 크기가 맞지 않으면 오히려 악화.
- 고정
min-height만 쓰고 행 수는 무시. - SSR prefetch 후 “skeleton 신경 안 써도 됨” — query key miss 시 flash·CLS 재발.
관련 개념
- critical-rendering-path — layout·paint 순서
- largest-contentful-paint — LCP는 양호해도 CLS는 별도
- ssr-hydration-mismatch — 첫 클라이언트 렌더와 서버 HTML 정합
- ssr-prefetch-query-cache-hydration — prefetch로 skeleton 노출 자체를 줄이기