안녕하세요.
이번에는 스켈레톤 로딩 UI를 도입하면서 느꼈던 부분들을 공유드릴려고 합니다.
스켈레톤 로딩 UI의 경우 사용자가 콘텐츠 로딩 중에도 페이지 구조를 파악할 수 있도록 돕는 방식입니다.
특히, 네트워크 응답 시간이 긴 경우나 데이터 요청이 많은 화면에서 유용합니다.
아래는 스켈레톤 로딩 UI를 도입하며 느꼈던 장점과 고려해야 할 점들입니다.
사용자 경험 개선 :
단순히 로딩 스피너를 보여주는 것보다, 스켈레톤은 콘텐츠 크기와 배치를 미리 보여주기 때문에 긴 로딩 시간에도 사용자 불편을 최소화합니다.
네트워크 지연이나 데이터 처리로 인해 로딩 시간이 길어지는 경우, 스켈레톤 UI는 특히 유용합니다.
부분 로딩 :
데이터가 단계적으로 로드될 경우, 스켈레톤 UI는 특정 부분만 로딩 중임을 표시하는 데 적합합니다.
구현 복잡성 :
화면 구조가 복잡할수록 스켈레톤을 각 화면에 맞게 구현하는 데 추가적인 작업이 필요했습니다. 특히, 다양한 뷰포트와 화면 비율을 지원해야 하는 경우 더 많은 시간과 노력이 요구되었습니다.
성능 영향 :
스켈레톤 UI는 단순한 스피너보다 더 많은 DOM 요소를 생성할 수 있어, 성능에 영향을 미칠 가능성이 있음을 확인했습니다. 따라서 경량화된 컴포넌트를 사용하거나 성능 최적화를 함께 고려해야 했습니다.
전체적으로 스켈레톤을 보여줄 페이지
1. 대량의 데이터 패칭이 이루어지고, 화면 전환 후 로딩 시간이 상대적으로 길어질 가능성이 있는 페이지
2. 페이지의 대부분이 비어있거나 초기 상태에서 콘텐츠가 없을 가능성이 큰 페이지
예: 대시보드, 게시판 목록, 검색 결과 페이지 등.
* 데이터가 로드될 때까지 사용자 경험을 유지하기 위해 전체적인 스켈레톤 UI가 필요함.
부분적으로 스켈레톤을 보여줄 페이지
1. 상대적으로 적은 데이터 패칭이 이루어지며, 화면의 일부만 비어 있을 가능성이 있는 페이지입니다.
2. 데이터 패칭의 범위가 제한적이며, 주요 레이아웃은 이미 렌더링된 상태.
예: 프로필 페이지, 상품 상세 페이지, 또는 모달 로딩 시.
* 스켈레톤 UI를 필요로 하는 구간은 컨텐츠의 특정 영역으로 제한됨.
//@skeleton/common/SkeletonProfileCard
...
import { Skeleton } from '@mui/material';
const SkeletonProfileCard = () => {
return (
<DeferredComponent>
<div className='px-[12px] pt-[20px] pb-[40px]'>
<Skeleton
variant='rounded'
width='100%'
height={229}
animation='wave'
sx={{ borderRadius: '25px' }}
/>
</div>
</DeferredComponent>
);
};
export default SkeletonProfileCard;
프로젝트 메인 홈 화면에서는 Suspense를 활용해 부분적으로 스켈레톤 UI를 적용하고 있습니다.
프로젝트 홈 화면의 경우, 메인 프로필 정보를 제외하면 추가적인 데이터 패칭이 이루어지지 않기 때문에, 전체적인 스켈레톤 UI보다는 특정 영역에만 부분적인 스켈레톤 UI를 적용하는 방식이 적합했습니다. 이를 통해 화면의 주요 레이아웃은 즉시 렌더링되며, 데이터 로드가 필요한 구역에서만 스켈레톤이 표시됩니다.
Suspense를 통한 스켈레톤 UI 구현
React의 Suspense를 사용해, 데이터 로딩 중인 특정 컴포넌트만 스켈레톤 UI로 대체하고 있습니다. 이를 통해 렌더링 성능을 유지하면서도, 필요한 부분에만 경량화된 스켈레톤 컴포넌트를 적용할 수 있었습니다. 아래는 주요 구현 예시입니다.
<section className='flex flex-col'>
<div className='px-1'>
<Suspense fallback={<SkeletonProfileCard />}>
<GuardianPatientCardSpread
userId={userId}
patientList={patientList}
/>
</Suspense>
</div>
</section>
경량화된 스켈레톤 컴포넌트 사용
Material-UI에서 제공하는 Skeleton 컴포넌트를 활용하여, 불필요한 DOM 요소를 줄이고 경량화된 로딩 화면을 구성했습니다. 이를 통해 성능 부담을 최소화하면서도, 자연스러운 로딩 경험을 제공할 수 있었습니다.
지연 시간 기반 데이터 표시 전략
또한, 스켈레톤 UI 적용 시 지연 시간을 고려한 표시 전략을 도입했습니다.
1초 미만의 지연 시간에서는 스켈레톤 UI를 표시하지 않고, 데이터를 즉시 렌더링하도록 처리했습니다.
이는 "1초 미만의 로딩 시간에서는 스켈레톤보다 데이터를 바로 보여주는 것이 사용자 경험에 더 유리하다"는 연구 결과를 바탕으로 적용을 결정 하였습니다.
import { PropsWithChildren, useEffect, useState } from 'react';
// ** 지연시간 200ms 미만일 때 스켈레톤 미노출 / 200ms 이상일 때 스켈레톤 노출 **
const DeferredComponent = ({ children }: PropsWithChildren) => {
const [isDeferred, setIsDeferred] = useState(false);
useEffect(() => {
const timeOut = setTimeout(() => {
setIsDeferred(true);
}, 200);
return () => clearTimeout(timeOut);
}, []);
if (!isDeferred) return null;
return <>{children}</>;
};
export default DeferredComponent;
이미지 로드와 스켈레톤 UI 적용 방식
이미지 로딩은 사용자가 체감하는 시각적 완성도에 큰 영향을 미칩니다. 따라서, 이미지를 로드하기 전 스켈레톤 UI를 보여주는 방식을 채택하여 더 나은 사용자 경험을 제공하고자 고민했습니다.
Next.js의 Image 컴포넌트에서 제공하는 onLoadingComplete 및 onLoad 이벤트를 활용하여, 이미지가 로드되기 전에는 스켈레톤 UI를 표시하고, 로드가 완료된 후에는 스켈레톤을 숨기는 방식으로 구현했습니다.
import React, { ReactElement, useState } from 'react';
import Image from 'next/image';
import { Skeleton } from '@mui/material';
interface SkeletonImageProps {
src: string | undefined | null;
alt: string;
imgStyle: string;
[key: string]: any;
}
const SkeletonImage = ({
src,
alt,
imgStyle,
onLoad,
...props
}: SkeletonImageProps) => {
const [isLoading, setIsLoading] = useState(true);
if (!src) {
return <Skeleton variant='rectangular' className={`${imgStyle}`} />;
}
return (
<>
{isLoading && (
<>{isLoading && <Skeleton variant='rectangular' className={`${imgStyle}`} />}</>
)}
<Image
src={src}
alt={alt}
fill
priority={true}
onLoad={(e) => {
if (onLoad) onLoad(e);
setIsLoading(false);
}}
{...props}
/>
</>
);
};
export default SkeletonImage;
이미지 로딩을 위한 스켈레톤 UI를 공통 컴포넌트로 관리함으로써, 모든 이미지에 대한 로딩 상태를 일관되게 처리하고, 개발 시간과 유지 보수 비용을 절감할 수 있었습니다. 팀원들 간에도 코드가 일관성 있게 유지될 수 있게 했습니다.😊