[React.js] 우당탕탕 카운트업 기능 개발하기

TK·2024년 5월 13일
0
post-custom-banner

기능구현 목표

  • 다음과 같이 스크롤을 내리면 나타나는 카운트업 기능을 만들고자 한다.

  • 부분적으로 보기

📝첫번째 구현

  • 여러 레퍼런스들을 참고하여 아래와 같이 코드를 작성하였다.

    • useCountUp.jsx

      import { useEffect, useState } from "react";
      
      /* eslint-disable no-unused-vars */
      const useCountUp = (end, start = 0, duration = 2000) => {
        const [count, setCount] = useState(start);
        useEffect(() => {
          let currentNum = start;
          const delay = duration / end;
          const countUp = setInterval(() => {
            currentNum++;
            setCount(currentNum);
            if (currentNum === end) {
              clearInterval(countUp);
            }
          }, delay);
        }, [end, start, duration]);
        return count.toFixed(0);
      };
      export default useCountUp;
    • Counting.jsx

      
      import styled from "styled-components";
      import useCountUp from "../Hooks/useCountUp";
      
      const CountingWrapper = styled.div`
        display: flex;
        flex-direction: column;
        gap: 2rem;
        align-items: center;
        width: 100%;
      `;
      const Count = styled.div`
        font-size: ${(props) => props.theme.fontSize.title_2};
        font-weight: bold;
        color: ${(props) => props.theme.color.darkGreen};
      `;
      const Name = styled.div`
        font-size: ${(props) => props.theme.fontSize.content_14};
        font-weight: bold;
      
        color: ${(props) => props.theme.color.orange};
      `;
      
      export default function Counting({ ...props }) {
        console.log(props.number);
        console.log(Number(props.number));
      
        return (
          <CountingWrapper>
            <Count>{useCountUp(Number(props.number), 0, 2000)}</Count>
            <Name>{props.name}</Name>
          </CountingWrapper>
        );
      }

      참고로 Counting에서 전달받는 props.number은 문자이다. 그래서 Number함수로 처리했을때 처음엔 다음과 같은 오류가 발생했었다.
      uncaught typeerror: number is not a function
      검색해보니 내가 스타일을 위해 Number를 선언해 사용하고 있어서 그런 것이었다.(예약어) 그래서 중복되는 이름이 없도록 number라고 사용한 것을 모두 제거하고 Count로 스타일 이름을 바꾸니 오류가 사라지게 되었다.

  • 그러자 다음과 같이 작동하였다.


💻첫번째 구현 작동화면

참고: 맨처음 숫자인 800k+는 우선 800을 먼저 넣어보았다.

  • 문제점

    구분원본내가 만든 것
    렌더링 시기화면에서는 보이지 않지만 원본은 스크롤해서 해당 구역이 비춰질때부터 카운팅 시작함.처음 전체 화면 렌더링 되자마자 시작됨.
    카운팅 방식원본은 3초 정도의 시간안에 동시에 끝남.내가 만든건 숫자가 높을수록 늦게까지 혼자 카운팅 되고있음.
    화면 효과자연스럽게 숫자가 나타나는 페이드인? 이 있음
  • 카운팅 방식을 먼저 수정해보자.


📝두번째 구현

모두 같은 시간에 종료 되도록 먼저 만들자.

Q. 처음 작성한 코드에서 const delay = duration / end;에 의해 이론적으로 end 숫자가 커질수록 delay 속도가 빨라지게 하여 모두 동시에 끝나는게 맞지만 왜 그렇게 동작하지 않을까?

A. mdn공식문서를 확인해보니 최소 4ms까지만 가능 ⇒ 그럼 최소 4ms로 맞추고 나머지는 숫자를 띄엄띄엄 세는 식으로 가야겠따.

지연 제한

