티스토리 뷰

Situation

경찰청 공공 API를 활용한 분실물/유실물 찾기 서비스 제작 프로젝트 진행 중 메인 화면에 거슬리는 것이 하나 있었다.

주인을 찾아요 아래에 최근 습득물을 보여주는 UI가 서버에서 데이터를 받아오는 동안 화면에 보이지 않았다가 뒤늦게 나타난다.

기다리면 갑자기 UI가 나타남

 

물론 이 메인 화면을 구경하고 있는 사용자가 있을 수 있겠지만, 보통은 본인이 원하는 메뉴를 탐색하면 그곳을 클릭하기 때문에 저 자리에 어떤 것이 있는지 사용자가 인식하지 못하고 지나칠 수도 있다.

Task

그래도 나름 사용자를 위해서 만든 기능인지라, 저곳에 무언가가 있다는 것을 사용자에게 인식시켜 줄 필요가 있었다.

Action

출처: Tailwind CSS 공홈

그래서 생각난 것이 스켈레톤 라이브러리다.

 

스켈레톤 라이브러리는 웹 페이지나 애플리케이션에서 데이터 로딩 과정을 사용자에게 시각적으로 보다 친근하게 표현하기 위해 사용되는 라이브러리이다.

 

데이터가 비동기적으로 로드되는 동안 사용자에게 빈 화면을 보여주는 대신, 컨텐츠가 로드될 위치에 스켈레톤 스크린(skeleton screens)이라고 불리는 플레이스홀더를 표시한다.

이 플레이스홀더는 일반적으로 컨텐츠의 형태와 유사한 라인, 사각형, 원 등으로 구성되어 있고 애니메이션 효과를 통해 데이터가 로딩 중임을 나타낸다.

스켈레톤 라이브러리의 주요 목적은 사용자 경험(UX)을 향상시키는 것이다. 

데이터 로딩 시간 동안 사용자의 주의를 끌고, 애플리케이션이 여전히 반응하고 있으며 데이터를 불러오고 있다는 인상을 줌으로써 사용자의 인내심을 증가시킬 수 있다.

이러한 방식은 사용자가 빈 화면을 보는 것보다 훨씬 더 긍정적인 경험을 제공한다.

 

따라서 현재 문제를 해결하기 위해서는 가장 안성맞춤인 셈이다.

 

스켈레톤 스크린은 실제 컨텐츠의 레이아웃과 유사하게 디자인해서 UI의 일관성을 유지해야 한다.

또, 반짝이는 등 애니메이션을 추가해서 데이터 로딩 진행 상태에 대한 시각적 피드백을 사용자에게 제공하여 작동 중이라는 것을 인식시켜야 한다.

 

사진과 같이 레이아웃과 유사하게 디자인을 해야했다.

 

Skeleton.tsx

const Skeleton: React.FC = () => {
  return (
    <div className="mb-12px flex h-140px w-335px items-center justify-between rounded-20px bg-white pl-20px pr-10px shadow">
      <div className="flex-grow space-y-3">
        <div className="h-26px w-128px animate-pulse rounded bg-slate-200"></div>
        <div className="h-21px w-61px animate-pulse rounded-full bg-slate-200"></div>
        <div className="h-16px w-46px animate-pulse rounded bg-slate-200"></div>
        <div className="h-16px w-77px animate-pulse rounded bg-slate-200"></div>
      </div>
      <div
        className={`animate-shimmer h-120px w-120px animate-pulse rounded-14px bg-slate-200`}
      ></div>
    </div>
  );
};

export default Skeleton;

 

완성된 모습

꽤 비슷한 모양으로 레이아웃을 짰다.

어플리케이션이 작동 중임을 알려주기 위해서 Tailwind CSS에서 제공하는 animation-pulse로 애니메이션을 적용해주었다.

 

그 다음은 이렇게 완성한 Skeleton 컴포넌트를 적용시킬 일만 남았다.

 

SwiperItem.tsx

...
import { useEffect, useState } from 'react';
import Skeleton from './Skeleton';

...

const SwiperItem: React.FC = () => {
  ...
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    (async () => {
      setIsLoading(true);
      ...
      setIsLoading(false);
    })();
  }, []);

  if (isLoading) {
    return <Skeleton />;
  }

  return (
    ... // Swiper가 적용될 코드
  );
};

export default SwiperItem;

ㄴ 상관없는 코드는 ...으로 처리

 

useState를 사용하여 상태를 관리하고 비동기 통신 코드에서 setter 함수에 true로 지정하고 데이터 통신을 다 받아오면 setter 함수에 false 값을 지정하였다.

 

그 아래에 if문을 사용하여 isLoading이 true인 경우 Skeleton 컴포넌트를 내뱉도록 하였다.

 

이렇게 하면 스켈레톤 UI가 실제 UI가 들어갈 자리를 차지하고 있다가 데이터를 다 받아오면 setter 함수에 의해 false가 되어 스켈레톤 UI는 사라지고 실제 UI가 들어간다.

Result

스켈레톤 UI가 적용된 모습

 

데이터를 받아오는 속도가 조금 느리긴 하지만 문제 해결은 완료되었다.

레이아웃을 일치시켜 이질감을 없애는 데 신경을 썼다.

 

다음에도 동일한 문제를 해결하기 위해 자주 사용할 것 같다.