본문 바로가기
개발(Development)/JS(자바스크립트)

[JS]requestAnimationFrame/cancelAnimationFrame원리와 사용 방법(Feat. 브라우저 작동 방식)

by 카레유 2023. 1. 21.

<requestAnimationFrame/cancelAnimationFrame 원리와 사용 방법> 

requestAnimationFrame을 3줄로 요약하면 아래와 같다.

 

1. requestAnimationFrame은 브라우저가 화면을 그리기(Paint) 직전에 실행할 코드를 등록한다.

2. requestAnimationFrame을 재귀적으로 반복호출하면, 60FPS 목표로 애니메이션을 최적화한다.

3. cancelAnimationFrame은 requestAnimationFrame으로 등록해둔 코드를 취소한다.

 

이게 도대체 무슨말인가?!!

 

requestAnimationFrame을 제대로 이해하기 위해선

일단 브라우저가 화면을 그리는 원리부터 알아두면 좋다.

 

차근차근 정리해 보려 한다.

생각보다 쉽다.

 

# 브라우저 작동 방식

- 브라우저가 화면을 그리는 원리는 대략 아래와 같다.

 

1. 서버에 요청하여 HTML, CSS, JS 문서를 받는다.

2. HTML을 읽어서 DOM 구조를 만든다.

3. CSS를 읽어서 CSSOM을 만든다.

4. DOM과 CSSOM을 합쳐 Render Tree를 만든다.

   - Render Tree는 각 DOM요소에 CSS속성이 매칭된 상태

DOM, CSSOM, Render Tree-출처: https://web.dev/critical-rendering-path-render-tree-construction/

 

5. Render Tree를 이용해 화면을 그린다. 이걸 페인트(Paint)라고 한다.

6. JS에서 DOM이나 CSS를 수정하는 경우,

7. 위치가 변경되었다면, 레이아웃을 다시 잡고(리플로우)

8. 화면을 다시 그린다(리페인트)


# requestAnimationFrame 작동 원리

requestAnimationFrame은 브라우저가 화면을 그리기 직전

즉, 페인트/리페인트 직전에 호출되어야 할 코드를 등록해두는 함수다.

 

무슨 말이냐 하면,

원래 브라우저는 JS코드를 위에서부터 아래로 내려가며 차례로 실행해 나간다.

그런데 requestAnimationFrame을 만나면 마치 setTimeout처럼 작동한다.

 

차이점은

- setTimeout이 특정 ms 후에 실행될 코드를 등록한다면,

- requestAnimationFrame은 조금(?) 있다가 브라우저가 화면을 그리기 시작하기 직전에 실행될 코드를 등록한다.

정도이다.

 

일종의 지연 효과가 발생하는 것이다.

 

예를 들어 아래 코드를 보자.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <title>Document</title>
  </head>
  <body>
  </body>
  <script>
  
    // requestAnimationFrame에 콜백함수 등록
    requestAnimationFrame(() => {
      // 뒤에 있는 그냥 console.log보다 늦게 실행된다.
      console.log("requestAnimationFrame 콘솔");
    });
    
    // 이 코드가 먼저 실행된다.
    console.log("그냥 콘솔");
    
  </script>
  </body>
</html>

 

뒤에 있는 코드들이 먼저 실행되고,

requestAnimationFrame에 인자로 들어온 콜백함수는 브라우저가 실제로 화면을 그리기 직전에 실행된다.

 

requestAnimationFrame은 그냥 그런 일을 하는 함수다.

좋아! 무슨 일을 하는진 알겠는데, 이게 왜 필요한거지?


#  setInterval 애니메이션의 치명적 약점

애니메이션의 원리는

특정 코드를 반복 호출하며 화면을 변화시키는 것이다. 

