[브라우저 API] 쏟아지는 스크롤이벤트를 어떻게 감당하나요? requestAnimationFrame !!

2025. 4. 27. 00:44·과거의 이력/프론트엔드

requestAnimationFrame이 뭐지?

requestAnimationFrame은 웹 개발에서 애니메이션을 더 부드럽게 만들기 위해 자주 쓰이는 브라우저 API야.

requestAnimationFrame은 브라우저에게 다음 "화면 갱신" 때 호출될 함수를 예약하는 함수야.

즉,

"브라우저가 화면을 그릴 준비가 됐을 때 콜백을 실행해줄게!"
라고 예약하는 거야.


즉, 애니메이션이나 화면 갱신이 최적화되도록 도와주는 API인것.

 

반면에, 예를 들어 scroll 이벤트를 직접 처리하거나, mousemove 같은 걸 직접 받으면?

  • 사용자가 마우스를 엄청 빠르게 움직이면
  • 스크롤하면
  • → 엄청나게 많은 이벤트가 초당 100번, 200번, 심지어 300번도 발생할 수 있어.

➡️ 이 많은 이벤트마다 바로바로 DOM 조작을 하면,

  • 매번 화면을 업데이트하려고 하고
  • 매번 layout/repaint 작업이 일어나고
  • 메인 쓰레드가 너무 바빠져서 브라우저가 버벅거리게 돼.

 

 

그래서, requestAnimationFrame을 쓰면 어떻게 되는가?

  1. mousemove, scroll, resize 등등 이벤트가 아무리 빠르게 와도
  2. 우리는 일단 값(예: 스크롤 위치, 마우스 위치)만 변수에 저장해두고
  3. requestAnimationFrame으로 다음 프레임 때 1번만 작업을 함.

👉 즉, 초당 최대 60번까지만 작업을 하고, 그 외의 과잉 호출을 무시하게 돼.

 

 

비유로 설명하면

  • 그냥 이벤트를 쓰면 → 오는 대로 바로바로 반응 (전화 100통 다 받는 느낌)
  • requestAnimationFrame을 쓰면 → "1초에 60번만 전화 받을게" 정해놓는 것

그래서 CPU 사용량을 줄이고, GPU도 효율적으로 쓰고,
결과적으로 부드러운 60fps를 유지할 수 있는 거야.

 

// 1. 직접 처리 (안좋음)
window.addEventListener('scroll', () => {
  updateHeaderPosition(window.scrollY); // 스크롤 이벤트마다 호출
});

// 2. rAF로 최적화 (좋음)
let latestScrollY = 0;
let ticking = false;

window.addEventListener('scroll', () => {
  latestScrollY = window.scrollY;
  
  if (!ticking) {
    requestAnimationFrame(() => {
      updateHeaderPosition(latestScrollY);
      ticking = false;
    });
    ticking = true;
  }
});

 

  • 스크롤 이벤트는 막 쏟아지지만,
  • updateHeaderPosition은 1프레임에 최대 1번만 호출돼.

요약
✅ rAF를 쓰면 → 초당 60번 이하로 작업 제한됨
✅ 과잉 호출 방지 + 부드러운 렌더링 유지
✅ 메인 쓰레드 부담 줄이기

 

 

 

 

왜 requestAnimationFrame을 쓰는 거야?

  • 기본적으로 브라우저는 화면을 60fps(1초에 60번)로 갱신하려고 해.
    → 그러니까, 1초에 16.6ms(1000ms ÷ 60fps)마다 화면을 다시 그려야 해.

  • 만약, 애니메이션이나 화면 업데이트를 직접 setTimeout이나 setInterval로 처리한다면, 그 주기가 브라우저의 렌더링 주기와 맞지 않아서 깜빡임이나 불규칙한 속도로 애니메이션이 진행될 수 있어.

  • requestAnimationFrame은 바로 이걸 해결해줘.
    → 브라우저가 화면을 그리기 전에 함수를 실행하므로 최적화된 방식으로 애니메이션을 실행할 수 있게 돼.

 

동작을 예시로 봐보자.

function animate() {
  // 애니메이션을 위한 작업 (예: 위치 변경, 스타일 업데이트 등)
  console.log('Animating...');

  // 다음 애니메이션 프레임을 요청
  requestAnimationFrame(animate);
}

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

 

animate() 함수는 화면을 그리기 전에 호출되고,
requestAnimationFrame(animate)는 애니메이션이 끝날 때마다 다시 호출돼서 반복적인 애니메이션을 할 수 있게 돼.

 

 