간격(interval)은 중첩될 수 있습니다. 즉, setInterval()에 대한 콜백이 setInterval()을 호출하여 첫 번째 간격이 계속 진행 중일지라도 다른 간격의 실행을 시작할 수 있습니다. 이것이 성능에 미칠 수 있는 잠재적인 영향을 완화하기 위해 간격이 5개 수준 이상으로 중첩되면 브라우저는 자동으로 간격에 대해 4 ms 최소 값을 적용합니다. 중첩 호출이 심화된 setInterval()의 호출에서 4 ms 미만의 값을 지정하면 4 ms로 고정됩니다.

  • 각 숫자에 대해서 다음 표와 같이 실험(?)을 진행했다.

    목표숫자800k+8912250
    지속시간(가정)2000ms2000ms2000ms2000ms
    시작숫자0000
    예상 지연시간0.0025ms22.5ms166ms8ms
    4ms이상?XOOO
    예상 순위4111
    실제 순위4113

⇒ 250을 200으로 고쳤을때도 진짜 아주 살짝 늦게 끝나고(10ms) 150으로 고쳐보니 동일하게 끝나는 것을 확인했다. (13ms) 그래서 안전하게 20ms 로 맞추기로 한다. (100번 나누기 이하일 경우)

  • 작성코드

    // useCountUp.jsx
    import { useEffect, useState } from "react";
    
    /* eslint-disable no-unused-vars */
    const useCountUp = (end, start = 0, duration = 2000) => {
      const [count, setCount] = useState(start);
      useEffect(() => {
        let currentNum = start;
        if (Math.abs(end) > 99) {
          const delay = 20;
          const countUp = setInterval(() => {
            currentNum = currentNum + end / (duration / delay);
            setCount(currentNum);
            if (currentNum === end) {
              clearInterval(countUp);
            }
          }, delay);
        } else {
          const delay = Math.abs(Math.floor(duration / end));
          const countUp = setInterval(() => {
            currentNum++;
            setCount(currentNum);
            if (currentNum === end) {
              clearInterval(countUp);
            }
          }, delay);
        }
      }, [end, start, duration]);
      return count.toFixed(0);
    };
    export default useCountUp;

💻두번째 구현 작동화면

맨처음 숫자를 8000으로 바꾸었을 때 화면이다.

  • 문제점
    • 8000에서는1의자리 숫자가 전혀 변하지 않는다.
    • 또한 8000이라는 숫자까지는 도달하지만 실제로는 원본과 같이 숫자와 문자가 조합된 800k+라는 숫자를 써야 한다.

📝세번째 구현

800k+ 가 800000(팔십만) 으로 읽을 수 있도록 변환시켜주자.

  • 레퍼런스를 참고하여 다음 단위변환 커스텀 훅을 작성하였다.
//useConvert.jsx
const useConvert = (num, digits) => {
  const si = [
    { value: 1, symbol: "" },
    { value: 1e3, symbol: "k" },
    { value: 1e6, symbol: "M" },
    { value: 1e9, symbol: "G" },
    { value: 1e12, symbol: "T" },
    { value: 1e15, symbol: "P" },
    { value: 1e18, symbol: "E" },
  ];
  const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
  let i;
  for (i = si.length - 1; i > 0; i--) {
    if (num >= si[i].value) {
      break;
    }
  }
  return (num / si[i].value).toFixed(digits).replace(rx, "$1") + si[i].symbol;
};

export default useConvert;
  • 그리고 이 훅을 useCounting.jsx에 다음 코드와 같이 적용시켰는데 에러가 발생하였다.
//useCounting.jsx
//생략
  if (count>1000){
    const convertedNum = useConvert(count,0) // 이 부분 에러
  }
  return count.toFixed(0);
};
export default useCountUp;

React Hook "useConvert" is called conditionally. React Hooks must be called in the exact same order in every component render.

⇒ 리액트 훅은 조건식 안에 사용될 수 없다고 한다. 간과한 리액트 훅의 규칙은 다음과 같다.

최상위(at the Top Level)에서만 Hook을 호출해야 합니다