<!DOCTYPE html>
<html lang="ko">
  <head>
    <title>테스트</title>
  </head>
  <body>
    <!-- 파란색 100x100 사각형 -->
    <div
      class="rect"
      style="width: 100px; height: 100px; background-color: blue"
    ></div>

    <script>
      // div DOM 취득
      const div_rect = document.querySelector(".rect");

      // 사각형의 너비
      let anim_width = 100;

      // 반복적으로 호출하며 사각형의 너비를 늘리는 애니메이션
      setInterval(() => {
        anim_width += 2;
        div_rect.style.width = `${anim_width}px`;
      }, 10);
    </script>
  </body>
</html>

 

이런 간단한 애니메이션이라면 setInterval만으로도 충분하다.

 

그런데 만약 setInterval로 반복 호출하는 코드가 상당히 오랜시간 걸리는 코드라면?

예를 들어 엄청나게 복잡한 계산을 한 결과를 DOM에 반영하는 코드라면?

그리고 이런 코드가 한두개가 아니라면?

 

브라우저는 해당 코드가 실행되는 동안 화면을 그리지 못하게될 가능성이 있고,

사용자는 상당히 버벅이는 듯한 화면을 보게될 가능성이 생겨버린다.

 

지금이다!

requestAnimationFrame이 필요한 순간은!


# requestAnimationFrame - 재귀적 반복 호출

거두 절미하고 requestAnimationFrame을 사용하면,

화면 버벅임이 최소화 되고, 애니메이션 성능이 최적화 된다.

 

일단 애니메이션을 위해 반복 호출해야 할 코드를 loop 함수로 만들었다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <title>테스트</title>
  </head>
  <body>
    <!-- 파란색 100x100 사각형 -->
    <div
      class="rect"
      style="width: 100px; height: 100px; background-color: blue"
    ></div>

    <script>
      // div DOM 취득
      const div_rect = document.querySelector(".rect");

      // 사각형의 너비
      let anim_width = 100;

      // 반복적으로 호출하며 사각형의 너비를 늘리는 함수
      function loop() {
        anim_width += 2;
        div_rect.style.width = `${anim_width}px`;

        // 이 함수를 재귀적으로 반복 호출되도록 등록한다. 단, 리페인트 전에 실행되도록!
        requestAnimationFrame(loop);
      }

      // loop 함수 호출 등록 => 무한 반복된다!
      requestAnimationFrame(loop);
    </script>
  </body>
</html>

 

loop 함수는 내부에서 본인(loop 함수)를 또 호출하기 때문에,

한번만 호출되면 재귀적으로 반복 호출되는 함수다.

 

단, loop함수를 바로 호출 하지 않고,

requestAnimationFrame의 인자로 등록하여 브라우저가 화면을 그리기 전에 호출되도록 한다.

 

이렇게 해두면 얼마나 빨리 반복 호출될까?

정답은 화면이 최대한 부드럽게 그려질 수 있는한 최대한 빠르게! 이다.

 

일반적으로는 60FPS(1초에 60회 반복 호출되어 화면을 그리는 속도)를 목표로 한다.

즉, 16.6ms 정도 마다 반복 호출되며 화면을 그리는 속도이다.

(물론 컴퓨터 성능, 브라우저 환경 등에 따라 다르다.)

 

여담이지만, 60FPS 이상부터는 인간이 인식하기 힘들다는 것 같다.

 

아니 근데 60FPS(16ms마다 반복) "보장"이 아니고, "목표" 인 이유가 뭘까?


requestAnimationFrame - 애니메이션 최적화 원리

혹시 반복 호출하는 코드가 16.6ms 보다 오래 걸린다면?

 

만약 해당 코드가 requestAnimationFrame에 등록되어 재귀적으로 반복 호출 된다면,

브라우저는 "기다리지 않고" 화면을 먼저 그려 버린다.

코드 실행은? 건너 뛴다!

 

응?!!!

 

코드가 실행되는 동안

브라우저가 화면을 못 그리고 기다리면 애니메이션이 끊겨 보일 수 있다.

 

따라서 일단 화면은 이전 DOM 상태로 그려두고,

