스켈레톤 UI는 서버로부터 데이터를 가져오는 동안, 앞으로 보여질 컨텐츠를 대략적으로 표현하는 UI이다.
사용자는 빈 화면을 보며 하염없이 기다리는 것 보다, 스켈레톤 UI 를 통해 지루함을 해소할 수 있다.
스켈레톤 UI 는 일반적인 로딩 스피너보다 컨텐츠를 구체적으로 보여줄 수 있다.
이는 지루한 대기 시간도 줄일 수 있으며,
컴포넌트가 갑자기 불쑥 튀어나와 레이아웃이 크게 변경되는 CLS 도 방지할 수 있다. 따라서 SEO에도 좋은 점수를 얻을 수 있겠다.
CLS (Cumulative Layout Shift)
시각적인 안정성을 측정하는 Web vital 지표료, 우수한 사용자 경험을 제공하려면 0.1 이하의 CLS 를 유지해야한다.
이미지의 경로를 이미 클라이언트에서 가지고 있다면 이미지가 로드 되기 전에 스켈레톤 UI를 보여주면 되지만,
이미지 src
경로를 API 를 통해서 받아와서 다시 이미지를 로드해야 하는 경우엔 다음과 같이 두 가지 경우에도 스켈레톤 UI 를 보여주어야 한다.
- API fetch 가 아직 완료 되지 않아 이미지
src
가 없는 경우src
는 있지만image
로드가 완료되지 않은 경우
이 두 가지 경우를 모두 고려해서 SkeletonImage
컴포넌트를 생성했다.
import React, { ReactElement, useState } from 'react';
import { css, SerializedStyles } from '@emotion/react';
interface SkeletonImageProps {
src: string | undefined | null;
alt: string;
backgroundColor?: string;
imgStyle: SerializedStyles;
[key: string]: any;
}
/**
* 스켈레톤 UI를 보여주는 두 가지 경우
* 1. API Fetch 가 아직 완료 되지 않아서 src 가 없는 경우
* 2. src 는 있지만 image 로드가 완료 되지 않은 경우
*/
export function SkeletonImage({
src,
alt,
backgroundColor = '#F5F5F5',
imgStyle,
onLoad,
...props
}: SkeletonImageProps): ReactElement {
const [isLoading, setIsLoading] = useState(true);
// 1. API Fetch 가 아직 완료 되지 않아서 src 가 없는 경우
if (!src)
return (
<div css={[defaultSkeletonStyle(backgroundColor), imgStyle]} {...props}>
<div className="animationBar" />
</div>
);
return (
<>
// 2. src 는 있지만 image 로드가 완료 되지 않은 경우
{isLoading && (
<div css={[defaultSkeletonStyle(backgroundColor), imgStyle]} {...props}>
<div className="animationBar" />
</div>
)}
// 3. 이미지 로드가 완료된 경우
<img
src={src}
alt={alt}
css={[defaultImgStyle(isLoading), imgStyle]}
onLoad={(e) => {
if (onLoad) onLoad(e);
setIsLoading(false);
}}
{...props}
/>
</>
);
}
const defaultSkeletonStyle = (backgroundColor: string) => css`
background-color: ${backgroundColor};
width: 100%;
height: 100%;
@keyframes loading {
0% {
transform: translateX(0);
}
50%,
100% {
transform: translateX(100%);
}
}
.animationBar {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
rgba(245, 245, 245, 1) 0%,
#ffffffae 10%,
rgba(245, 245, 245, 1) 20%
);
animation: loading 1.5s infinite linear;
}
`;
const defaultImgStyle = (isLoading: boolean) => css`
display: ${isLoading ? 'none' : 'block'} !important;
`;
이미지가 로드 여부를 상태 isLoading
으로 관리했다.
isLoading
여부에 따라 img
컴포넌트를 조건부 렌더링 처리하면 load
이벤트가 발생하지 않는다.
따라서 img
컴포넌트의 노출 여부는 css 의 display
를 사용하여 처리했다.
만약 다음과 같이 img
컴포넌트를 isLoading
상태에 따라 조건부 렌더링하면 load
이벤트가 발생하지 않는다. 따라서 정상적으로 스켈레톤 UI를 보여줄 수 없으니 주의하자.
return (
<>
{isLoading && (
<div css={[defaultSkeletonStyle(backgroundColor), imgStyle]} {...props}>
<div className="animationBar" />
</div>
)}
// 주의 ! 이미지를 isLoading 에 따라 조건부 렌더링하면 load 이벤트가 발생하지 않음.
{isLoading &&
<img
src={src}
alt={alt}
css={imgStyle}
onLoad={(e) => {
if (onLoad) onLoad(e);
setIsLoading(false);
}}
{...props}
/>
}
</>
);
스켈레톤 애니메이션은 다음과 같이 css keyframes
을 사용하여 구현하였다.
<div css={[defaultSkeletonStyle(backgroundColor), imgStyle]} {...props}>
<div className="animationBar" />
</div>
const defaultSkeletonStyle = (backgroundColor: string) => css`
background-color: ${backgroundColor};
width: 100%;
height: 100%;
@keyframes loading {
0% {
transform: translateX(0);
}
50%,
100% {
transform: translateX(100%);
}
}
.animationBar {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
${backgroundColor} 0%,
#ffffffae 30%,
${backgroundColor} 60%
);
animation: loading 1.5s infinite linear;
}
`;
스켈레톤 UI를 적용하기 전과 적용 후에 라이트 하우스를 실행해 점수 변화를 확인했다.
적용 전
적용 후
최종 성능이 39 점에서 83점으로 드라마틱하게 좋게 측정되었다.
놀랍게도 CLS 뿐 아니라 TTI (Time To Interactive) 와 TBT(Total Blocking Time) 또한 좋아졌다. 그 이유는 정확히 파악은 안되지만, 웹 성능을 측정하는 기준이 생각보다 시각적인 레이아웃에 의존한다고 예측하고 있다.
하지만 실제 웹의 성능(속도, 접근성) 과는 별개로 사용성 측면에서 개선된 것이므로, 웹바이탈 점수가 좋게 나오더라도 성능 개선은 별도로 진행해야 할 것 같다.