개발 중인 여름방학 프로젝트의 사용자 경험을 증가시키기 위해 스켈레톤 UI 추가해볼 것이다.
1. 현재 상황
먼저, 현재 나의 상황이다. 네트워크를 느리게 설정하여 확인해보겠다.
그림을 보여줘야 하는 부분에서, api를 호출 중일 때 로딩 스피너가 보이고, (임의로 둠)
사진이 뜰 때 흰색 그림이 보인 후 그림이 다 보여지는 경우 있음


2. 스켈레톤 UI란?
실제 콘텐츠가 들어갈 자리를 잠시 대신하게 되는 빈 껍데기이다.
출처 : https://ui.toast.com/weekly-pick/ko_20201110
더 나은 UX를 위한 React에서 스켈레톤 컴포넌트 만들기
스켈레톤 컴포넌트가 무엇인지 알고 있는가? 스켈레톤 컴포넌트는 데이터를 가져오는 동안 콘텐츠를 표시하는 컴포넌트이다. 사용자는 콘텐츠를 기다리다가 쉽게 지치고 지루함을 느끼므로 단
ui.toast.com


서버에서 데이터를 가져오고 있다는 것을 알려주는 사용자 친화적인 UI이다.
만약 스켈레톤 UI가 없다면, 사용자는 빈 화면만 보게 되어 좋지 않은 사용자 경험을 겪게 된다.
이를 통해, 사용자의 대기 시간을 덜 지루하게 만들어준다.
스켈레톤 UI를 만들 때, 가장 중요한 것은 실제 UI와 최대한 비슷해야한다는 것이다.
실제 UI와 너무 큰 차이가 나게되면, 사용자가 이를 또 다른 독립적인 컴포넌트라고 인식할 수 있다.
3. 스켈레톤 UI 추가하기
3.1. 홈페이지에 스켈레톤 UI 추가
기존에 보이는 이미지와 같은 사이즈, border값을 적용하였다.
애니메이션 같은 경우에는, tailwind css를 사용하고 있었기 때문에, 간단히 추가할 수 있었다.
<javascript />
const HomeDiaryItemSkeleton = () => {
return (
<div className="h-[120px] w-[120px] animate-pulse rounded-md bg-gray-300"></div>
);
};
export default HomeDiaryItemSkeleton;
위 컴포넌트를 isLoading 중일 때 불러오면 되는데, 내 화면에서 4개 정도면 가로 페이지를 모두 커버하기 때문에, map함수로 4개를 넣어주었다.
<html />
isLoading ? (
<div className="flex gap-2">
{[1, 2, 3, 4].map((_, idx) => (
<HomeDiaryItemSkeleton key={"home my skeletion" + idx} />
))}
</div>
)

