[HTML] requestAnimationFrame

yongkini ·2021년 12월 8일
21

HTML

목록 보기
10/12
post-thumbnail

Using requestAnimationFrame the browser can further optimize the resource consumption and make the animations smoother.

배경

우리는 애니메이션 (움직이는 효과)을 구현할 때 주로 css animation을 써서 간단한 구현을 한다. transform 의 translate 등을 써서 움직이는 효과를 내고 transition-duration : 1000ms 등의 속성을 추가하여 천천히 움직이는 등의 효과를 연출(?)한다. 이에 더하여 JS 로도 Animation을 구현하기도 하는데, 이 경우는 CSS로는 구현하기 어려운 좀더 복잡한 부분을 구현할 때 쓴다. 과거에는 주로 setInterval, setTimeOut 등을 이용해서 간격을 정해놓고 연속으로 특정 이미지를 없앴다가 만들었다가를 빠르게 해서(과거 영화를 보여주던 원리와 같이 정적 이미지를 빠르게 넘기면서 보여주면 움직이는 효과가 나는 것처럼) 구현을 했었다. 그러나, 이 방법은 이벤트 루프(동기)에 의해 delay가 발생할 수 있고(간격이 빨라지고, 로직이 복잡해지면) 이에 따라 유저들이 해당 애니메이션을 부드럽지 않다고 느낄 가능성이 높아진다. 그래서 canvas 혹은 좀 더 부드러운 애니메이션을 위해서 HTML5 에서 등장한 것이 'requestAnimationFrame' 이다.

보통 브라우저는 60FPS(Frame Per Second)를 지원한다. 즉, 초당 60개의 프레임을 찍을 수 있다. 그리고 이를 계산해보면(밀리세컨 단위로) 결국 한 프레임을 찍는데 16ms 정도가 이상적이라고 할 수 있다. 이 때, setTimeout, setInterval을 쓰면, 프레임 누수(?)가 발생하는데, 그 이유는 브라우저의 프레임 생성 간격에 알맞게 setTimeout 에 의한 콜백함수가 실행되지 않으면(자바스크립트는 알다시피 싱글 스레드 언어이기 때문에 원하는 간격으로 실행을 하지 못하는 경우가 가능하다. 이벤트 루프 자체가 동기적으로 일을 처리하기 때문이다), 해당 프레임을 잃기 때문이다.
: 위에처럼 16ms 간격의 끝부분에서 setTimeout이 작동하고, 그에 따라 렌더링이(혹은 리렌더링)16ms의 한 파트를 건너뛰고, 이뤄진다면 그 부분의 프레임은 유실되는 것이다. 프레임 유실은 곧 애니메이션에서 버벅거림으로 나타난다. 그래서 'requestAnimationFrame'가 해결책으로 나온 것이다.

requestAnimationFrame이란?

: window(브라우저에서만 제공하고, NodeJS에서는 다른 API를 써야함) 객체 안에 있는 'requestAnimationFrame'는 앞서 말했듯이 부드러운 애니메이션을 위해 사용하고 그러한 기능을 제공한다. 좀더 쉽게 말해 브라우저가 렌더링을 할 수 있을때에 다음 렌더링을 진행할 수 있도록 최적화 해주는 툴과 같다.


: 위의 그림은 이전 방식인 'setInterval'과 setTimeout을 썼을 때, 노란색 부분이 js code를 실행하는 시간이고, 보라색, 초록색이 레이아웃, 페인트(렌더링) 과정을 나타낸다. 이를 살펴보면, 특정 js 코드를 렌더링 과정이 블로킹하기도 하고, js 코드 실행 부분이 너무 길어져서 렌더링이 지연되기도 한다. 이처럼 과거 방식은 'choppy'한 애니메이션의 한계를 갖는다.
: 그러나, requestAnimationFrame을 쓰면, 위와 같은 지연 및 블로킹 현상이 생기지 않아, 부드러운 애니메이션을 제공할 수 있다.

requestAnimationFrame의 원리

  • 'requestAnimationFrame'은 매개변수로 콜백 함수를 받는데, 리페인트 이전에 실행할 콜백 함수를 인자로 받는다.
  • 알고 있듯이 브라우저는 리플로우, 리페인트 과정을 거쳐서 리렌더링을 하게된다. 이 때, 아직, 이전 리렌더링이 끝나지도 않았는데, 다음 애니메이션 로직을 실행하도록 명령을 내린다면 애니메이션이 의도한대로 부드럽게 움직이지 않을 것이다.
  • requestAnimationFrame은 위와 같은 문제를 해결해준다.
  • 그래서 리페인트 과정이 끝난 후 적용할 애니메이션을 requestAnimationFrame의 콜백으로 넣어주면 된다. 예를 들어,
