브라우저의 애니메이션을 구현하기 위해 Transform, Transition, Animation에 대해 알아보고 더 나아가 3D도 간단하게 살펴보았습니다.
위 세 가지의 애니메이션은 CSS를 사용하여 구현하는 방식이고 주로 간단한 애니메이션을 구현할 때 사용합니다.
보다 복잡한 애니메이션과 사용자와 상호작용을 구현하기위해 Javascript를 사용하여 애니메이션을 구현할 수 있습니다.
하지만 Javascript를 사용하여 구현하는 애니메이션의 치명적인 단점은 CSS보다 성능이 떨어진다는 것입니다. 특히 모바일 환경에서 더 치명적입니다.
따라서 Javascript를 사용하여 구현하는 애니메이션의 경우 이를 위한 최적화 기법이 존재하는데 그 방법이 바로 RequestAnimationFrame을 사용하는 것 입니다.
애니메이션은 여러장의 연결된 사진을 주기적으로 보여주는 단순한 구조입니다.
같은 애니메이션이라도 더 많은 사진으로 분리해서 보여주는것이 더 부드러운 애니메이션을 표현할 수 있습니다.
⭐️ 1000ms / 60fps = 16.666...
이것을 구현하는 방식은 setInterval 과 RequestAnimationFrame 두 방식이 있는데 어떤 차이점을 가지고있는지 살펴보도록 하겠습니다.
🙋🏻♂️ 왜 하필 60fps 인가요?
사람의 눈으로 보았을 때 60fps 정도가 되어야 끊김없고 부드러운 애니메이션으로 인식하기 때문입니다.
또한 PC와 Mobile의 화면 주사율이 최소 60fps로 설정되어 있기 때문입니다.
즉, 자바스크립트로 사용자에게 부드러운 애니메이션을 제공하기 위해서는 16.6ms 마다 호출되는 함수로 애니메이션을 구현하면 됩니다.
자바스크립트는 setInterval과 RequestAnimationFrame를 사용하여 애니메이션을 구현할 수 있다고 했고 최적화된 방법은RequestAnimationFrame이라고 했습니다. 그럼 둘의 차이는 어떤것이 있을까요?
setInteval은 본디 애니메이션을 위해 만들어진 함수가 아니기 때문에 애니메이션을 구현하는데에 문제점들이 있습니다.
타이머 함수는 페이지가 렌더링중인지, 콜스택에 어떤 함수가 대기중인지 전혀 신경쓰지 않고 콜스택에 작업을 밀어 넣습니다.
즉, 브라우저 렌더링 파이프를 전혀 신경쓰지 않고 자신의 일만을 한다는 뜻입니다.
바이올린 연주자가 연주중인데 갑자기 드럼도 치라고하면 어떻게될까요? 연주가 멈추거나 드럼연주를 거절하겠죠?
이로인해 프레임이 누락되는 경우가 있어 버벅거림이 발생할 수 있는데 이런 경우를 "프레임드랍" 이라고하며, 이를 해결하기위해 rAF( RequestAnimationFrame )이 탄생 하였습니다.
백그라운드 동작 중지
setInterval 함수는 브라우저의 탭 전환 및 최소화를 고려하지 않고 함수를 호출한다. 이로 인해 시스템의 리소스( CPU 자원 )를 낭비하고 모바일의 경우 배터리를 소모하는 결과를 초래합니다. 반면 requestAnimationFrame는 페이지가 비활성화 된 상태에 최적화 되어있어 함수 호출을 정지하고 화면에 그리기도 멈추기 때문에 시스템의 리소스( CPU 자원 )를 최적화하고 모바일 디바이스의 배터리 소모도 줄일 수 있습니다.
별도의 Queue 에서 연산
RequestAnimationFrame 함수도 비동기 방식으로 동작하는 다른 함수들과 마찬가지로 Queue에 스케쥴링되어 처리되는데 이 때 일반적인 큐와 달리 Animation Frame이라는 별도의 Queue에서 처리됩니다.
별도의 Queue에서 처리하기 때문에 다른 비동기 함수와 경쟁하지 않아도 되기 때문에 실행이 뒤쳐져 프레임드랍이 발생하는 경우의 수를 획기적으로 줄일 수 있습니다.
❗️rAF도 비동기 방식으로 동작하는 것은 여타 비동기 함수와 동일하기 때문에 완벽하게 부드러운 작동을 보장할수는 없습니다. 브라우저가 처리할 양이 많아 CPU와 GPU에 부하가 생기면 프레임 드랍이 발생할 수 있습니다.
rAF는 함수 재귀 호출을 이용해 무한히 반복시켜 사용해야합니다. 그렇지 않다면 1번 밖에 실행되지 않습니다.
구문
function rAF(timestamp) {
console.log(timestamp); //진행중인 타임스탬프를 매개변수로 받습니다.
rAFId = requestAnimationFrame(rAF); //rAF Id를 반환하고 콜백함수를 스케쥴링합니다. 재귀호출 !
}
btn.addEventListener("click", () => {
cancelAnimationFrame(rAFId); //rAFId를 사용하여 반복을 정지시킬 수 있습니다.
});
프레임과 진행시간을 표시해주는 예제
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Animation Frame Example</title>
<style>
.stick1 {
width: 100px;
height: 30px;
background-color: red;
transform-origin: left;
position: absolute;
top: 50px;
}
.info {
position: absolute;
top: 100px;
}
</style>
</head>
<body>
<div class="stick1"></div>
<div class="info">
<p id="elapsed-time">Elapsed Time: 0ms</p>
<p id="frame-count">Frame Count: 0</p>
</div>
</body>
</html>
<script>
const item = document.getElementsByClassName("stick1")[0];
const elapsedTimeDisplay = document.getElementById("elapsed-time");
const frameCountDisplay = document.getElementById("frame-count");
let start;
let previousTimeStamp;
let frameCount = 0;
function step(timestamp) {
if (start === undefined) {
start = timestamp;
}
const elapsed = timestamp - start;
frameCount++;
elapsedTimeDisplay.textContent = `Elapsed Time: ${elapsed.toFixed(2)}ms`;
frameCountDisplay.textContent = `Frame Count: ${frameCount}`;
if (previousTimeStamp !== timestamp) {
const count = Math.min(0.1 * elapsed, 200);
item.style.transform = `translateX(${count}px)`;
}
if (elapsed < 2000) {
previousTimeStamp = timestamp;
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
</script>