과제 테스트 중 숫자가 올라가는 애니메이션을 구현해야할 상황이 생겼다.
과제 올린 Repo를 메일로 공유해달라 했으니, 과제 내용의 일부를 개인 블로그에 정리해도 되지 않을까 싶다.
숫자가 올라가는 애니메이션
- 각 숫자는 0부터 시작합니다.
- 세 숫자 모두 2초 동안 증가하고, 동시에 끝나야 합니다.
- 증가 속도가 느려지는 효과를 구현해야 합니다.
- React와 DOM API만을 이용해 구현해야 합니다.
굉장히 간단해 보였으나 생각만큼 쉽지는 않았다.
기본적인 뷰와 나머지 요구사항을 구현하였고, 숫자가 올라가는 애니메이션만 구현하면 완료하는 상황이었다.
구글링해본 결과 jQuery를 사용한 방법, Vanilla Javascript를 이용한 방법, 라이브러리를 사용하는 방법 외에는 정확하게 나온 자료를 찾을 수 없었다.
그나마 찾은 방안이 아래의 Scroll animation과 결합한 방식이었다.
이 방식을 토대로 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
}
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
}
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
}