22.2.26 (토) +34days
스켈레톤(Skeleton) 이란 ?

데이터가 로드되기 전에 콘텐츠의 자리비움(placeholder)를 표시해서 사용자가 기다리는 시간을 좀 덜 지루하게 느끼게끔 하는 UI
스켈레톤 컴포넌트의 장단점
장점 (Pros)
아래의 표는 80명의 참가자를 대상으로 스켈레톤, 스피너, 블랭크 화면을 보여주며 테스트 했을 때 인지되는 로딩 시간을 보여줌. 결과적으로 블랭크 페이지 < 스피터 < 스켈레톤 순서대로 더 빠르다고 느끼게 됨.

단점 (Cons)
아무래도 스켈레톤을 화면마다 표시해야 되기 때문에 상대적으로 시간이나 비용이 많이 든다.
스켈레톤 컴포넌트 사용 예시
페이스북

링크드인

구글 드라이브

유튜브

더 나은 경험을 위한 스켈레톤 규칙
- 스켈레톤은 콘텐츠의 로드를 방해하면 안된다. 당연한 얘기지만 실제 콘텐츠는 데이터를 가용할 수 있는 시점이 되면 즉시 스켈레톤을 대체해야 된다.
- 스켈레톤을 디자인 할 때 애니메이션을 사용하는 것이 좋다. 애니메이션은 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;


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