[RIW]부드럽게 움직이는 숫자창을 구현해보자.

강경석(핸디)·2021년 9월 15일
3

시작

가끔식 유튜브를 돌아다니다 CSS로 동적이고 화려한 컴포넌트? 위젯?을 구현하는 것을 보곤합니다. 그리고 괜찮아보이는 것들을 제 프로젝트에 적용합니다.
다만 대부분은 CSS 유튜버들은 바닐라 자바스크립트 기반으로 작성을 하고 저는 React를 사용하기에 다시한번 구현해야한다는 번거로움이 있긴합니다.
그래도 이쁘기에 구현을 해서 가져다 쓰는데, 그걸 오픈소스 기반으로 한번 만들어보면 어떨까 라는 생각을 가지게 되었고 일단 단기적인 목표로 주마다 위젯 하나씩을 구현해보는 것으로 목표를 세웠습니다.

구현하고자 하는 위젯

해당 시리즈의 첫번째로 로빈후드의 홈페이지를 한번 살펴보겠습니다.

보시다시피 Tesla 하단에 있는 US$742.00 이란 숫자가 위아래 슬라이드 되면서 숫자가 표시되는 것을 볼 수 있습니다.
그래서 이번에는 이걸 한번 만들어볼겁니다.

아이디어

첫번째 시도 : Scroll 사용

한 3개월 전쯤인가 CSS에 대해 공부를 하고 있을때 이걸 보고 구현을 시도해본 경험이 있습니다.

차트 아래에 숫자가 대상이었습니다. 이때에는 숫자를 세로로 쭉 세운뒤에 Scroll를 바꾸며 구현을 했었습니다. 하지만 보시다시피 훨씬 투박하고 이상합니다.

두번째 시도 : CSS 이미지 스프라이트(Image Sprite)

이번에는 CSS 이미지 스프라이트를 이용해보기로 했습니다.

CSS 이미지 스프라이트란?
본래 CSS 이미지 스프라이트는 여러 개의 이미지를 하나의 이미지로 합쳐서 관리하는 최적화 기법에 해당합니다.
각각의 이미지를 매번 서버에 요청하지 말고 통합된 이미지로 요청한다음 필요한 부분만 가져다 쓰는 방법입니다.

대동여지도가 정확한 예시는 아니지만 저렇게 각각의 이미지를 그려서 대동여지도 전도(스프라이트된 이미지)를 서버에서 한번에 요청하고 실제 사용할 때는 위치별로 잘라서 사용한다 라는 방법입니다.

사용된 이미지가 많을 경우 웹 브라우저는 서버에 해당 이미지의 수만큼 요청해야만 하므로 웹 페이지의 로딩 시간이 오래 걸리게 됩니다.

이미지 스프라이트(image sprite)를 사용하면 이미지를 다운받기 위한 서버 요청을 단 몇 번으로 줄일 수 있습니다.

다시 돌아가서

첫번째에는 직접 숫자를 만들어서 div로 넣어주다보니 CSS로 애니메이션 넣기가 힘들었는데 이미지로 관리하다보니 애니메이션 넣기가 훨씬 수월해졌습니다.

import React from "react";
import { makeStyles } from "@material-ui/core/styles";