requestAnimationFrame의 장점

  1. 브라우저 최적화
    • requestAnimationFrame은 브라우저의 렌더링 주기와 동기화되므로, 화면을 부드럽게 그릴 수 있어.
  2. 자동적으로 fps맞추기
    • 사용자가 60hz를 사용하던 144hz를 사용하던 상관없이 사용자의 디스플레이 프레임에 맞게 애니메이션을 진행해.
  3. 브라우저가 비활성화될 때 호출하지 않음
    • 브라우저가 비활성화되거나 백그라운드에서 작동할 때는 requestAnimationFrame이 호출되지 않아, 불필요한 자원 낭비를 줄여줌.

 

실제 예시 1.

스크롤 이벤트는 엄청 자주 발생해서 직접 처리하면 버벅거릴 수 있어.
이걸 requestAnimationFrame으로 최적화할 수 있어.

let latestScrollY = 0;
let ticking = false;

window.addEventListener('scroll', () => {
  latestScrollY = window.scrollY;

  if (!ticking) {
    requestAnimationFrame(() => {
      // 스크롤 위치에 따라 무언가 작업
      document.getElementById('header').style.top = `${-latestScrollY}px`;

      ticking = false;
    });

    ticking = true;
  }
});
 

 

  • 스크롤 이벤트마다 바로 작업하지 않고,
  • 다음 프레임에 맞춰 작업해서 성능 저하를 막는 방법이야.

 

 

 

실제 예시 2.

브라우저가 실제로 몇 FPS로 돌고 있는지 확인하는 것도 할 수 있어.

let lastTime = performance.now();
let frameCount = 0;

function countFPS(now) {
  frameCount++;

  if (now - lastTime >= 1000) {
    console.log(`FPS: ${frameCount}`);
    frameCount = 0;
    lastTime = now;
  }

  requestAnimationFrame(countFPS);
}

requestAnimationFrame(countFPS);

 

  • 1초에 몇 번 그려지는지 직접 콘솔에 찍어볼 수 있어.
  • 개발자들이 FPS 모니터링할 때 종종 쓰는 방법이야.

 

 

 

실제 예시 3.

마우스를 움직일 때 DOM을 부드럽게 갱신할 수도 있어.

let mouseX = 0;
let mouseY = 0;
let isDragging = false;

document.addEventListener('mousedown', () => isDragging = true);
document.addEventListener('mouseup', () => isDragging = false);
document.addEventListener('mousemove', (event) => {
  mouseX = event.clientX;
  mouseY = event.clientY;
});

function render() {
  if (isDragging) {
    const box = document.getElementById('box');
    box.style.transform = `translate(${mouseX}px, ${mouseY}px)`;
  }

  requestAnimationFrame(render);
}

requestAnimationFrame(render);
 
  • 마우스를 움직일 때 바로바로 움직이는 것처럼 보이지만,
  • 실제로는 다음 화면 갱신 주기에 맞춰 움직여서 훨씬 부드러워.

 

 

✅ 질문:

"requestAnimationFrame 안에서 무거운 작업을 하면 어떻게 돼?"

정답:
프레임이 끊긴다!
즉, 60fps를 유지하려던 목표가 깨지고, 화면이 버벅거려.

 

왜 그런가?

  1. requestAnimationFrame은 브라우저가 화면을 그리기 직전에 콜백을 호출해줘.
  2. 그래서 짧고 빠른 작업만 해야 화면을 바로 그릴 수 있어.
  3. 그런데 무거운 작업(ex. for문 10000번 돌기, 무거운 계산, 복잡한 DOM 조작)을 해버리면
    • 메인 쓰레드를 점령해버리고
    • 브라우저가 "아직 일 안 끝났네?" 하면서
    • 화면을 그릴 타이밍을 놓쳐버려.
  4. 이러면 → 다음 프레임으로 밀려나고 → FPS가 떨어져.

 

비유로 설명하면

  • requestAnimationFrame은 "야, 이제 그림 그릴 시간이야!" 하는 신호야.
  • 그런데 너가 그림 그려야 할 시간에 갑자기 무거운 짐을 들어야 한다면?
  • 그림 못 그리고 땀만 흘리다 시간 초과되는 거야. 😂

 

 

코드 예시

1. 나쁜 예시 (무거운 작업이 들어간 경우)

