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'가 해결책으로 나온 것이다.
: window(브라우저에서만 제공하고, NodeJS에서는 다른 API를 써야함) 객체 안에 있는 'requestAnimationFrame'는 앞서 말했듯이 부드러운 애니메이션을 위해 사용하고 그러한 기능을 제공한다. 좀더 쉽게 말해 브라우저가 렌더링을 할 수 있을때에 다음 렌더링을 진행할 수 있도록 최적화 해주는 툴과 같다.
: 위의 그림은 이전 방식인 'setInterval'과 setTimeout을 썼을 때, 노란색 부분이 js code를 실행하는 시간이고, 보라색, 초록색이 레이아웃, 페인트(렌더링) 과정을 나타낸다. 이를 살펴보면, 특정 js 코드를 렌더링 과정이 블로킹하기도 하고, js 코드 실행 부분이 너무 길어져서 렌더링이 지연되기도 한다. 이처럼 과거 방식은 'choppy'한 애니메이션의 한계를 갖는다.
: 그러나, 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()의 호출할 함수가 16ms 이상이 걸린다면 그 다음에 호출될 requestAnimationFrame이 생략되는 문제가 있다. 당연한게 requestAnimationFrame은 특정 브라우저가 렌더링을 할 수 있는 능력치만큼을 반영해 콜백을 실행시키고자 하는건데, 한번의 렌더링 후에 callback을 실행했을 때 다음 렌더링까지 16ms 이상이 걸리는 로직이 콜백에 있다면, 싱글 스레드인 JS에서는 그 다음 requestAnimationFrame을 실행하지 못하는 문제가 생긴다. 그래서 한 템포의 렌더링을 놓치게 되고, 이후 로직이 완료되면 RAF를 실행하게 된다(즉, 하나의 프레임을 잃게 되는 것이다). RAF는 프레임 유실을 최대한 방지하기 위해서 제공하는건데 프레임이 유실되는 형태로 쓰인다면 제 역할을 하지 못하는 것이 되므로 RAF의 콜백에 넣는 함수는 최대한 light하게 작성하거나 쪼개서 작성하는걸 추천한다.
ctrl + shift + p
를 누른 다음에 Show Frame Per Seconds Meter를 검색하면 나오는 툴이 있는데, : setTimeout이 있으면 clearTimeout이 있는 것 처럼 requestAnimationFrame의 동작을 멈추고 싶으면 cancelAnimationFrame을 통해 멈추면 된다.
"이 때, 새로운 requestAnimationFrame을 생성하면, 이전 것은 반드시 cancelAnimationFrame을 통해 삭제해줘야한다. 안그러면 콜백 리스트에 계속해서 쌓이게된다. 예를 들어"
부분이 잘못된 정보인 것 같아보여요~
저는 cancel은 콜백을 취소할 필요가 있을 때에만 호출하면 되는걸로 이해하고 있었는데 그렇지는 않은가요?