“setInterval() 함수는 언제나 콜백함수를 호출하기 때문에 브라우저의 다른 탭이 선택된 경우와 같이 실제 화면을 다시 그릴 필요가 없는 경우에도 화면을 다시 그린다. 그래서 시스템의 리소스 낭비를 초래하여, 불필요한 전력을 소모하게 만든다. 또한, 디스플레이 갱신 전에 캔버스를 여러 번 고치더라도 디스플레이 갱신 바로 직전의 캔버스 상태만 적용이 되기 때문에, 프레임 손실이 발생할 수도 있다.”
reference : Beautiful Code
다시 한번 setInterval 방식의 비효율성을 위의 문장을 통해 리마인드(?)하고 RAF를 바탕으로 구현 연습을 해보고자 한다.
: 오늘은 Reference를 참고해서 기능 구현 연습을 해보고자 한다(RAF = RequestAnimationFrame 을 사용해서).
provides a smoother and more efficient way to create animated webpages by calling the animation frame when the system is ready to paint the frame
위의 레퍼런스 사이트에 나오는 문장인데, 이것이 RAF를 정확히 설명해준다고 생각한다. 특히 'calling the animation frame when the system is ready to paint the frame' 이 부분이 가장 결정적이다. 애니메이션 프레임을 페인팅할 준비가 됐을 때 호출한다는 부분은 setInterval 방식이 준비가 되지 않아도 페인팅을 요청하는 것과 대비되면서 RAF만의 장점으로 가장 적절한 표현이라고 생각한다. 이에 더하여,
This resulted in overdrawn animations, wasted CPU cycles, and extra power usage. Further, animation frequently occurs even when a website isn't visible, particularly when the website uses pages in background tabs or when the browser is minimized.
이 부분을 보면, 이전의 방식(setInterval, setTimeout)을 쓰면, CPU 리소스 낭비 그리고 웹 사이트가 비활성화 상태임에도 애니메이션이 계속해서 발생하는 낭비의 문제가 발생한다고 한다.
: 실제로 requestAnimationFrame을 써서 Hello there라는 글자가 쓰여진 빨간 박스가 움직이도록 구현해봤다. 마우스로 박스를 클릭하면 멈추고(cancelAnimationFrame), 다시 클릭하면 재실행 된다.
var elm = document.getElementById("animated");
var handle = null;
var lPos = 0;
renderLoop();
function renderLoop() {
elm.style.left = ((lPos += 3) % 600) + "px";
handle = window.requestAnimationFrame(renderLoop);
}
document.getElementById("animated").addEventListener(
"click",
function () {
if (handle) {
window.cancelAnimationFrame(handle);
handle = null;
} else {
renderLoop();
}
},
false
);
: 위의 코드로 구성돼있다. handle이라는 변수를 둬서 이벤트 마다 null인지 아닌지 체크하는 분기 처리를 한 것은 만약 이미 RAF로 애니메이션을 진행중이라면 다시 renderLoop 함수를 실행하면 중복해서 애니메이션을 실행하는 꼴(?)이 되기 때문에 처리해줬다. 그래서 만약에 이미 진행중이라면 handle = null로 해서 또 클릭하면 다시 진행할 수 있게하고, cancelAnimationFrame으로 진행중인 프레임을 없애줬다.
: 위에 gif는 requestAnimationFrame을 이용해 내가 만들어본 일종의 줄긋기 게임(?)이라고 할 수 있다. 상당히 조잡하지만,
const moveRect = (now) => {
try {
if (!last || (now - last) / 100 >= 0) {
dot[0] += ways[dir][0] * speed;
dot[1] += ways[dir][1] * speed;
last = now;
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.moveTo(movingDot[0], movingDot[1]);
movingDot[0] = dot[0];
movingDot[1] = dot[1];
ctx.lineTo(dot[0], dot[1]);
ctx.stroke();
ctx.fillRect(dot[0], dot[1], 10, 10);
ctx.restore();
cancelAnimationFrame(moveRect);
handle = requestAnimationFrame(moveRect);
}
} catch {
handle = null;
document.body.style.overflow = "";
movingDot[0] = dot[0];
movingDot[1] = dot[1];
cancelAnimationFrame(moveRect);
}
};
아래와 같은 로직과
window.addEventListener("keydown", (e) => {
dir = e.key;
if (dir in ways)
inform.textContent = `speed : ${speed} | direction : ${dir.substring(5)}`;
else {
return;
}
if (handle) {
handle = null;
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "hidden";
handle = requestAnimationFrame(moveRect);
}
});
이러한 이벤트 핸들러를 이용해서 구현했다. 키보드 화살표 버튼을 통해 검정 모양의 직사각형을 조종(?)할 수 있고, 화살표 이외의 키를 누르면 멈춘다. 이 때, 직사각형이 움직이는 경로를 따라서 선을 긋도록 했다. 조잡하지만 smooth한 애니메이션을 구현할 수 있었다. 이러한 원리를 이용해서 팩맨 게임도 만들 수 있을 것이고, 그림판도 만들 수 있을 것(그림판의 경우 마우스 이벤트로 만들면 될 것이다).
: 추가적으로 위 gif 바로 위에는 현재 방향키의 방향과 내가 설정해놓은 속도가 나타나도록 했다. 다음번에는 곡선 형태로 움직이는 별똥별과 같은 애니메이션을 만들어봐야겠다. 사실 HTML로 게임을 만들기도 한다는 사실은 이전부터 알고 있었지만, 실제로 (게임이라고 하기엔 부족하지만 ㅎ) 게임과 같은 인터페이스를 제공하는 애니메이션을 구현해보니까 재밌었다. 개인 플젝에 이를 어떻게 활용할지 고민해봐야겠다.
: RAF와 canvas API를 다른 것을 레퍼런스하지 않고, 내가 배운 내용 중 기억하는 것을 바탕으로 뭔가를 기획, 계획해서 만들어본 재밌는 경험이었다. 이 경험을 통해 후에 더 퀄높은 뭔가를(?) 구현할 수 있을 것 같다 :)