function heavyTask() {
  // 엄청 무거운 연산
  for (let i = 0; i < 100000000; i++) {
    Math.sqrt(i);
  }
}

function animate() {
  heavyTask(); // 프레임마다 이걸 하니, 메인쓰레드가 막힘!
  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

➡️ 이러면 브라우저는 화면 그리기 전에 heavyTask 때문에 멈춰서,

  • 화면이 버벅
  • fps 10~20으로 떨어질 수도 있어.

 

 

2. 좋은 예시 (작업을 쪼개서 처리)

 
let i = 0;

function animate() {
  for (let j = 0; j < 1000; j++) {
    Math.sqrt(i);
    i++;
  }

  if (i < 100000000) {
    requestAnimationFrame(animate);
  }
}

requestAnimationFrame(animate);

 

  • 한 번에 1000개만 처리하고
  • 다음 프레임으로 넘겨서 천천히 처리해.
  • 이렇게 하면 매 프레임 짧게 짧게 처리하면서 부드럽게 돌아가!

 

 

경우 결과
rAF 안에서 무거운 작업 프레임 드랍, 끊김, 버벅거림
rAF 안에서 가벼운 작업 부드러운 60fps 유지 가능
무거운 작업이 필요할 땐? - 작업을 쪼개거나
- Web Worker 같은 백그라운드 쓰레드 사용

 

 

보너스

진짜 엄청 무거운 계산(ex. 영상 처리, AI 계산 등)은 아예 Web Worker로 빼버리기도 해!
Web Worker는 메인쓰레드와 별개로 백그라운드에서 돌아가는 자바스크립트야.

 

 

 

 

setInterval과 비교

setInterval(function animation() {
	// . . . style 변경등의 DOM 제어
}, 1000 / 60); // 1초에 60번 실행 (60 프레임)

 

JS를 통해 애니메이션을 줄 수 있는 쉬운 방법 중 하나는 setInterval을 사용하는 방법이고 위와 같은 방법으로 setInterval(callback, delay)를 통해 delay 마다 callback을 실행시켜 애니메이션을 줄 수 있다.

 

그런데 setInterval을 통한 애니메이션은 단점들이 있다.

 

- setInterval의 delay 옵션은 callback이 반복적으로 실행될 시간을 정해주는 것이지 애니메이션 프레임을 설정하는 것이 아니다.

- setInterval은 delay후의 실행을 보장하진 않는다.

 

  자바스크립트가 싱글 스레드에서 비동기를 지원하는 방식 때문인데 Event Loop는 Call Stack이 비는 시점에 Callback Queue의 원소를 Call Stack으로 옮기고 Call Stack에 들어가야 callback은 실행된다. 이때, delay후 Call Stack이 비어있지 않다면 callback은 바로 실행되지 않는다.

 

 추가적으로 위와 같은 callback은 Promise.then()에 우선순위가 밀려 실제 서비스에선 delay후 실행을 보장할 수 없다.

 

이런 이유로 같은 delay 값을 사용할 때, setInterval을 여러개 걸어놓는 것 보다 하나의 callback을 걸어 놓는게 효과적이다. (Call Stack에 옮길때 한번에 옮기게)

// Bad
setInterval(animation1, 200);
setInterval(animation2, 200);
setInterval(animation3, 200);
 
// Good
// 총 실행시간이 3 ~ 4ms를 넘지 않는 선에서 아래와 같이 묶는게 효과적
setInterval(() => {
	animation1();
    animation2();
    animation3();
}, 200);

 

requestAnimationFrame 는 이런 단점을 극복하는것 뿐만 아니라 장점도 많다.

 

추가적으로 setInterval은 delay만 지나면 repaint를 요청하지만 (callback으로 style변경시) requestAnimationFrame을 통한 callback은 다음 repaint가 진행되기 전에 애니메이션 업데이트를 요청한다. (렌더링 프로세스 고려)

 

  requestAnimationFrame을 사용하면 페이지가 비활성화인 상태일 때 repaint 작업이 멈춘다. 애니메이션이 일시중지 되므로 CPU 리소스를 낭비하지 않는다. setInterval로 애니메이션을 돌리면 clearInterval될 때 까지 그냥 백그라운드에서 계속 돈다.

 

  Animation Frames의 우선순위는  Microtask (Promise) 보다는 낮고, MacroTask 보다는 높다. Animation Queue(requestAnimationFrame)의 우선순위는 Task Queue(setInterval) 보단 높으니 다른 Web APIs의 콜백보단 먼저 실행되는게 보장된다.

 

 

예시 코드

const animate = ({timing, draw, duration}) => { 
    const start = performance.now();
    const animatationCallback = (time) => {
        const timeFraction = (time - start) / duration;
        const progress = timing(timeFraction);
 
        draw(progress);
 
        if (timeFraction < 1) { // 100% 안넘으면 재귀
            requestAnimationFrame(animatationCallback);
        }
    };
	// 실제 등록
    requestAnimationFrame(animatationCallback);
};
 
// 애니메이션 새부 설정
const timing = (timeFraction) => timeFraction; // transition-timing-function : linear
const draw = (progress) => {
    // ...DOM 제어 및 스타일링 로직
    Element.style.left = progress + 'px';
};
 
// 등록
animate({
  timing,
  draw,
  duration: 1000,
});

 

 

https://web.dev/articles/optimize-javascript-execution?hl=ko

 

자바스크립트 실행 최적화  |  Articles  |  web.dev

JavaScript는 종종 시각적 변화를 유발합니다. 어떤 경우에는 스타일 조작을 통해 직접 구현되거나, 어떤 경우에는 데이터 검색 또는 정렬과 같은 시각적 변화를 일으키는 계산을 통해 구현됩니다.

web.dev

 

 

https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame

 

Window: requestAnimationFrame() method - Web APIs | MDN

The window.requestAnimationFrame() method tells the browser you wish to perform an animation. It requests the browser to call a user-supplied callback function before the next repaint.

developer.mozilla.org

 

 

https://www.devdic.com/javascript/reference/window/method:2249/requestAnimationFrame()

 

requestAnimationFrame()<DevDic for WEB Development>

requestAnimationFrame() 개요 웹브라우저에게 수행하기를 원하는 애니메이션을 알리고 다음 리페인트(repaint)가 진행되기 전에 해당 애니메이션을 업데이트하는 지정된 함수를 호출한다. 설명 requestAni

www.devdic.com

 

https://jaeano.tistory.com/entry/Javascript-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-requestAnimationFrame

 

[Javascript] 자바스크립트 애니메이션, requestAnimationFrame

개요[1] 브라우저 렌더링[2] 애니메이션[3] requestAnimationFrame[4] requestAnimationFrame 사용법[5] reference[1] 브라우저 렌더링  우선 브라우저 렌더링에 대해 간략하게 알아야한다.아래의 순서대로 순차적

jaeano.tistory.com

 

'과거의 이력 > 프론트엔드' 카테고리의 다른 글

브라우저 렌더링  (0) 2025.04.27
[ERROR] domexception blocked a frame with origin from accessing a cross-origin frame  (0) 2022.11.07
package-lock.json 이란 무엇일까?  (0) 2022.04.26
'과거의 이력/프론트엔드' 카테고리의 다른 글
  • 브라우저 렌더링
  • [ERROR] domexception blocked a frame with origin from accessing a cross-origin frame
  • package-lock.json 이란 무엇일까?
정많이 정만이
정많이 정만이
jeongmany
  • 정많이 정만이
    정많이 정만이
    정많이 정만이
  • 전체
    오늘
    어제
    • 분류 전체보기 (80)
      • 과거의 이력 (71)
        • CS (12)
        • 프론트엔드 (4)
        • javascript (21)
        • Vue.js (7)
        • bootstrap (1)
        • [그리드] ag-grid (3)
        • [그리드] vue-grid-layout (1)
        • HTML_CSS (5)
        • NPM (1)
        • [차트]highcharts (0)
        • JAVA (9)
        • 백엔드 (1)
        • 기본개념 (손필기) (5)
        • 프로그래머스 (1)
      • 알고리즘 (6)
      • 통계 (9)
        • 통계지식 (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    vue
    vue.js
    ubuntu설치
    ES6
    반복문
    vuejs
    JavaScript
    ubuntu
    aggrid
    ag-grid
    vue.config.js
    자바스크립트
    js map
    개발자
    VirtualBox
    java
    객체
    webpack.config.js
    js
    알고리즘
    공유메모리
    우분투
    bootstrap
    HTML
    cs
    selectbox
    CSS
    코딩테스트
    버추얼박스
    Webpack
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
정많이 정만이
[브라우저 API] 쏟아지는 스크롤이벤트를 어떻게 감당하나요? requestAnimationFrame !!
상단으로

티스토리툴바