const useStyles = makeStyles((theme) => ({
  ScrollAnimatedNumberLayout: {
    position: "relative",
    "& > span": {
      padding: "0px 2px 0px 2px",
    },
  },
  numberSpan: {
    transition: "all 0.5s ease",
    display: "inline-block",
    color: "transparent",
    backgroundPositionX: "center",
    backgroundImage:
      "url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMzAwIiB2aWV3Qm94PSIwIDAgMjAgMzAwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJNOS42IDIzLjNDOC4yIDIzLjMgNyAyMi45IDYgMjJDNSAyMS4xIDQuNCAyMC4xIDQuMyAxOC43SDcuMUM3LjIgMTkuNCA3LjQgMTkuOSA3LjkgMjAuM0M4LjQgMjAuNyA4LjkgMjAuOSA5LjcgMjAuOUMxMS4zIDIwLjkgMTIuMiAyMC4yIDEyLjYgMTguOUMxMyAxNy41IDEzLjIgMTYuMyAxMy4zIDE1LjJDMTIuOSAxNS44IDEyLjQgMTYuMyAxMS43IDE2LjdDMTEgMTcuMSAxMC4zIDE3LjIgOS41IDE3LjJDNy45IDE3LjIgNi41IDE2LjcgNS41IDE1LjdDNC41IDE0LjcgNCAxMy4zIDQgMTEuNkM0IDEwIDQuNSA4LjYgNS42IDcuNkM2LjYgNi41IDguMSA2IDEwIDZDMTIuNSA2IDE0LjEgNi44IDE0LjkgOC41QzE1LjcgMTAuMiAxNi4xIDEyLjIgMTYuMSAxNC43QzE2LjEgMTYuNyAxNS42IDE4LjYgMTQuNyAyMC41QzEzLjggMjIuNCAxMi4xIDIzLjMgOS42IDIzLjNaTTEzLjEgMTEuN0MxMy4xIDEwLjggMTIuOCAxMCAxMi4zIDkuM0MxMS44IDguNiAxMSA4LjMgOS45IDguM0M4LjkgOC4zIDguMSA4LjYgNy42IDkuM0M3LjEgMTAgNi44IDEwLjggNi44IDExLjdDNi44IDEyLjYgNyAxMy40IDcuNSAxNC4xQzggMTQuOCA4LjggMTUuMiA5LjggMTUuMkMxMC45IDE1LjIgMTEuNyAxNC44IDEyLjIgMTQuMUMxMi43IDEzLjQgMTMuMSAxMi42IDEzLjEgMTEuN1oiIGZpbGw9ImJsYWNrIi8+CjxwYXRoIGQ9Ik0xNi4yIDQ4QzE2LjIgNDkuNiAxNS42IDUwLjkgMTQuNSA1MS44QzEzLjMgNTIuOCAxMS45IDUzLjIgMTAuMSA1My4yQzguMyA1My4yIDYuOSA1Mi43IDUuNyA1MS44QzQuNSA1MC45IDQgNDkuNiA0IDQ4QzQgNDcgNC4zIDQ2LjEgNC45IDQ1LjRDNS40IDQ0LjYgNi4yIDQ0LjIgNy4xIDQzLjlDNi4zIDQzLjYgNS43IDQzLjIgNS4zIDQyLjZDNC45IDQyIDQuNiA0MS4zIDQuNiA0MC41QzQuNiAzOS4xIDUuMSAzOCA2IDM3LjJDNi45IDM2LjQgOC4zIDM2IDEwIDM2QzExLjcgMzYgMTMuMSAzNi40IDE0IDM3LjJDMTQuOSAzOCAxNS40IDM5LjEgMTUuNCA0MC41QzE1LjQgNDEuMyAxNS4yIDQyIDE0LjggNDIuNkMxNC40IDQzLjIgMTMuOCA0My42IDEzIDQzLjlDMTQgNDQuMSAxNC44IDQ0LjYgMTUuMyA0NS4zQzE2IDQ2LjEgMTYuMiA0NyAxNi4yIDQ4Wk02LjggNDhDNi44IDQ5IDcuMSA0OS43IDcuNyA1MC4zQzguMyA1MC44IDkuMSA1MS4xIDEwLjEgNTEuMUMxMS4xIDUxLjEgMTEuOCA1MC44IDEyLjUgNTAuM0MxMy4xIDQ5LjggMTMuNCA0OSAxMy40IDQ4QzEzLjQgNDcuMSAxMy4xIDQ2LjQgMTIuNSA0NS45QzExLjkgNDUuNCAxMS4xIDQ1LjEgMTAuMSA0NS4xQzkuMSA0NS4xIDguNCA0NS40IDcuNyA0NS45QzcuMSA0Ni40IDYuOCA0Ny4xIDYuOCA0OFpNMTIuOCA0MC42QzEyLjggMzkuOCAxMi41IDM5LjEgMTIgMzguN0MxMS41IDM4LjMgMTAuOCAzOCAxMC4xIDM4QzkuNCAzOCA4LjcgMzguMiA4LjIgMzguN0M3LjcgMzkuMSA3LjQgMzkuOCA3LjQgNDAuNkM3LjQgNDEuNCA3LjcgNDIgOC4yIDQyLjRDOC43IDQyLjggOS40IDQzIDEwLjIgNDNDMTEgNDMgMTEuNiA0Mi44IDEyLjIgNDIuNEMxMi42IDQyIDEyLjggNDEuNCAxMi44IDQwLjZaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNNCA2OS40VjY3SDE2VjY5LjNDMTQuMiA3MS4zIDEyLjcgNzMuNCAxMS41IDc1LjdDMTAuNCA3OCA5LjcgODAuNyA5LjQgODMuN0g2LjNDNi41IDgxLjEgNy4yIDc4LjUgOC41IDc2LjFDOS44IDczLjYgMTEuMyA3MS41IDEzLjEgNjkuNUg0VjY5LjRaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMTAuNiA5NkMxMiA5NiAxMy4yIDk2LjQgMTQuMiA5Ny4yQzE1LjIgOTggMTUuOCA5OS4xIDE1LjggMTAwLjVIMTNDMTIuOSA5OS44IDEyLjcgOTkuMyAxMi4yIDk4LjhDMTEuOCA5OC40IDExLjIgOTguMiAxMC40IDk4LjJDOC44IDk4LjIgNy45IDk4LjkgNy41IDEwMC4yQzcuMSAxMDEuNSA2LjkgMTAyLjggNi44IDEwMy45QzcuMiAxMDMuMyA3LjcgMTAyLjggOC40IDEwMi40QzkuMSAxMDIuMSA5LjggMTAxLjkgMTAuNiAxMDEuOUMxMi4zIDEwMS45IDEzLjcgMTAyLjQgMTQuNyAxMDMuNUMxNS43IDEwNC42IDE2LjEgMTA1LjkgMTYuMSAxMDcuNUMxNi4xIDEwOS4xIDE1LjYgMTEwLjUgMTQuNSAxMTEuNUMxMy40IDExMi42IDEyIDExMy4xIDEwLjMgMTEzLjFDNy43IDExMy4xIDYgMTEyLjMgNS4yIDExMC42QzQuNCAxMDguOSA0IDEwNi45IDQgMTA0LjRDNCAxMDIuNCA0LjUgMTAwLjUgNS40IDk4LjZDNi40IDk2LjkgOC4xIDk2IDEwLjYgOTZaTTcuMSAxMDcuNkM3LjEgMTA4LjUgNy40IDEwOS4zIDcuOSAxMTBDOC40IDExMC43IDkuMiAxMTEuMSAxMC4yIDExMS4xQzExLjIgMTExLjEgMTIgMTEwLjcgMTIuNSAxMTBDMTMgMTA5LjMgMTMuMyAxMDguNSAxMy4zIDEwNy42QzEzLjMgMTA2LjcgMTMgMTA1LjkgMTIuNSAxMDUuMkMxMiAxMDQuNSAxMS4yIDEwNC4yIDEwLjIgMTA0LjJDOS4yIDEwNC4yIDguNCAxMDQuNSA3LjkgMTA1LjJDNy40IDEwNS44IDcuMSAxMDYuNiA3LjEgMTA3LjZaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNNC40IDEzNS4xTDYuMyAxMjZIMTUuM1YxMjguNEg4LjRMNy41IDEzMi42QzcuOSAxMzIuMiA4LjQgMTMxLjkgOC45IDEzMS43QzkuNSAxMzEuNSAxMC4xIDEzMS40IDEwLjcgMTMxLjRDMTIuNCAxMzEuNCAxMy44IDEzMS45IDE0LjcgMTMzQzE1LjYgMTM0LjEgMTYuMSAxMzUuNCAxNi4xIDEzN0MxNi4xIDEzOC4zIDE1LjYgMTM5LjcgMTQuNyAxNDFDMTMuOCAxNDIuMyAxMi4yIDE0MyAxMC4xIDE0M0M4LjQgMTQzIDcgMTQyLjYgNS44IDE0MS43QzQuNiAxNDAuOCA0IDEzOS42IDQgMTM3LjlINi44QzYuOSAxMzguOCA3LjIgMTM5LjUgNy44IDE0MEM4LjQgMTQwLjUgOS4xIDE0MC43IDEwIDE0MC43QzExLjIgMTQwLjcgMTIuMSAxNDAuMyAxMi41IDEzOS42QzEzIDEzOC45IDEzLjIgMTM4IDEzLjIgMTM3QzEzLjIgMTM2LjEgMTIuOSAxMzUuMyAxMi40IDEzNC42QzExLjkgMTMzLjkgMTEgMTMzLjYgOS45IDEzMy42QzkuMyAxMzMuNiA4LjcgMTMzLjcgOC4yIDEzMy45QzcuNyAxMzQuMSA3LjMgMTM0LjUgNy4xIDEzNS4xSDQuNFYxMzUuMVoiIGZpbGw9ImJsYWNrIi8+CjxwYXRoIGQ9Ik0xNi40IDE2Ny41VjE2OS42SDE0LjJWMTczLjdMMTEuNSAxNzMuNlYxNjkuN0g0VjE2N0wxMS41IDE1N0gxNC4yVjE2Ny41SDE2LjRaTTExLjQgMTYwLjFMNi4yIDE2Ny41SDExLjVMMTEuNCAxNjAuMVoiIGZpbGw9ImJsYWNrIi8+CjxwYXRoIGQ9Ik0xMy40IDE5OEMxMy40IDE5Ni43IDEyLjkgMTk1LjkgMTIgMTk1LjZDMTEgMTk1LjMgMTAgMTk1LjEgOC45IDE5NS4yVjE5My4yQzkuOCAxOTMuMiAxMC43IDE5My4xIDExLjUgMTkyLjdDMTIuMyAxOTIuMyAxMi43IDE5MS42IDEyLjcgMTkwLjZDMTIuNyAxODkuOSAxMi40IDE4OS4zIDExLjkgMTg4LjlDMTEuNCAxODguNSAxMC43IDE4OC4yIDEwIDE4OC4yQzkgMTg4LjIgOC4zIDE4OC42IDcuOCAxODkuM0M3LjMgMTkwIDcuMSAxOTAuOCA3LjEgMTkxLjZINC4zQzQuNCAxOTAgNC45IDE4OC42IDUuOSAxODcuNkM2LjkgMTg2LjUgOC4zIDE4NiAxMC4xIDE4NkMxMS41IDE4NiAxMi43IDE4Ni40IDEzLjkgMTg3LjFDMTUuMSAxODcuOCAxNS42IDE4OSAxNS42IDE5MC40QzE1LjYgMTkxLjIgMTUuNCAxOTIgMTUgMTkyLjZDMTQuNiAxOTMuMiAxNCAxOTMuNyAxMy4xIDE5NEMxNC4xIDE5NC4yIDE0LjkgMTk0LjcgMTUuNCAxOTUuNEMxNiAxOTYuMiAxNi4yIDE5Ny4xIDE2LjIgMTk4LjFDMTYuMiAxOTkuNyAxNS42IDIwMC45IDE0LjQgMjAxLjhDMTMuMiAyMDIuNyAxMS43IDIwMy4yIDEwLjEgMjAzLjJDOC4xIDIwMy4yIDYuNiAyMDIuNyA1LjYgMjAxLjZDNC42IDIwMC41IDQuMSAxOTkuMSA0IDE5Ny40SDYuOEM2LjggMTk4LjQgNyAxOTkuMyA3LjYgMTk5LjlDOC4yIDIwMC42IDkgMjAwLjkgMTAuMSAyMDAuOUMxMSAyMDAuOSAxMS44IDIwMC42IDEyLjUgMjAwLjFDMTMuMSAxOTkuNyAxMy40IDE5OC45IDEzLjQgMTk4WiIgZmlsbD0iYmxhY2siLz4KPHBhdGggZD0iTTE1LjkgMjMxLjVWMjMzLjlINEM0IDIzMi41IDQuNCAyMzEuMyA1LjEgMjMwLjNDNS44IDIyOS4zIDYuNyAyMjguNCA3LjkgMjI3LjZDOSAyMjYuOCAxMC4yIDIyNiAxMS40IDIyNS4yQzEyLjYgMjI0LjQgMTMuMiAyMjMuNCAxMy4yIDIyMi4xQzEzLjIgMjIxLjUgMTMgMjIwLjkgMTIuNyAyMjAuMkMxMi40IDIxOS41IDExLjUgMjE5LjIgMTAuMyAyMTkuMkM5LjIgMjE5LjIgOC40IDIxOS42IDggMjIwLjRDNy42IDIyMS4yIDcuMyAyMjIuMiA3LjMgMjIzLjRINC40QzQuNCAyMjEuNiA0LjkgMjIwIDUuOSAyMTguOEM2LjkgMjE3LjYgOC40IDIxNyAxMC40IDIxN0MxMi41IDIxNyAxNCAyMTcuNiAxNC44IDIxOC43QzE1LjYgMjE5LjkgMTYgMjIxIDE2IDIyMi4xQzE2IDIyMy40IDE1LjYgMjI0LjUgMTQuOCAyMjUuNEMxNCAyMjYuMyAxMy4xIDIyNyAxMi4xIDIyNy43QzExLjEgMjI4LjQgMTAuMSAyMjkgOS4yIDIyOS42QzguMyAyMzAuMiA3LjcgMjMwLjggNy41IDIzMS42SDE1LjlWMjMxLjVaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNNiAyNTEuOFYyNDkuN0M3LjIgMjQ5LjcgOC4zIDI0OS41IDkuMyAyNDkuMUMxMC4zIDI0OC43IDEwLjkgMjQ4IDExLjEgMjQ3SDEzLjRWMjYzLjZIMTAuM1YyNTEuOUg2VjI1MS44WiIgZmlsbD0iYmxhY2siLz4KPHBhdGggZD0iTTQgMjg0LjZDNCAyODIuNyA0LjQgMjgwLjggNS4xIDI3OC45QzUuOSAyNzcgNy41IDI3NiAxMCAyNzZDMTIuNSAyNzYgMTQuMSAyNzcgMTQuOSAyNzguOUMxNS43IDI4MC44IDE2IDI4Mi44IDE2IDI4NC42QzE2IDI4Ni41IDE1LjYgMjg4LjQgMTQuOSAyOTAuM0MxNC4xIDI5Mi4yIDEyLjUgMjkzLjIgMTAgMjkzLjJDNy41IDI5My4yIDUuOSAyOTIuMiA1LjEgMjkwLjNDNC4zIDI4OC40IDQgMjg2LjUgNCAyODQuNlpNNi44IDI4NC42QzYuOCAyODYuMyA3IDI4Ny44IDcuNSAyODkuMUM3LjkgMjkwLjQgOC44IDI5MSAxMCAyOTFDMTEuMiAyOTEgMTIuMSAyOTAuNCAxMi41IDI4OS4xQzEyLjkgMjg3LjggMTMuMiAyODYuMyAxMy4yIDI4NC42QzEzLjIgMjgyLjkgMTMgMjgxLjQgMTIuNSAyODAuMUMxMi4xIDI3OC44IDExLjIgMjc4LjIgMTAgMjc4LjJDOC43IDI3OC4yIDcuOSAyNzguOCA3LjUgMjgwLjFDNyAyODEuNCA2LjggMjgyLjkgNi44IDI4NC42WiIgZmlsbD0iYmxhY2siLz4KPC9zdmc+Cg==)",
  },
}));

