requestAnimationFrame(rAF)은 브라우저의 리페인트 주기에 맞춰 콜백을 실행합니다. setTimeout으로 애니메이션을 만들면 화면 찢김이 발생할 수 있지만, rAF를 사용하면 부드러운 60fps 애니메이션을 구현할 수 있습니다.

기본 사용법

JS
function animate(timestamp) {
  // timestamp: 페이지 로드 후 경과 시간 (밀리초, DOMHighResTimeStamp)

  // 애니메이션 로직
  element.style.transform = `translateX(${timestamp * 0.1}px)`;

  // 다음 프레임 예약
  requestAnimationFrame(animate);
}

// 시작
const animationId = requestAnimationFrame(animate);

// 중지
cancelAnimationFrame(animationId);

setTimeout vs requestAnimationFrame

JS
// 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);
}
// 브라우저 리페인트 직전에 실행되어 부드러운 애니메이션
항목setTimeoutrequestAnimationFrame
실행 타이밍지정된 시간 후다음 리페인트 전
프레임 동기화XO
탭 비활성화 시계속 실행일시 정지
배터리 소모높음낮음

시간 기반 애니메이션

프레임 속도에 독립적인 애니메이션을 만들려면 시간 기반으로 계산해야 합니다.

JS
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)

JS
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)`;
});

스크롤 애니메이션

JS
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);

게임 루프 패턴

JS
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();

스크롤 이벤트 최적화

JS
// rAF를 쓰로틀링으로 활용
let ticking = false;

window.addEventListener("scroll", () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      // 스크롤 위치에 따른 UI 업데이트
      updateParallax(window.scrollY);
      ticking = false;
    });
    ticking = true;
  }
});

FPS 카운터

JS
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);
}

주의사항

JS
// 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를 사용하는 것이 성능상 더 좋습니다.

댓글 로딩 중...