React | 이미지 스켈레톤 UI 적용하기 (CLS 개선하기, webvital 점수 개선)

dev_hee·2022년 6월 20일
7

React

목록 보기
5/7

스켈레톤 UI 란...

스켈레톤 UI는 서버로부터 데이터를 가져오는 동안, 앞으로 보여질 컨텐츠를 대략적으로 표현하는 UI이다.
사용자는 빈 화면을 보며 하염없이 기다리는 것 보다, 스켈레톤 UI 를 통해 지루함을 해소할 수 있다.

스켈레톤 UI 는 일반적인 로딩 스피너보다 컨텐츠를 구체적으로 보여줄 수 있다.
이는 지루한 대기 시간도 줄일 수 있으며,
컴포넌트가 갑자기 불쑥 튀어나와 레이아웃이 크게 변경되는 CLS 도 방지할 수 있다. 따라서 SEO에도 좋은 점수를 얻을 수 있겠다.

CLS (Cumulative Layout Shift)
시각적인 안정성을 측정하는 Web vital 지표료, 우수한 사용자 경험을 제공하려면 0.1 이하의 CLS 를 유지해야한다.

이미지 스켈레톤 UI

이미지의 경로를 이미 클라이언트에서 가지고 있다면 이미지가 로드 되기 전에 스켈레톤 UI를 보여주면 되지만,

이미지 src 경로를 API 를 통해서 받아와서 다시 이미지를 로드해야 하는 경우엔 다음과 같이 두 가지 경우에도 스켈레톤 UI 를 보여주어야 한다.

  1. API fetch 가 아직 완료 되지 않아 이미지 src 가 없는 경우
  2. 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}
        />
		}
    </>
  );

스켈레톤 UI 애니메이션

스켈레톤 애니메이션은 다음과 같이 css keyframes 을 사용하여 구현하였다.

  • 스켈레톤 UI
      <div css={[defaultSkeletonStyle(backgroundColor), imgStyle]} {...props}>
        <div className="animationBar" />
      </div>
  • 스켈레톤 UI CSS
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;
  }
`;

Web Vitals 성능 개선

스켈레톤 UI를 적용하기 전과 적용 후에 라이트 하우스를 실행해 점수 변화를 확인했다.

  • 적용 전

  • 적용 후

최종 성능이 39 점에서 83점으로 드라마틱하게 좋게 측정되었다.

놀랍게도 CLS 뿐 아니라 TTI (Time To Interactive) 와 TBT(Total Blocking Time) 또한 좋아졌다. 그 이유는 정확히 파악은 안되지만, 웹 성능을 측정하는 기준이 생각보다 시각적인 레이아웃에 의존한다고 예측하고 있다.

하지만 실제 웹의 성능(속도, 접근성) 과는 별개로 사용성 측면에서 개선된 것이므로, 웹바이탈 점수가 좋게 나오더라도 성능 개선은 별도로 진행해야 할 것 같다.

profile
🎨그림을 좋아하는 FE 개발자👩🏻‍💻

0개의 댓글