그러면 위와 같이, 이미지가 불러와지기 전 회색 스켈레톤 UI가 보이는 것을 확인할 수 있다!
3.2. 피드페이지에 스켈레톤 UI 추가
피드페이지에도 스켈레톤 UI를 추가해보겠다.
피드페이지의 각 아이템같은 경우에는, 홈페이지의 아이템처럼 크기가 일정하지 않다.
현재 idx에 따라서 높이와 너비를 다르게 설정해두어서, 스켈레톤 UI도 해당 높이와 너비를 적용해주어야 한다.
또한, 이 페이지는 현재 무한스크롤로 구현되어 있다.
3.3. isLoading & isFetcingNextPage
useInfiniteQuery로부터 현재 로딩중인지 속성을 꺼내와야 한다.
근데 처음에는 isLoading값만 가져와서 사용했다.
하지만, isLoading은 처음 데이터를 가져올 때만 true값이고, 그 이후에는 계속 false였다.
그래서, 다음 페이지를 가져오고 있음을 뜻하는 isFetchingNextPage 속성을 꺼내와 사용한다.
<typescript />
const {
data: diaryData,
fetchNextPage,
hasNextPage,
isLoading,
isFetchingNextPage,
} = useInfiniteQuery(["getPublicDiary"], fetchPublicData, {
getNextPageParam: (lastPage, pages) => {
if (lastPage && lastPage?.length > 0) return pages.length + 1;
else return undefined;
},
});
그리고, 스켈레톤 UI 호출하는 코드를 2번 써주어야 한다.
isLoading이 ture인 경우 (= 첫 데이터의 로딩) 와,
isFetchingNextPage가 true인 경우 (= 첫 페이지 이후가 로딩) 에 스켈레톤 UI를 호출해야 한다.
<html />
<div className="grid grid-flow-dense grid-cols-3 grid-rows-3">
{isLoading
? Array.from({ length: 12 }).map((_, idx) => (
<FeedItem idx={idx} key={"feedItem-skeleton" + idx} />
))
: diaryItems.map(({ diary, totalCount }, idx) => (
<FeedItem
image={diary.imageUrl}
idx={idx}
like={totalCount}
_id={diary.id}
key={"feedItem" + diary.id}
/>
))}
<div ref={ref} />
{isFetchingNextPage &&
Array.from({ length: 12 }).map((_, idx) => (
<FeedItem idx={idx} key={"feedItem-skeleton" + idx} />
))}
</div>
그리고, 기존의 feedItem의 너비와 높이를 그대로 이어가기 위해, 컴포넌트를 그대로 사용하면서, props 값의 변화를 주었다.
<typescript />
interface FeedItemProps {
image?: string;
like?: number;
idx: number;
_id?: string;
}
props 중 idx만 필수로 지정해주고, 나머지는 없어도되는 값으로 지정해준다.
그리고 나서 아래와 같이, like와 image 값이 유효할 때와 아닐때로 나누어서 리턴한다.
- like와 image 값이 유효하면, 실제 데이터를 불러오는 컴포넌트
- 유효하지 않으면, loading 컴포넌트로서 불러오는 컴포넌트
여기에서 getGridRatio()와 getSize()는 피드페이지에서 너비와 높이를 다르게 설정하기 위한 함수이다.
<typescript />
const getGridRatio = () => {
switch (idx % 6) {
case 0:
case 3:
case 5:
return "col-span-1 row-span-1";
case 1:
return "col-span-2 row-span-1";
case 2:
case 4:
return "col-span-1 row-span-2";
default:
return "col-span-1 row-span-1";
}
};
const getSize = () => {
return `${idx % 6 === 1 ? "w-[66vw] custom-breakpoint:w-[300px]" : "w-[33vw] custom-breakpoint:w-[150px]"} ${idx % 6 === 2 || idx % 6 === 4 ? "h-[66vw] custom-breakpoint:h-[300px]" : "h-[33vw] custom-breakpoint:h-[150px]"} `;
};
<html />
like != undefined && image ? (
<div
className={`${getGridRatio()} relative cursor-pointer ${getSize()} `}
onClick={linkToDetalPage}
>
<img
src={"https://" + image}
className={`${getSize()} object-cover`}
loading="lazy"
/>
<div
className={`absolute top-0 bg-black bg-opacity-10 ${getSize()}`}
></div>
<StarCountInImage like={like} />
</div>
) : (
<div className={`${getGridRatio()} ${getSize()} `}>
<div
className={`animate-pulse bg-gray-200 ${getSize()} border-solid border-gray-100 custom-breakpoint:border-[1px]`}
></div>
</div>
);

기존의 로딩 스피너보다, 스켈레톤 UI를 추가하니 조금 더 사용자가 기다릴 때 지루하지 않은 것 같다.
'FRONTEND > React' 카테고리의 다른 글
[ React ] 15초 후 다음 페이지로 자동 이동 기능 (0) | 2024.03.24 |
---|---|
[ React + vercel ] vercel로 배포한 React 앱에 vercel analytics 사용하기 (0) | 2024.03.19 |
[ React ] textarea / input 태그 maxLength 속성 한글에서만 작동하지 않음 해결 (0) | 2024.03.08 |
[ React + typeScript ] 카카오톡 공유 기능 ( KaKao Developers ) (0) | 2024.02.28 |
[ React] 페이지네이션 & sorting 구현하기 (2) | 2023.12.29 |