Post

애니메이션 - requestAnimationFrame

📌시작하며

회사에서 프로젝트를 진행하면서, 애니메이션을 구현할 일이 생겼다. 웹상에서 애니메이션을 구현하는 방식에는 CSS를 이용하거나 라이브러리를 이용하는 방식도 있지만, JavaScript의 requetAnimationFrame을 사용해 애니메이션을 구현하는 방식도 있다.

개인적으로 requestAnimationFrame은 가볍게 연습용으로만 사용해보고, 개인 프로젝트에 적극적으로 적용해본적은 없어서, 최초 작성된 코드에 있는 애니메이션의 흐름과 작성자의 의도를 만족할 만큼 이해하기 어려웠고, 이번 기회에 부드러운 애니메이션을 구현하는 방식에 대해 정리해보고자 한다.

✅requestAnimationFrame

requestAnimationFrame()은 브라우저가 애니메이션을 효율적으로 처리할 수 있도록 돕는 API다. 브라우저의 화면 업데이트 주기에 맞춰 애니메이션을 실행하여, 부드러운 애니메이션 효과를 얻을 수 있다.

이렇게만 설명하면 정확히 무엇을 의미하는지 이해하기 어려운데, 프레임에 초점을 맞춰 생각해보자.

프레임은 정지된 한 화면을 의미하는데, 이러한 프레임(장면)이 여러장 모여 하나의 애니메이션이 완성된다. 1초에 60개의 그림이 모여 만들어진 애니메이션과 24개의 그림이 모여 만들어진 애니메이션 중, 어떤 애니메이션이 더 부드럽게 느껴질까? 당연히, <60개의 그림이 모여 만들어진 애니메이션일 것이다.

✅예제 1

움직이는 박스 애니메이션을 만들어보자. 박스 div가 1초간 0~200px을 움직이는 애니메이션이다. 박스 애니메이션 시작 이란 버튼을 눌러 박스가 움직인다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
"use client"
import React, { useRef } from "react"

const BoxAnimation = () => {
  const boxRef = useRef<HTMLDivElement | null>(null)

  const boxAnimation = () => {
    let start: null | number = null
    const duration = 1000 // 1초 동안 애니메이션

    const animate = (timestamp: number) => {
      if (!start) start = timestamp
      const elapsed = timestamp - (start as number) // 경과 시간

      const progress = Math.min(elapsed / duration, 1) // 0에서 1까지
      if (boxRef.current) {
        boxRef.current.style.transform = `translateX(${progress * 200}px)`
      }

      if (progress < 1) {
        requestAnimationFrame(animate)
      }
    }

    requestAnimationFrame(animate)
  }

  return (
    <div>
      <button
        type="button"
        className="bg-green-200 p-2 mb-10"
        onClick={boxAnimation}
      >
        박스 애니메이션 시작
      </button>
      <div
        ref={boxRef}
        className="w-20 h-20 bg-green-300"
        style=
        id="box"
      ></div>
    </div>
  )
}

export default BoxAnimation

✅애니메이션 작성 법

위의 예제가 다소 복잡해보이지만 애니메이션을 작성하는 전체적인 틀은 다음과 같다.

  1. useRef로 애니메이션이 적용될 요소를 선택한다.

  2. 원하는 애니메이션의 duration(지속 시간)을 설정한다.

  3. timestamp를 인수로 받는 애니메이션이 실행 함수를 작성한다.

  • requestAnimationFrame 함수가 호출되면 브라우저가 렌더링을 시작할 때마다 timestamp 를 자동으로 넘겨준다.
  • 이때 timestamp는 애니메이션 시작부터 현재 시점까지의 시간을 밀리초로 나타낸 값이다.
  1. start 시간이 없을 경우, timestamp로 설정하고, 시간이 경과된 경우 timestamp - start로 elapsed(경과 시간)을 계산한다.
  • start는 애니메이션이 시작된 시점으로, requestAnimationFrame이 처음 호출된 시간을 저장한다. 이를 통해 requestAnimationFrame이 호출되어 timestamp값이 전달되었을 때 처음 호출된 시간을 저장해둔 start값을 기준으로 얼마나 시간이 흘렀는지를 계산할 수 있다.
  1. progress를 통해 0에서 1까지의 진행 상태를 설정한다.
  • progress는 전체 애니메이션 진행 상황을 비율로 나타내며, elapsed(경과시간) / duration(지정한 애니메이션의 총 시간) 으로 계산된다. 진행 상황은 0~1 값을 가지고 있게 된다.
  1. boxRef.current가 존재하는 경우, 어떤 애니메이션이 진행되어야 하는지 CSS 속성을 조절한다.

✅예제2

좌우로 이동하는 애니메이션을 만들어보자. 해당 예제의 경우 direction 변수를 추가하여, CSS로 조정되는 애니메이션 방향을 바꿔주었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
"use client"
import React, { useRef } from "react"

const CircleAnimation = () => {
  const circleRef = useRef<HTMLDivElement | null>(null)

  const circleAnimation = () => {
    let start: number | null = null
    const duration = 20000 // 애니메이션 지속 시간 20초
    let direction = 1 // 이동 방향: 1이면 오른쪽, -1이면 왼쪽
    let progress = 0 // 0에서 1까지의 진행 상태

    const animate = (timestamp: number) => {
      if (!start) start = timestamp

      const elapsed = timestamp - (start as number)
      const totalProgress = Math.min(elapsed / duration, 1) // 0에서 1까지

      // progress가 증가하거나 감소하도록 설정
      progress += direction * totalProgress // 현재 방향으로 진행 상태 변경

      // 진행 상태가 1에 도달하면 방향 바꾸기
      if (progress >= 1) {
        direction = -1 // 왼쪽으로 이동
        progress = 1 // 1로 고정
        start = timestamp // 새로운 시작 시간 초기화
      } else if (progress <= 0) {
        direction = 1 // 오른쪽으로 이동
        progress = 0 // 0으로 고정
        start = timestamp // 새로운 시작 시간 초기화
      }

      // 원을 이동
      if (circleRef.current) {
        circleRef.current.style.transform = `translateX(${progress * 200}px)`
      }

      requestAnimationFrame(animate)
    }

    requestAnimationFrame(animate)
  }

  return (
    <>
      <button
        type="button"
        className="bg-blue-200 p-2 mb-10"
        onClick={circleAnimation}
      >
         애니메이션 시작
      </button>
      <div
        ref={circleRef}
        className="w-20 h-20 bg-blue-300"
        style=
        id="circle"
      ></div>
    </>
  )
}

export default CircleAnimation

🗂️참고 사이트

This post is licensed under CC BY 4.0 by the author.