2025년 11월 3일

requestAnimationFrame란?

H

Haruシ

FE

들어가며

최근 회사에서 개발하던 중 성능 이슈를 마주쳤습니다.

페이지에 여러 애니메이션이 겹치면서 버벅거리는 현상이 생겼고, 이를 해결하다 보니 requestAnimationFrame이라는 함수를 발견하게 됐습니다.

FrontEnd에서 애니메이션을 만들 때는 보통 CSS 또는 JavaScript를 사용합니다.

두 가지 모두 장단점이 있지만, 동적이고 복잡한 애니메이션을 다룰 때는 JavaScript가 필요합니다.

문제는 JavaScript 애니메이션을 부주의하게 구현하면 성능이 급격히 떨어진다는 것입니다.

이 글에서는 JavaScript 애니메이션을 최적화할 수 있는 requestAnimationFrame 함수에 대해 정리해보겠습니다.

requestAnimationFrame이란?

requestAnimationFrame(이하 rAF)는 JavaScript로 애니메이션을 구현할 때 성능과 UX를 최적화하기에 좋은 함수입니다.

rAF 사용 시에 브라우저에게 작업의 실행을 알려주게 되며, 내부적으로는 requestAnimationFrame 전용의 queue에 콜백함수를 등록합니다. 브라우저는 다음 리페인트 직전에 queue에 등록된 모든 콜백을 순서대로 실행합니다. 이를 통해 JavaScript 애니메이션이 브라우저의 최적화된 렌더링 사이클과 자동으로 동기화됩니다.

setTimeout/setInterval과의 차이점

JavaScript 애니메이션은 보통 setTimeout이나 setInterval로도 구현할 수 있습니다. 하지만 rAF가 더 효율적입니다.

setTimeout/setInterval은 고정된 시간 간격(예: 16ms)으로 콜백을 실행하기 때문에, 브라우저의 리페인트 타이밍과 맞지 않을 수 있습니다. 결과적으로 프레임 누락이나 불필요한 리페인트가 발생합니다. 반면 rAF는 브라우저의 실제 리페인트 직전에 실행되어 프레임 손실 없이 부드러운 애니메이션을 보장합니다.

또한 rAF는 탭이 비활성 상태일 때 자동으로 일시 중지되어 CPU와 배터리 자원을 절약합니다. 하지만 setTimeout은 탭이 비활성이어도 계속 실행됩니다.

기본 사용 방법

function animate(timestamp) {
  // 애니메이션 로직 구현
  element.style.transform = `translateX(${timestamp}px)`;
  
  // 애니메이션 계속 실행
  requestAnimationFrame(animate);
}

// 애니메이션 시작
requestAnimationFrame(animate);

rAF는 콜백에 타임스탐프(밀리초 단위)를 인자로 전달합니다. 이를 활용해 경과 시간을 계산하고 일정한 속도의 애니메이션을 구현할 수 있습니다.

let startTime = null;

function animate(timestamp) {
  if (!startTime) startTime = timestamp;
  
  const elapsed = timestamp - startTime;
  const progress = (elapsed / 1000) % 1; // 1초마다 반복
  
  element.style.opacity = progress;
  
  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

애니메이션 취소

rAF는 ID를 반환합니다. cancelAnimationFrame에 이 ID를 전달하면 애니메이션을 중단할 수 있습니다.

const animationId = requestAnimationFrame(animate);

// 조건에 따라 취소
if (someCondition) {
  cancelAnimationFrame(animationId);
}

성능 최적화 팁

DOM 조작 배치 처리: 여러 요소를 수정할 때 읽기와 쓰기 작업을 분리하여 레이아웃 스래싱을 방지합니다.

// 나쁜 예
elements.forEach(el => {
  el.style.left = el.offsetLeft + 10 + 'px'; // 읽고 쓰기 반복
});

// 좋은 예
const positions = elements.map(el => el.offsetLeft + 10); // 모두 읽기
elements.forEach((el, i) => {
  el.style.left = positions[i] + 'px'; // 모두 쓰기
});

transform과 opacity 활용: left, top 같은 레이아웃 속성 대신 transform과 opacity를 사용하면 GPU 가속을 받아 성능이 우수합니다.

// 나쁜 예 - 리플로우 유발
element.style.left = x + 'px';
element.style.top = y + 'px';

// 좋은 예 - GPU 가속
element.style.transform = `translate(${x}px, ${y}px)`;

복잡한 계산 최소화: 각 프레임마다 무거운 연산을 피하고, 필요하면 Web Worker에서 백그라운드 계산을 수행합니다.

실제 활용 사례

  • 스크롤 애니메이션: 페이지를 부드럽게 스크롤할 때
  • 로딩 애니메이션: 진행 바나 회전 애니메이션
  • 게임 루프: Canvas 기반 게임의 프레임 업데이트
  • UI 트랜지션: 모달, 드롭다운 등의 나타남과 사라짐
  • 실시간 데이터 시각화: 그래프나 차트 업데이트

정리

requestAnimationFrame은 브라우저의 최적화된 렌더링 사이클과 동기화되어 부드럽고 효율적인 JavaScript 애니메이션을 제공합니다. setTimeout 대신 rAF를 활용하면 더 나은 성능과 사용자 경험을 얻을 수 있습니다.