const canvas = document.querySelector(".canvas");
const context = canvas.getContext("2d");
function draw() {
  context.arc(10, 150, 10, 0, Math.PI * 2, false);
  context.fill();
  console.log("그렸다!");
  
  requestAnimationFrame(draw);
}
draw();

위의 코드를 실행하면, 초당 60 프레임을 그린다(60fps). 또한,

const canvas = document.getElementById("p-canvas");
const context = canvas.getContext("2d");
let x = 1;

function draw() {
  context.beginPath();
  context.fillRect(0, 0, x, x);
  context.closePath();
  context.fill();
  x++;

  requestAnimationFrame(draw);
}

draw();

위와 같이 코드를 짜주면,
이런식으로 'smooth'하게 움직이면서 사각형이 만들어진다(이 때, clearInterval처럼 멈추고 싶을 때는 'cancelAnimationFrame'을 사용한다.)
사실 저정도 구현은

const canvas = document.getElementById("p-canvas");
const context = canvas.getContext("2d");
let x = 1;

function draw() {
  context.beginPath();
  context.fillRect(0, 0, x, x);
  context.closePath();
  context.fill();
  x++;
}

setInterval(draw, 10);

이렇게 과거의 방식을 써도 충분히 smooth하게 구현된다. 그러면 requestAnimationFrame의 장점은 뭘까?

requestAnimationFrame의 장점

  • 페이지가 비활성 상태이면 페이지의 화면 그리기 작업도 브라우저에 의해 일시 중지되므로 브라우저를 따르는 requestAnimationFrame도 렌더링을 중지합니다. 이에 따라 CPU 리소스와 배터리 수명을 낭비하지 않게된다(setInterval은 비활성 상태여도 백그라운드에서 계속 실행됨)
  • 브라우저가 언제 업데이트를 할지 알게해줌으로써 프레임(frame) 손실을 방지해줌. 이에 따라 리소스도 균등하게 분배해서 사용할 수 있고, 간격도 균등하게 가져갈 수 있다.

requestAnimationFrame도 만능은 아니다.

: 만약, requestAnimationFrame()의 호출할 함수가 16ms 이상이 걸린다면 그 다음에 호출될 requestAnimationFrame이 생략되는 문제가 있다. 당연한게 requestAnimationFrame은 특정 브라우저가 렌더링을 할 수 있는 능력치만큼을 반영해 콜백을 실행시키고자 하는건데, 한번의 렌더링 후에 callback을 실행했을 때 다음 렌더링까지 16ms 이상이 걸리는 로직이 콜백에 있다면, 싱글 스레드인 JS에서는 그 다음 requestAnimationFrame을 실행하지 못하는 문제가 생긴다. 그래서 한 템포의 렌더링을 놓치게 되고, 이후 로직이 완료되면 RAF를 실행하게 된다(즉, 하나의 프레임을 잃게 되는 것이다). RAF는 프레임 유실을 최대한 방지하기 위해서 제공하는건데 프레임이 유실되는 형태로 쓰인다면 제 역할을 하지 못하는 것이 되므로 RAF의 콜백에 넣는 함수는 최대한 light하게 작성하거나 쪼개서 작성하는걸 추천한다.

Frame Per Seconds(FPS) 관련 최적화 툴

  • 개발자 도구에서 ctrl + shift + p 를 누른 다음에 Show Frame Per Seconds Meter를 검색하면 나오는 툴이 있는데,
    이를 이용하면 현재 브라우저가 해당 페이지에서 몇 fps를 보증하고 있는지를 볼 수 있다. 따라서 fps가 급격히 낮아지는 부분이 있거나 하면 이를 체크하여 최적화를 할 수 있다.

CancelAnimationFrame

: setTimeout이 있으면 clearTimeout이 있는 것 처럼 requestAnimationFrame의 동작을 멈추고 싶으면 cancelAnimationFrame을 통해 멈추면 된다.

profile
완벽함 보다는 최선의 결과를 위해 끊임없이 노력하는 개발자

3개의 댓글

comment-user-thumbnail
2023년 2월 25일

"이 때, 새로운 requestAnimationFrame을 생성하면, 이전 것은 반드시 cancelAnimationFrame을 통해 삭제해줘야한다. 안그러면 콜백 리스트에 계속해서 쌓이게된다. 예를 들어"
부분이 잘못된 정보인 것 같아보여요~
저는 cancel은 콜백을 취소할 필요가 있을 때에만 호출하면 되는걸로 이해하고 있었는데 그렇지는 않은가요?

1개의 답글
comment-user-thumbnail
2023년 11월 18일

좋은 정리 감사합니다

답글 달기