반복문, 조건문 혹은 중첩된 함수 내에서 Hook을 호출하지 마세요. 대신 early return이 실행되기 전에 항상 React 함수의 최상위(at the top level)에서 Hook을 호출해야 합니다. 이 규칙을 따르면 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되는 것이 보장됩니다. 이러한 점은 React가 useState 와 useEffect 가 여러 번 호출되는 중에도 Hook의 상태를 올바르게 유지할 수 있도록 해줍니다. 이 점에 대해서 궁금하다면 아래에서 자세히 설명해 드리겠습니다.

오직 React 함수 내에서 Hook을 호출해야 합니다

Hook을 일반적인 JavaScript 함수에서 호출하지 마세요. 대신 아래와 같이 호출할 수 있습니다.

  • ✅ React 함수 컴포넌트에서 Hook을 호출하세요.
  • ✅ Custom Hook에서 Hook을 호출하세요. (다음 페이지에서 이 부분을 살펴볼 예정입니다)
    이 규칙을 지키면 컴포넌트의 모든 상태 관련 로직을 소스코드에서 명확하게 보이도록 할 수 있습니다.
  • 수정코드 ⇒ 리액트 훅을 조건문 밖으로 빼냈다.
//useCounting.jsx
//생략
  const convertedNum = useConvert(count, 0); // 조건문 밖으로 뺌
  
  if (count > 1000) {
    return convertedNum;
  } else {
    return count.toFixed(0);
  }
};
export default useCountUp

💻세번째 구현 작동화면

  • 문제점
    에러 발생은 사라졌으나 숫자 카운팅이 다음과 같이 일어났다. → 1000단위 위부터는 xk로 세기 시작한다.

📝네번째 구현

최종 숫자만 단위변환에 적용되도록 한다.

import { useEffect, useState } from "react";
import useConvert from "./useConvert";

/* eslint-disable no-unused-vars */
const useCountUp = (end, start = 0, duration = 5000) => {
  const [count, setCount] = useState(start);

  useEffect(() => {
		// 아래는 1000 이상인 경우에 대해 새로 작성한 코드
    let currentNum = start;
    if (Math.abs(end) > 1000) {
      const delay = 20;
      const countUp = setInterval(() => {
        currentNum = currentNum + 950 / (duration / delay);
        setCount(currentNum);
        if (currentNum === 950) {
          clearInterval(countUp);
        }
      }, delay);
   
    } else if (Math.abs(end) > 99) {
      // (중략)

  const convertedNum = useConvert(end, 0);
  if (count > 949) {
    return convertedNum;
  }

  return count.toFixed(0);
};
export default useCountUp;

💻네번째 구현 작동화면(최종)

참고: 페이드인 스타일 적용함


📝구현 완료된 기능

  • 숫자 카운트 업
  • 숫자 크기와 상관없이 1의 자리 숫자 변화가 눈에 보이도록 함
  • 숫자 크기와 상관없이 같은 시각에 종료되도록 함
  • 1000단위 이상의 숫자를 문자로 변환하여 표기
  • 페이드 인 (fade-in) 애니메이션

📝번외 : 추가 구현해야할 기능

  • 스크롤이벤트를 이용하여 해당 컴포넌트 도달시 카운트업이 수행되도록 한다. (useScrollCount 구현)

📝마치며

  • 기능을 구현하면서 setInterval을 사용하여 카운트 업 기능을 구현할 때는 지연간격의 제한이 있기 때문에 숫자가 얼만큼 크냐에 따라 조건별로 나눠 적용시켜줘야 한다는 것을 알게되었다.
  • 기능을 구현하기 전엔 단순히 숫자가 카운트업 되기만 하면 되어 굉장히 쉬울 것이라고 예상했지만 의외로 한 단계의 과정을 거치면 또 손봐줘야 할 다음 단계가 있었고, 그것을 풀어나가는 과정이 흥미로웠다.
  • 스크롤이벤트를 기능을 추가할때 추가 포스팅을 할 예정이다.

참고 레퍼런스 출처

profile
쉬운게 좋은 FE개발자😺
post-custom-banner

0개의 댓글