본문 바로가기

프론트엔드

css animation steps로 Spinner 구현하기

반응형
  • 프로젝트: loplat ui
  • 키워드: css animation steps, sprite image, 용량 최적화
  • 상황
    1. 로딩 상태일 때 화면에 표시할 Loading UI Component가 필요하다.
    2. loplat ui library에서 재사용 가능한 컴포넌트를 직접 구현하고자한다.
    3. 로딩 애니메이션을 위해 gif를 사용할 경우 용량과 성능 문제가 발생한다.
    4. sprite image와 css animation의 steps 속성을 이용하여 애니메이션 효과를 구현할 수 있다.
  • 해결과정
    1. 먼저 디자이너로부터 30장 이상으로 이루어진 sprite image를 받고 img 태그의 src에 넣었다.
    2. React Component의 prop으로 duration(애니메이션 재생 시간), scale(컴포넌트 크기 조절), zIndex를 받도록 하고, emotion을 이용하여 img 태그에 css를 입힌다.
        export interface SpinnerProps {
        duration?: number;
        scale?: number;
        zIndex?: number;
      }
      
      export const CircleSpinner = ({ duration = 1200, scale = 1, zIndex = 0 }: SpinnerProps): React.ReactElement => {
        const steps = 60;
        return (
          <SpriteImage src={Circle} alt="" duration={duration} steps={steps} />
        );
      };​
    3. animation css 와 animation-timing-function 속성인 steps(https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function)로 애니메이션을 구현한다.
      steps는 일반적인 애니메이션처럼 연속적으로 움직이지 않고, 계단처럼 뚝뚝 끊겨 움직인다. 즉, 영화 필름처럼 여러장의 사진을 빠르게 보여주어 움직임을 구현하는 원리이다.
        const SpriteImage = styled.img<ImageProps>`
        animation: ${(props) => `play ${props.duration}ms steps(${props.steps}) infinite`};
      
        @keyframes play {
          from {
            transform: translateX(0);
          }
          to {
            transform: translateX(-100%);
          }
        }
      `;​
      이제 이미지가 왼쪽으로 이동(translateX)하면서 애니메이션처럼 움직이는 것을 확인할 수 있다.

    4. 하지만 눈에 보여야하는 것은 sprite image 전체가 아닌 '원 하나'이기 때문에 img를 Wrapper로 감싸주어야한다.
        export const CircleSpinner = ({ duration = 1200, scale = 1, zIndex = 0 }: SpinnerProps): React.ReactElement => {
        const steps = 60;
        return (
          <Wrapper width={6960} height={132} steps={steps} scale={scale} zIndex={zIndex}>
            <SpriteImage src={Circle} alt="" duration={duration} steps={steps} />
          </Wrapper>
        );
      };
      
      
      const Wrapper = styled.div<WrapperProps>`
        width: ${(props) => `calc(${props.width}px / ${props.steps})`};
        height: ${({ height }) => height}px;
        overflow: hidden;
        transform: scale(${({ scale }) => scale});
        z-index: ${({ zIndex }) => zIndex};
      `;
      전체 width(6960)와 height(132)는 실제 sprite image 의 크기와 똑같이 설정했다.
      Wrapper의 width는 '전체width / steps' 이다.(전체 이미지 중 하나의 원에 해당하는 width)
      또한, overflow: hidden으로 하나의 원만 보이도록 했다.

    5. 이제 하나의 원이 계속 돌아가는 애니메이션이 구현되었고, 같은 원리로 cube spinner도 만들었다.
      (https://loplat-ui.web.app/?path=/story/components-spinner--default)
    6. 안드로이드 기기에서 sprite image가 잘리는 버그가 있어(기기 성능 문제로 판단), 사진의 이미지 개수를 절반으로 줄여 이미지 용량을 줄였다.(circle 60 steps -> 30 steps, cube 55 steps -> 28 steps)
      webp를 사용하여 용량을 더 줄이려했지만 safari 크로스브라이징 이슈가 있어 png를 사용하기로 했다.(picture tag로 개선 가능할 것 같다.)
반응형