requestAnimationFrame api 사용하기

김상두·2023년 3월 10일
0

트러블슈팅

목록 보기
8/12

들어가며

브라우저에서 애니메이션을 다루거나, 렌더링관련 이슈를 다루게되면 한번씩 듣게 되는 api가 바로 requestAnimationFrame api(이하 raf)입니다. 개인 프로젝트를 진행할때 프레임에 맞추어서 요소를 렌더링할때 사용해보았는데, 정확하게 동작하는 원리가 궁금해서 공부한 내용을 소개합니다.

애니메이션과 프레임

애니메이션의 원리는 연속적인 동작이 그려진 그림을 빠른속도로 전환하여 보여주는것입니다. 이렇게 정적인 그림이 단지 빠르게 보여주는것 만으로 동적인 영상이 되는것은 인간의 뇌는 사물을 인지하는 시간보다 짧은 시간에 화면을 바꾸어주면 변경된 사실을 인지하지 못하고 이전사진과 현재사진을 합쳐서 마치 이동한것처럼 느끼기 때문입니다. 이를 잔상효과라고 하는데, 현대의 애니메이션은 이러한 잔상효과를 활용해 만들어졌습니다.

여기서 같이 등장하는개념은 바로 프레임입니다. 프레임이란, 1초당 얼마나 많은 그림을 보여주느냐 입니다. 당연한이야기이지만, 많은 그림을 보여줄수록 애니메이션이 매끄럽게 느껴집니다. 그렇지만 60hz 즉 1초에 60장 정도의 충분히 매끄럽게 느껴지고, 이는 대부분의 모니터 디스플레이가 60hz로 이루어져있는 이유이기도 합니다.

raf 사용해보기

이름에 animation이 들어간것처럼 raf는 애니메이션을 만들때 주로 사용합니다. raf는 모니터의 프레임에 맞추어서 해당 렌더링이 진행되기 직전 담겨있는 콜백함수를 실행해주기 때문입니다. 예를들어 자신의 모니터가 60hz라면 16.6초에 한번 콜백함수가 실행됩니다. 요소가 이동하는 애니메이션을 만드는 간단한 예제를 이용해 한번 사용해보겠습니다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <style>
      #root {
        position: relative;
        width: 1000px;
        height: 1000px;
        border: 1px solid black;
        margin: 100px auto 0 auto;
      }
      #move {
        width: 10px;
        height: 10px;
        left: 0;
        top: 0;
        position: absolute;
        background-color: black;
      }
    </style>
    <title>rafTest</title>
  </head>
  <body>
    <div id="root">
      <div id="move"></div>
    </div>
    <script>
      const element = document.getElementById("move");
	  let myReq ;
      const move = () => {
        const left = Number(
          element.style.left.substring(0, element.style.left.length - 2)
        );

        if (left < 990) {
          element.style.left = left + 10 + "px";
          myReq = window.requestAnimationFrame(move);
        }
      };
      window.cancelAnimationFrame(myReq);
    </script>
  </body>
</html>

raf는 콜백함수를 전달받고, 해당 콜백함수를 매번 실행하여주는데, 이 콜백함수를 재귀적으로 구성하여 raf가 다시 실행될수 있도록 해주어야합니다. 다만 재귀함수가 끝나고나면 더이상 실행되지 않기때문에 cancelAnimationFrame을 통해 사용하지 않는 타이밍에는 적절히 삭제해주어야합니다.

이벤트루프 속 raf


그렇다면 raf는 어떤방식으로 동작하는것일까요? 사실 이는 자바스크립트 엔진의 이벤트루프를 알고있어야 이해하기 편합니다. 따라서 이벤트 루프와 함께 raf를 알아봅시다