interface ScrollAnimatedNumberProp {
  prefix?: string;
  text: number | string;
  suffix?: string;
  fontSize?: number;
}

const ScrollAnimatedNumber = ({ prefix = "", text = 0, suffix = "", fontSize = 20 }: ScrollAnimatedNumberProp) => {
  const classes = useStyles();
  const scrollAnimatedTargetNumber = [...text.toString()];
  const calFontSize = fontSize / 20;
  return (
    <div className={classes.ScrollAnimatedNumberLayout}>
      {prefix.length > 0 ? <span style={{ fontSize: fontSize }}>{prefix}</span> : undefined}
      {scrollAnimatedTargetNumber.map((letter, index) => {
        if (Number.isNaN(Number(letter))) return <span style={{ fontSize: fontSize }}>{letter}</span>;
        return (
          <span
            className={classes.numberSpan}
            style={{
              fontSize: fontSize,
              backgroundPositionY: `${(Number(letter) + 1) * calFontSize * 30}px`,
              backgroundSize: `${calFontSize * 30}px ${calFontSize * 300}px`,
            }}
          >
            {letter}
          </span>
        );
      })}
      {suffix.length > 0 ? <span style={{ fontSize: fontSize }}>{suffix}</span> : undefined}
    </div>
  );
};

export default ScrollAnimatedNumber;

