DEVELOP
article thumbnail

개발 중인 여름방학 프로젝트의 사용자 경험을 증가시키기 위해 스켈레톤 UI 추가해볼 것이다. 

 

현재 상황

먼저, 현재 나의 상황이다. 네트워크를 느리게 설정하여 확인해보겠다. 

그림을 보여줘야 하는 부분에서, api를 호출 중일 때 로딩 스피너가 보이고, (임의로 둠)

사진이 뜰 때 흰색 그림이 보인 후 그림이 다 보여지는 경우 있음 

스켈레톤 UI란?

실제 콘텐츠가 들어갈 자리를 잠시 대신하게 되는 빈 껍데기이다.

출처 : https://ui.toast.com/weekly-pick/ko_20201110

 

더 나은 UX를 위한 React에서 스켈레톤 컴포넌트 만들기

스켈레톤 컴포넌트가 무엇인지 알고 있는가? 스켈레톤 컴포넌트는 데이터를 가져오는 동안 콘텐츠를 표시하는 컴포넌트이다. 사용자는 콘텐츠를 기다리다가 쉽게 지치고 지루함을 느끼므로 단

ui.toast.com

 

서버에서 데이터를 가져오고 있다는 것을 알려주는 사용자 친화적인 UI이다.

만약 스켈레톤 UI가 없다면, 사용자는 빈 화면만 보게 되어 좋지 않은 사용자 경험을 겪게 된다. 

이를 통해, 사용자의 대기 시간을 덜 지루하게 만들어준다. 

 

스켈레톤 UI를 만들 때, 가장 중요한 것은 실제 UI와 최대한 비슷해야한다는 것이다. 

실제 UI와 너무 큰 차이가 나게되면, 사용자가 이를 또 다른 독립적인 컴포넌트라고 인식할 수 있다. 

 

 

스켈레톤 UI 추가하기

홈페이지에 스켈레톤 UI 추가 

기존에 보이는 이미지와 같은 사이즈, border값을 적용하였다.

애니메이션 같은 경우에는, tailwind css를 사용하고 있었기 때문에, 간단히 추가할 수 있었다.

const HomeDiaryItemSkeleton = () => {
  return (
    <div className="h-[120px] w-[120px] animate-pulse rounded-md bg-gray-300"></div>
  );
};

export default HomeDiaryItemSkeleton;

위 컴포넌트를 isLoading 중일 때 불러오면 되는데, 내 화면에서 4개 정도면 가로 페이지를 모두 커버하기 때문에, map함수로 4개를 넣어주었다. 

          isLoading ? (
            <div className="flex gap-2">
              {[1, 2, 3, 4].map((_, idx) => (
                <HomeDiaryItemSkeleton key={"home my skeletion" + idx} />
              ))}
            </div>
          )

그러면 위와 같이, 이미지가 불러와지기 전 회색 스켈레톤 UI가 보이는 것을 확인할 수 있다! 

 

피드페이지에 스켈레톤 UI 추가 

피드페이지에도 스켈레톤 UI를 추가해보겠다. 

 

피드페이지의 각 아이템같은 경우에는, 홈페이지의 아이템처럼 크기가 일정하지 않다.

현재 idx에 따라서 높이와 너비를 다르게 설정해두어서, 스켈레톤 UI도 해당 높이와 너비를 적용해주어야 한다. 

 

또한, 이 페이지는 현재 무한스크롤로 구현되어 있다.

isLoading & isFetcingNextPage 

useInfiniteQuery로부터 현재 로딩중인지 속성을 꺼내와야 한다.

근데 처음에는 isLoading값만 가져와서 사용했다. 

하지만, isLoading은 처음 데이터를 가져올 때만 true값이고, 그 이후에는 계속 false였다. 

그래서, 다음 페이지를 가져오고 있음을 뜻하는 isFetchingNextPage 속성을 꺼내와 사용한다. 

  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를 호출해야 한다. 

    <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 값의 변화를 주었다. 

interface FeedItemProps {
  image?: string;
  like?: number;
  idx: number;
  _id?: string;
}

 

props 중 idx만 필수로 지정해주고, 나머지는 없어도되는 값으로 지정해준다.

그리고 나서 아래와 같이, like와 image 값이 유효할 때와 아닐때로 나누어서 리턴한다.

  • like와 image 값이 유효하면, 실제 데이터를 불러오는 컴포넌트
  • 유효하지 않으면, loading 컴포넌트로서 불러오는 컴포넌트

여기에서 getGridRatio()와 getSize()는 피드페이지에서 너비와 높이를 다르게 설정하기 위한 함수이다.

더보기
  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]"} `;
  };
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 추가 결과

기존의 로딩 스피너보다, 스켈레톤 UI를 추가하니 조금 더 사용자가 기다릴 때 지루하지 않은 것 같다. 

profile

DEVELOP

@JUNGY00N