그 다음 리페인트 직전에 다시 반복코드를 실행하는 식으로 작동하는 것이다!

 

만약 그 때도 안 끝났으면?

역시 또 화면은 화면대로 그리고, 그 다음 턴을 노리는 식이다.

 

오!!! 그렇다!

 

requestAnimationFrame으로 애니메이션용 코드를 반복 호출하면,

최대한 화면이 부드럽게 애니메이션되도록 호출 속도를 조절하는 것이다.

최대 60FPS 수준의 성능을 "목표"로!

 

이러한 원리로 requestAnimationFrame을 재귀적으로 반복 호출하면,

애니메이션이 최적화된다고 하는 것이다.

 

그리고 이것이 바로 requestAnimationFrame의 이름이 

request "Animation Frame"인 이유! 가 아닐까 라고 생각해본다.

 

따라서 애니메이션이 많은 인터랙션 웹을 만든다면

requestAnimationFrame을 적절하게 잘 활용하면 좋을 것 같다.

 

특히 모바일 환경에서 성능 차이가 느껴진다고 한다.


# 반복 정지 - cancelAnimationFrame 사용방법

requestAnimationFrame 으로 반복 호출되는 애니메이션 코드를 멈출 땐,

일반적으로 cancelAnimationFrame을 사용한다.

 

단, 착각하지 말자!

 

cancelAnimationFrame는 반복을 멈추는 함수가 아니다.(break; 같은게 아니다)

requestAnimationFrame으로 등록해둔 콜백함수의 등록을 취소하는 것이다.

 

즉, 리페인트 전에 호출하도록 등록해둔 코드를 등록 해제하는 것이다.

이런 원리로 반복이 멈추는 것이다.

 

requestAnimationFrame(콜백함수)를 호출하면 "등록 ID"를 리턴하는데,

cancelAnimationFrame(등록ID)를 호출하여 등록을 해제할 수 있다.

 

코드는 아래와 같다. (원리를 알면 참 쉽고, 응용도 가능하다.)

<!DOCTYPE html>
<html lang="ko">
  <head>
    <title>테스트</title>
  </head>
  <body>
    <!-- 파란색 100x100 사각형 -->
    <div
      class="rect"
      style="width: 100px; height: 100px; background-color: blue"
    ></div>

    <script>
      // div DOM 취득
      const div_rect = document.querySelector(".rect");

      // 사각형의 너비
      let anim_width = 100;

      // 반복적으로 호출하며 사각형의 너비를 늘리는 애니메이션
      function loop() {
        anim_width += 2;
        div_rect.style.width = `${anim_width}px`;

        // 이 함수를 재귀적으로 반복 호출되도록 등록한다. 단, 리페인트 전에 실행되도록!
        // rafID 변수에 "등록ID"가 할당된다.
        const rafID = requestAnimationFrame(loop);

        // 너비가 200이 넘으면?
        if (anim_width > 200) {
          // 리페인트 전에 호출하도록 등록해둔 코드를 "등록 해제"한다. => 즉, 반복이 멈춘다.
          cancelAnimationFrame(rafID);
        }
      }

      // loop 함수를 호출한다. 단, 페인트 전에 실행되도록!
      requestAnimationFrame(loop);
    </script>
  </body>
</html>

# requestAnimationFrame이란? - 3줄 요약

1. requestAnimationFrame은 브라우저가 화면을 그리기 직전에 호출할 코드를 등록한다.

2. requestAnimationFrame을 재귀적으로 반복호출하면, 60FPS 목표로 애니메이션을 최적화한다.

3. cancelAnimationFrame은 requestAnimationFrame으로 등록해둔 코드를 취소한다.

 

와! 이제 위의 3줄 요약이 완벽하게 이해된다!!

그렇다면 너무나 다행입니다!

 

조금 실수한 부분이 있더라도 너그러이 양해 부탁 드리며,

긴 글 읽어주셔서 감사합니다 : )

 

 

댓글