가로세로 30px X 300px 짜리 0~9가 있는 숫자 이미지를 만들고 backgroundPosition에 따라 위치를 조정하고 transition으로 애니메이션을 넣었습니다.
그리고 이미지는 import를 하지 않고 base64로 변환해서 넣었습니다.(한 파일에서 관리하기 편하라고)

사용법

<ScrollAnimatedNumber text={number} fontSize={40}/>
<ScrollAnimatedNumber text={"1235.12"} fontSize={40} prefix={"$"}/>

최종 완성본


실제로는 슬라이딩?이 더 부드러운데 gif로 변환하다보니 약간 끊기는 느낌은 있습니다.
아무튼 로빈후드 주가창처럼 움직이는 위젯을 만들어봤습니다.

한계

  1. SVG를 Base64로 가져다쓰면서 해당 숫자의 색상을 컨트롤 할 수 있는 기능을 버렸습니다. 향후에 오픈소스로 내보낼때는 해당 SVG의 색상을 바꿀수 있는 기능을 제공해야할 듯합니다.

  2. SVG를 만들때 정렬을 제대로 하지 않았는지 숫자들이 딱 정렬되지 않습니다. 이것도 수정필요..

  3. 코드를 보면 실제 숫자가 transparent 되어있습니다. 이미지를 사용하기때문에 숫자 drag를 할 수 없어서 대안으로 구현했습니다.

의의

  1. 그래도 이전에 Scroll로 만든 것에 비해 의미있는 위젯이 만들어졌습니다.

  2. CSS Image Sprite를 사용해봤습니다. ㅎㅎ

마치면서

시간이 날때 CSS 완벽 가이드 책을 읽고 있습니다. 근데 내용이 너무 방대해서 언제쯤 완독할지 모르겠네요. 이 책을 완독하고 익히면 더 좋은 방법이 떠오르지 않을까 기대하고 있습니다.

빠르게 발전하는 브라우저와 자바스크립트, CSS가 있고 또 이걸 기반으로 React, Vue, Scss, tailandCss등이 나오고 있는데 이렇게 하나씩 써보면서 익혀나가는 꾸준함이 필요한 것 같습니다.
제가 이 위젯을 만드는 시간에도 어마무시한 개발자형님들이 더 무시무시한 기능을 만들고 있을 거기 때문에.. 오늘도 하나씩 배워갑니다.

참고자료

profile
frondend dev

0개의 댓글