본문 바로가기
챌린지/패스트캠퍼스 공부 루틴 챌린지

패스트캠퍼스 챌린지 34일차 (Skeleton)

by 무벅 2022. 2. 26.
반응형

22.2.26 (토) +34days

스켈레톤(Skeleton) 이란 ?

데이터가 로드되기 전에 콘텐츠의 자리비움(placeholder)를 표시해서 사용자가 기다리는 시간을 좀 덜 지루하게 느끼게끔 하는 UI


스켈레톤 컴포넌트의 장단점

장점 (Pros)

아래의 표는 80명의 참가자를 대상으로 스켈레톤, 스피너, 블랭크 화면을 보여주며 테스트 했을 때 인지되는 로딩 시간을 보여줌. 결과적으로 블랭크 페이지 < 스피터 < 스켈레톤 순서대로 더 빠르다고 느끼게 됨.

사용자에게 인지된 로딩 시간

단점 (Cons)

아무래도 스켈레톤을 화면마다 표시해야 되기 때문에 상대적으로 시간이나 비용이 많이 든다.


스켈레톤 컴포넌트 사용 예시

페이스북

페이스북 뉴스피드, 2018

 

링크드인

링그드인 홈 화면, 2018

 

구글 드라이브

구글 드라이브 부분 로드된 상태, 2018

 

유튜브

유투브 홈 화면, 2018

 


더 나은 경험을 위한 스켈레톤 규칙

  • 스켈레톤은 콘텐츠의 로드를 방해하면 안된다. 당연한 얘기지만 실제 콘텐츠는 데이터를 가용할 수 있는 시점이 되면 즉시 스켈레톤을 대체해야 된다.
  • 스켈레톤을 디자인 할 때 애니메이션을 사용하는 것이 좋다. 애니메이션은 wave, pulse 중 wave를 사용하는 것이 로딩 시간을 더 짧게 느끼게끔 한다.
  • 느리고 안정적인 애니메이션을 사용하는 것이 로딩 시간을 더 짧게 느끼게끔 한다.

스켈레톤 실습

Preview

프로젝트 생성

$ npx create-react-app skeleton-playground --template typescript

@emotion/styled, @emotion/react 설치

npm i @emotion/styled @emotion/react

Skeleton.tsx

import styled from '@emotion/styled/macro';
import { keyframes, css } from '@emotion/react';
import { useMemo } from 'react';

interface Props {
  width?: number;
  height?: number;
  circle?: boolean;
  rounded?: boolean;
  count?: number;
  unit?: string; // px, % 등의 단위
  animation?: boolean; // 애니메이션 유무
  color?: string; // 스켈레톤의 배경 컬러
  style?: React.CSSProperties; // 스켈레톤의 추가적인 스타일 객체
}

const pulseKeyframe = keyframes`
0% {
  opacity: 1
}
50% {
  opacity: 0.4
}
100% {
  opacity: 1;
}
`;
const pulseAnimation = css`
  animation: ${pulseKeyframe} 1.5s ease-in-out infinite;
`;

const Base = styled.span<Props>`
  ${({ color }) => color && `background-color: ${color}`};
  ${({ rounded }) => rounded && 'border-radius: 8px'};
  ${({ circle }) => circle && 'border-radius: 50%'};
  ${({ width, height }) => (width || height) && 'display: block'};
  ${({ animation }) => animation && pulseAnimation};
  width: ${({ width, unit }) => width && unit && `${width}${unit}`};
  height: ${({ height, unit }) => height && unit && `${height}${unit}`};
`;

const Content = styled.span`
  opacity: 0;
`;

export default function Skeleton({
  animation = true,
  width,
  height,
  circle,
  rounded,
  count,
  unit = 'px',
  color = '#F4F4F4',
  style,
}: Props) {
  // count 6 =>  '------'
  const content = useMemo(
    () => [...Array({ length: count })].map(() => '-').join(''),
    [count]
  );
  return (
    <Base
      animation={animation}
      width={width}
      height={height}
      circle={circle}
      rounded={rounded}
      count={count}
      unit={unit}
      color={color}
      style={style}
    >
      <Content>{content}</Content>
    </Base>
  );
}

App.tsx

import React, { Component, useEffect, useState } from 'react';
import styled from '@emotion/styled/macro';
import Skeleton from './components/Skeleton';

const Base = styled.div`
  display: grid;
  width: 100%;
  grid-template-columns: repeat(5, 1fr);
  column-gap: 12px;
  row-gap: 24px;
`;

const Container = styled.div`
  display: flex;
  flex-direction: column;
  box-shadow: rgb(0 0 0 / 4%) 0px 4px 16px 0px;
  border-radius: 4px;
`;

const ImageWrapper = styled.div`
  width: 100%;
`;

const Image = styled.img`
  width: 100%;
  height: 100%;
  object-fit: cover;
`;

const Info = styled.div`
  padding: 1rem;
  display: flex;
  flex-direction: column;
  flex: 1 1 0%;
`;

const Title = styled.h4`
  margin: 0;
  padding: 0;
  font-size: 24px;
`;

const Description = styled.p`
  margin: 8px 0 0 0;
  padding: 0;
  font-size: 16px;
`;

const Placeholder: React.FC = () => (
  <Container>
    <ImageWrapper>
      <Skeleton width={320} height={220} />
    </ImageWrapper>
    <Info>
      <Skeleton width={150} height={29} rounded />
      <div style={{ height: '8px' }} />
      <Skeleton width={200} height={19} rounded />
    </Info>
  </Container>
);

const Item: React.FC = () => {
  return (
    <Container>
      <ImageWrapper>
        <Image src="https://img.webmd.com/dtmcms/live/webmd/consumer_assets/site_images/article_thumbnails/other/cat_relaxing_on_patio_other/1800x1200_cat_relaxing_on_patio_other.jpg" />
      </ImageWrapper>
      <Info>
        <Title>Cat taking a nap</Title>
        <Description>zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz</Description>
      </Info>
    </Container>
  );
};

function App() {
  const [loading, setLoading] = useState<boolean>(true);

  useEffect(() => {
    setTimeout(() => setLoading(false), 2000);
  }, []);

  return (
    <Base>
      {loading
        ? Array.from({ length: 25 }).map((_, idx) => <Placeholder key={idx} />)
        : Array.from({ length: 25 }).map((_, idx) => <Item key={idx} />)}
    </Base>
  );
}

export default App;

 

 

 

 

 

 

 

 

https://bit.ly/37BpXiC

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다

 

 

 

 

 

 

반응형

댓글