requestAnimationFrame — 60fps 애니메이션과 렌더 타이밍
requestAnimationFrame(rAF)은 브라우저의 리페인트 주기에 맞춰 콜백을 실행합니다.setTimeout으로 애니메이션을 만들면 화면 찢김이 발생할 수 있지만, rAF를 사용하면 부드러운 60fps 애니메이션을 구현할 수 있습니다.
기본 사용법
function animate(timestamp) {
// timestamp: 페이지 로드 후 경과 시간 (밀리초, DOMHighResTimeStamp)
// 애니메이션 로직
element.style.transform = `translateX(${timestamp * 0.1}px)`;
// 다음 프레임 예약
requestAnimationFrame(animate);
}
// 시작
const animationId = requestAnimationFrame(animate);
// 중지
cancelAnimationFrame(animationId);
setTimeout vs requestAnimationFrame
// setTimeout — 문제 있는 방식
function animateWithTimeout() {
element.style.left = parseInt(element.style.left) + 1 + "px";
setTimeout(animateWithTimeout, 16); // 약 60fps이지만...
}
// 문제: 브라우저 리페인트와 동기화되지 않아 프레임 드롭 발생
// requestAnimationFrame — 올바른 방식
function animateWithRAF() {
element.style.left = parseInt(element.style.left) + 1 + "px";
requestAnimationFrame(animateWithRAF);
}
// 브라우저 리페인트 직전에 실행되어 부드러운 애니메이션
| 항목 | setTimeout | requestAnimationFrame |
|---|---|---|
| 실행 타이밍 | 지정된 시간 후 | 다음 리페인트 전 |
| 프레임 동기화 | X | O |
| 탭 비활성화 시 | 계속 실행 | 일시 정지 |
| 배터리 소모 | 높음 | 낮음 |
시간 기반 애니메이션
프레임 속도에 독립적인 애니메이션을 만들려면 시간 기반으로 계산해야 합니다.
function createAnimation(duration, updateFn) {
let startTime = null;
let animationId = null;
function frame(timestamp) {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / duration, 1); // 0~1
updateFn(progress);
if (progress < 1) {
animationId = requestAnimationFrame(frame);
}
}
animationId = requestAnimationFrame(frame);
// 취소 함수 반환
return () => cancelAnimationFrame(animationId);
}
// 사용: 1초 동안 0px → 300px 이동
const cancel = createAnimation(1000, (progress) => {
element.style.transform = `translateX(${progress * 300}px)`;
});
이징 함수 (Easing)
const easing = {
linear: (t) => t,
easeInQuad: (t) => t * t,
easeOutQuad: (t) => t * (2 - t),
easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
easeOutCubic: (t) => --t * t * t + 1,
easeOutElastic: (t) => {
const p = 0.3;
return Math.pow(2, -10 * t) * Math.sin(((t - p / 4) * (2 * Math.PI)) / p) + 1;
},
};
// 이징 적용
createAnimation(1000, (progress) => {
const easedProgress = easing.easeOutCubic(progress);
element.style.transform = `translateX(${easedProgress * 300}px)`;
});
스크롤 애니메이션
function smoothScrollTo(targetY, duration = 500) {
const startY = window.scrollY;
const diff = targetY - startY;
let startTime = null;
function frame(timestamp) {
if (!startTime) startTime = timestamp;
const progress = Math.min((timestamp - startTime) / duration, 1);
const easedProgress = easing.easeInOutQuad(progress);
window.scrollTo(0, startY + diff * easedProgress);
if (progress < 1) {
requestAnimationFrame(frame);
}
}
requestAnimationFrame(frame);
}
// 페이지 최상단으로 스크롤
smoothScrollTo(0, 800);
게임 루프 패턴
class GameLoop {
constructor(update, render) {
this.update = update;
this.render = render;
this.lastTime = 0;
this.running = false;
}
start() {
this.running = true;
this.lastTime = performance.now();
requestAnimationFrame((t) => this.loop(t));
}
loop(timestamp) {
if (!this.running) return;
const deltaTime = (timestamp - this.lastTime) / 1000; // 초 단위
this.lastTime = timestamp;
this.update(deltaTime);
this.render();
requestAnimationFrame((t) => this.loop(t));
}
stop() {
this.running = false;
}
}
// 사용
const game = new GameLoop(
(dt) => {
// 물리 업데이트 (dt = 프레임 간 시간)
player.x += player.velocityX * dt;
player.y += player.velocityY * dt;
},
() => {
// 렌더링
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(player.x, player.y, 20, 20);
}
);
game.start();
스크롤 이벤트 최적화
// rAF를 쓰로틀링으로 활용
let ticking = false;
window.addEventListener("scroll", () => {
if (!ticking) {
requestAnimationFrame(() => {
// 스크롤 위치에 따른 UI 업데이트
updateParallax(window.scrollY);
ticking = false;
});
ticking = true;
}
});
FPS 카운터
function createFPSCounter() {
let frameCount = 0;
let lastTime = performance.now();
function frame(timestamp) {
frameCount++;
if (timestamp - lastTime >= 1000) {
console.log(`FPS: ${frameCount}`);
frameCount = 0;
lastTime = timestamp;
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
주의사항
// 1. rAF 콜백에서 무거운 작업 금지
requestAnimationFrame(() => {
// 16ms 안에 끝나야 60fps 유지
// 무거운 계산은 Web Worker로 분리
});
// 2. 탭이 비활성화되면 rAF 호출이 중지됨
// → 시간 기반 애니메이션에서 큰 deltaTime 방지
const maxDelta = 1 / 30; // 최대 프레임 간격 제한
const clampedDelta = Math.min(deltaTime, maxDelta);
// 3. CSS 애니메이션이 가능하면 CSS를 우선 사용
// rAF는 JS 제어가 꼭 필요한 경우에만
**기억하기 **: requestAnimationFrame은 브라우저의 리페인트 주기에 동기화되어 부드러운 애니메이션을 만듭니다. 시간 기반(deltaTime)으로 계산하면 프레임 드롭에도 일관된 속도를 유지합니다. 단순 CSS 애니메이션이 가능한 경우에는 CSS를 사용하는 것이 성능상 더 좋습니다.
댓글 로딩 중...