레이아웃 시프트(Layout Shift)
레이아웃 시프트(Layout Shift)는 웹 페이지가 로드되는 도중 요소들이 예기치 않게 움직이는 현상을 말합니다.
사용자가 페이지를 읽거나 상호작용하려는 순간에 콘텐츠가 갑자기 이동하면 불쾌한 사용자 경험을 유발할 수 있습니다. 이 현상은 주로 이미지, 광고, 폰트, iframe 등의 크기가 미리 지정되지 않았을 때 발생합니다.
Layout Shift의 원인
- 크기가 지정되지 않은 이미지
- 동적으로 로드되는 광고나 iframe
- 웹 폰트 로딩 지연 (FOIT/FOUT)
- 비동기적으로 삽입되는 DOM 요소
해결 방법
1. 이미지에 고정 크기 또는 aspect-ratio 지정
<!-- 예시: width와 height 명시 -->
<img src="image.jpg" width="600" height="400" alt="예시 이미지" />
<!-- CSS aspect-ratio를 사용하는 방법 -->
<style>
.image-container {
aspect-ratio: 3 / 2;
width: 100%;
}
.image-container img {
width: 100%;
height: auto;
}
</style>
<div class="image-container">
<img src="image.jpg" alt="예시 이미지">
</div>
2. CSS로 미리 공간 확보
레이아웃에서 이미지나 광고가 들어올 공간을 미리 확보해두면 시프트를 줄일 수 있습니다.
3. 웹 폰트 지연 문제 해결
font-display: swap; 사용 → 시스템 폰트로 먼저 렌더링 후 웹폰트 로드
@font-face {
font-family: 'MyFont';
src: url('myfont.woff2') format('woff2');
font-display: swap;
}
4. CLS (Cumulative Layout Shift) 측정 및 개선
Core Web Vitals의 하나인 CLS 지표를 사용하여 shift 현상을 수치화할 수 있습니다. Chrome DevTools, Lighthouse, Web Vitals API 등을 활용하세요.
5. Skeleton UI를 사용하는 해결 방법
Skeleton UI는 콘텐츠가 로드되기 전에 자리만 차지하고 있는 **회색 박스(또는 애니메이션된 로딩 상태)**를 보여주는 UI입니다. 이는 실제 콘텐츠가 로드될 때까지 레이아웃을 고정시켜주기 때문에 layout shift를 방지하는 데 효과적입니다.
Skeleton UI를 사용하는 해결 방법
예시: 이미지가 로딩될 때 Skeleton 적용
<style>
.skeleton {
background-color: #e0e0e0;
width: 100%;
padding-top: 66.66%; /* 3:2 비율 유지용 */
position: relative;
overflow: hidden;
}
.skeleton img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
.skeleton.loaded img {
opacity: 1;
}
</style>
<div class="skeleton" id="image-container">
<img src="image.jpg" alt="예시 이미지" onload="this.parentNode.classList.add('loaded')" />
</div>
위 예제에서는 이미지가 로드되기 전까지 padding-top으로 비율을 확보하고, 이미지가 로드되면 자연스럽게 교체됩니다.
Skeleton UI의 장점
- 예상 레이아웃 유지 → CLS 방지
- 사용자에게 진행 중인 로딩 신호 제공
- 자연스러운 전환으로 부드러운 UX
Skeleton UI 라이브러리 예시
- React Skeleton
라이브러리: react-loading-skeleton
import Skeleton from 'react-loading-skeleton';
function ImageLoader({ isLoading }) {
return (
<div style={{ width: 300, height: 200 }}>
{isLoading ? (
<Skeleton height={200} width={300} />
) : (
<img src="image.jpg" width={300} height={200} alt="Loaded" />
)}
</div>
);
}
Skeleton UI React (with useState) 예시
import React, { useState } from 'react';
function ImageWithSkeleton() {
const [loaded, setLoaded] = useState(false);
return (
<div
style={{
width: 300,
height: 200,
backgroundColor: loaded ? 'transparent' : '#e0e0e0',
position: 'relative',
overflow: 'hidden',
}}
>
<img
src="https://via.placeholder.com/300x200"
alt="Sample"
onLoad={() => setLoaded(true)}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
opacity: loaded ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
position: 'absolute',
top: 0,
left: 0,
}}
/>
</div>
);
}
export default ImageWithSkeleton;
Skeleton UI Vue (Composition API) 예시
<template>
<div :class="['skeleton', { loaded }]">
<img
src="https://via.placeholder.com/300x200"
alt="Sample"
@load="loaded = true"
/>
</div>
</template>
<script setup>
import { ref } from 'vue';
const loaded = ref(false);
</script>
<style scoped>
.skeleton {
width: 300px;
height: 200px;
background-color: #e0e0e0;
position: relative;
overflow: hidden;
}
.skeleton img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
.skeleton.loaded img {
opacity: 1;
}
</style>
정리
원인 | 해결 방법 |
이미지 크기 미지정 | width/height 또는 aspect-ratio 지정 |
광고/iframe | 고정된 크기 지정 또는 미리 자리 확보 |
웹폰트 지연 | font-display: swap 사용 |
비동기 DOM 요소 삽입 | 공간 미리 확보 또는 애니메이션 사용 |
콘텐츠 로딩 지연 | Skeleton UI로 자리 고정 후 콘텐츠 로딩 처리 |
'프론트엔드' 카테고리의 다른 글
CSS sahpe() (0) | 2025.05.10 |
---|---|
AVIF, WebP (0) | 2025.05.05 |
SEO (3) | 2025.05.03 |
robots.txt 자동 생성 (0) | 2025.05.03 |
xml-sitemaps.com (1) | 2025.05.03 |