이벤트 루프는 자바스크립트의 비동기 처리작업(task)을 원활하게 처리될수 있도록 관리해주는 일종의 스케줄러입니다. 자바스크립트 콜스택이 비면 큐에서 기다리고있는 작업중 우선순위가 높은 작업을 꺼내어 콜스택으로 넣어주는것입니다. 이벤트루프를 중심으로 자바스크립트 작업 순위를 설명하자면 다음과 같습니다.

  1. call stack의 작업
    콜스택이 비면 태스크를 추가하기 때문에, 우선순위가 가장높습니다. script 태그를 읽고 파싱한 js함수등을 스택내부에 쌓아 메인 작업을 실행합니다 이과정에서 promise, raf, setTimeout등의 비동기 함수도 등록됩니다.

  2. microtask queue(promise)내부의 작업
    해당 과정은 콜스택이 비면 가장먼저 진행됩니다. 다만 이과정은 콜스택보다는 낮은 우선순위이지만 아래 순위보다는 높기때문에 너무많은 promise는 브라우저를 먹통으로 만들수 있습니다.

  3. render(raf)
    브라우저는 보통 16.6ms에 한번 화면을 갱신하는데, 이 갱신이 이루어지기 직전에(safari의 경우 직후에 실행된다고 합니다) raf의 콜백이 실행됩니다. 이는 대부분 macrotask 큐보다 우선순위가 높습니다.

  4. macrotask queue(setTimeout등)의 작업
    앞서 언급한 raf 외의 나머지 setInterval등의 작업이 진행됩니다.

즉 raf는 렌더링프로세스 내부에 들어있으면서 16.6ms 마다 렌더링 직전에 브라우저로부터 실행을 보장받는다는것을 알수 있습니다. 이로인해 애니메이션을 구현할때 장점이 있습니다. 다만 역시 싱글스레드이기 때문에 스레드가 오랜시간 블로킹 당하면(콜스택이 가득차거나 promise로 인해 microtaskqueue가 가득찬경우) 렌더링이 진행되지 않고, 렌더링이 진행되지 않으면 raf도 실행할수 없으므로 100% 진행을 보장하지는 않습니다.

setInterval 과 raf

앞서 raf가 자바스크립트에서 어떠한 방식으로 동작하였는지 이해하였다면 비슷한 기능을 할것같은 setInterval과도 비교해볼수 있습니다. setInterval에도 16.6ms만큼의 반복시간을 주면 raf와 동일할수도 있지 않을까 라는 생각이 들기 때문입니다. 크게 두가지 차이점을 가집니다.

  1. 프레임 drop 유무
    setInterval은 16.6ms 만큼의 지연시간을 명시하더라도 콜백함수의 작업이 길어져서 50ms가 걸린경우 2프레임이 생성되지 않아 뚝뚝 끊기는 현상이 일어납니다. 반면 raf는 콜백함수의 작업이 늦어지더라도 16.6ms에 한번의 작업을 보장하기위해 최대한 노력합니다. 따라서 프레임 드랍이 거의 발생하지 않아 애니메이션이 올바르게 보여집니다

  2. 백그라운드시 실행 유무
    브라우저에 여러 탭이 열려있고, setTimeout과 raf가 동작중인 탭이아닌 다른 탭을 열어두었을경우 해당탭은 백그라운드상태가 됩니다. 하지만 setTimeout은 계속 동작하지만, raf는 렌더링 되기 직전에만 동작하므로 실행되지 않아 브라우저 측면에서 성능상 이점을 가져갈수 있습니다.

따라서 애니메이션 같이 16.6ms에 1프레임을 가져가야하는 작업을 실행하는 경우 setInterval은 부여한 지연시간에 한번실행되는것을 보장할수 없고 성능상 문제가 있기 때문에, 반드시 raf를 사용하는것이 좋습니다.

마치며

애니메이션과 raf, 그리고 기존에 raf대신 사용하던 setInterval과의 차이점에 대해서 알아보았습니다. 사실 해당 api는 애니메이션 효과같은 곳에 사용하지 않는다면 큰 활용도가 없을것이라고 생각할수도 있지만, 굳이 애니메이션이 아니더라도 setTimeout 과 setInterval과 달리 정확한시간주기를 가지고 싶은 작업에 사용할수 있으므로 알아두면 유용할것 같습니다.

참고자료

https://itchallenger.tistory.com/838
https://wikidocs.net/158522

profile
프론트엔드 개발자 김상두입니다

0개의 댓글