requestAnimationFrame으로 애니메이션 만들기
포스트
취소

requestAnimationFrame으로 애니메이션 만들기

💻 애니메이션의 원리

사람이 움직이는 물체를 볼때, 초당 평균 150장의 스냅샷(snapshot) 을 찍은 후 해당 사진들을 연결시켜 움직인다고 인식하게 됩니다. 애니메이션에서는 이 스냅샷들은 프레임(frame)이라고 정의합니다.

왼쪽에서 오른쪽으로 위치를 이동하는 물체가 있다고 가정해 보겠습니다.
앞에서 언급했듯이, 물체를 이동시키기 위해서는 매 프레임별로 조금씩 위치를 움직여 주어야 합니다. 대부분의 디스플레이는 초당 60개의 프레임(60fps)을 보여주며, 한 프레임을 보여주는데 걸리는 시간은 대락 16.6ms(1s/60fps) 정도가 됩니다.
따라서, 60fps 디스플레이에서는 16.6ms 간격으로 물체를 움직여 주어여 한다는 결론이 나옵니다.

1
2
3
setInterval(() => {
  move();
}, 16.6);

위 코드는, setInterval 을 이용하여, 16.6ms 주기로 move 함수를 실행하는 코드입니다.
해당 코드의 문제점은 자원을 많이 차지하는 작업을 수행할 때, 설정한 시간이 보장되지 않는다는 점입니다. requestAnimationFrame 을 사용하면 해당 문제를 해결할 수 있습니다.

💻 requestAnimationFrame

requestAnimationFrame 함수는 다음 리페인트 직전에 넘겨 받은 콜백함수를 실행시켜주는 함수입니다. 따라서 연속적으로 실행을 시키기 위해서는 재귀호출을 해야합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const circle = document.querySelector("#circle");

const left = 0;

function move() {
  left += 0.5;

  circle.style.transform = `translateX(${left}px)`;

  if (left !== 300) {
    requestAnimationFrame(move);
  }
}

move();

위 예시는, 왼쪽에서 오른쪽으로 동그라미를 프레임별 0.5px 씩 300px 만큼 움직이게 하는 코드입니다. move 함수 내부에서 requestAnimationFrame 을 호출하고 있음을 알 수 있습니다.

request-animation-frame-1

💻 requestAnimationFrame의 문제점

requestAnimationFrame 의 문제점은 같은 애니메이션이라도 프레임률이 다른 디스플레이에서 다르게 보일 수 있다는 것입니다.
60fps의 프레임률을 가지는 디스플레이의 경우 requestAnimationFrame 이 16.6ms 주기로 실행되지만, 120fps 디스플레이의 경우 대략 8.3ms 만큼의 주기로 실행됩니다.

request-animation-frame-1 request-animation-frame-2

위 자료는 같은 애니메이션을 각각 120fps, 30fps 디스플레이에서 실행시킨 결과입니다. 120fps 디스플레이의 경우 30fps 디스플레이보다 실행 주기가 짧아 300px에 더 빨리 도달했음을 알 수 있습니다.

👨‍💻 해결책

requestAnimationFrame 의 콜백함수는 TimeStamp 를 인자로 받습니다. 이 인자는 콜백이 실행되고 경과된 시간을 의미합니다. 이를 활용하면 디스플레이간 차이를 없앨 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let left = 0;
let start = null;

function move(timeStamp) {
  if (!start) {
    // 최초 실행 시점
    start = timeStamp;

    // 최초 실행 시점으로 부터의 경과시간
    const elapsedTime = timeStamp - start;

    // 0.05px/ms 로 움직이도록 위치 지정
    left = Math.min(elapsedTime * 0.05, 300);

    circle.style.transform = `translateX(${left})`;

    if (left < 300) {
      requestAnimationFrame(move);
    }
  }
}

requestAnimationFrame(move);

처음으로 실행된 시점으로 부터의 시간을, 1ms당 움직이고자 하는 값에 곱해줌으로써 프레임율과 무관하게 항상 같은 애니메이션을 볼 수 있습니다.

request-animation-frame-3
request-animation-frame-3

💻 원운동하는 애니메이션 만들기

requestAnimationFrame 을 이용하여 원운동하는 물체를 만들어보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let left = 0;
let above = 0;
let start = null;

function move(timeStamp) {
  if (!start) {
    // 최초 실행 시점
    start = timeStamp;

    // 최초 실행 시점으로 부터의 경과시간
    const elapsedTime = timeStamp - start;

    left = 150 * Math.sin(0.0005 * elapsed) + 150;
    above = 150 * Math.cos(0.0005 * elapsed) + 150;

    circle.style.transform = `translate(${left}px, ${above}px)`;

    requestAnimationFrame(move);
  }
}

requestAnimationFrame(move);

원운동을 구현하기 위해서 한축에는 sin 함수의 값에 따른 좌표를, 다른 축에는 cos 함수의 값에 따른 좌표를 지정해 주어야합니다.

wikipedia

이제 삼각함수 부분을 살펴보겠습니다.

asin(bx + c) + d

위와 같은 삼각함수에서 주기, 최댓값, 최솟값은 각각 다음과 같습니다.

  • 주기: 2𝛑 / ❘b❘
  • 최댓값: ❘a❘ + d
  • 최솟값: -❘a❘ + d

이 정보를 코드의 150 * Math.sin(0.0005 * elapsed) + 150 에 대입해보면, 주기 2𝛑 / 0.0005, 최댓값 300, 최솟값 0 이 나옵니다.
계수 b 자리에 0.0005 를 설정해 주기를 크게 만든이유는, 진행되는 애니메이션의 속도를 줄이기 위해서입니다. 또한 a, b 자리에 150 을 설정해준 이유는 0px ~ 300px 사이를 움직이는 원을 만들기 위해서 입니다.

circle

📔 참고자료

Understanding animation, duration and easing using requestAnimationFrame circle-cos-sin

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.