React + TS: useCountUp hook 구현하기

Juhwan Lee·2022년 6월 24일
14

과제 테스트 중 숫자가 올라가는 애니메이션을 구현해야할 상황이 생겼다.
과제 올린 Repo를 메일로 공유해달라 했으니, 과제 내용의 일부를 개인 블로그에 정리해도 되지 않을까 싶다.

숫자가 올라가는 애니메이션

  • 각 숫자는 0부터 시작합니다.
  • 세 숫자 모두 2초 동안 증가하고, 동시에 끝나야 합니다.
  • 증가 속도가 느려지는 효과를 구현해야 합니다.
  • React와 DOM API만을 이용해 구현해야 합니다.

굉장히 간단해 보였으나 생각만큼 쉽지는 않았다.

기본적인 뷰와 나머지 요구사항을 구현하였고, 숫자가 올라가는 애니메이션만 구현하면 완료하는 상황이었다.

✅ 초기 useCountUp hook 구성하기

구글링해본 결과 jQuery를 사용한 방법, Vanilla Javascript를 이용한 방법, 라이브러리를 사용하는 방법 외에는 정확하게 나온 자료를 찾을 수 없었다.

그나마 찾은 방안이 아래의 Scroll animation과 결합한 방식이었다.

React Custom Hooks로 scroll animation 만들기 CountUp편

이 방식을 토대로 custom hook을 구현해보았다.

구현한 로직

import { useEffect, useState } from 'react'

export default function useCountUp(end: number, start = 0, duration = 2000) {
  const [count, setCount] = useState(start)
  const stepTime = Math.abs(Math.floor(duration / (end - start)))

  useEffect(() => {
    let currentNumber = start
    const counter = setInterval(() => {
      currentNumber += 1
      setCount(currentNumber)

      if (currentNumber === end) {
        clearInterval(counter)
      }
    }, stepTime)
  }, [end, start, stepTime])

  return count
}

✨ 렌더링된 페이지

문제점

  • 숫자가 올라가긴 하지만 700의 경우에 끝나는 시간이 다르다.
    -> stepTime을 60프레임으로 고정하기로 결정

✅ 동시에 끝나게 hook 수정

구현한 로직

import { useEffect, useState } from 'react'

export default function useCountUp(end: number, start = 0, duration = 2000) {
  const [count, setCount] = useState(start)
  const frameRate = 1000 / 60
  const totalFrame = Math.round(duration / frameRate)

  useEffect(() => {
    let currentNumber = start
    const counter = setInterval(() => {
      const progress = ++currentNumber / totalFrame
      setCount(Math.round(end * progress))

      if (progress === 1) {
        clearInterval(counter)
      }
    }, frameRate)
  }, [end, frameRate, start, totalFrame])

  return count
}
  • frameRate: 사람 눈에 자연스러운 60프레임으로 구성하였다.
  • totalFrame: 시간/프레임 으로 총 프레임 갯수를 선언 하였다.
  • progress: 진행 상황

✨ 렌더링된 페이지

보완 사항

  • ease-out animation을 적용해야한다.

✅ EaseOut 효과 적용하기

ease-out animation을 구현하기 위해 하단 링크를 참조했다.

Fabric.js easeOutExpo() Method
EASING FUNCTIONS FOR JAVASCRIPT

초기에는 easeOutSine 함수로 구현하였으나,

const easeOutSine = (t: number): number => {
  return t === 1 ? 1 : Math.sin(t * (Math.PI / 2))
}

조금 더 극적인 효과를 위해 easeOutExpo 함수로 변경하였다.

const easeOutExpo = (t: number): number => {
  return t === 1 ? 1 : 1 - Math.pow(2, -10 * t)
}

🚀 최종 코드

// useCountUp.ts
import { useEffect, useState } from 'react'

function easeOutExpo(t: number): number {
  return t === 1 ? 1 : 1 - Math.pow(2, -10 * t)
}

export default function useCountNum(end: number, start = 0, duration = 2000) {
  const [count, setCount] = useState(start)
  const frameRate = 1000 / 60
  const totalFrame = Math.round(duration / frameRate)

  useEffect(() => {
    let currentNumber = start
    const counter = setInterval(() => {
      const progress = easeOutExpo(++currentNumber / totalFrame)
      setCount(Math.round(end * progress))

      if (progress === 1) {
        clearInterval(counter)
      }
    }, frameRate)
  }, [end, frameRate, start, totalFrame])

  return count
}

profile
keep going

0개의 댓글