setTimeout과 requestAnimationFrame의 차이 - 이벤트 루프, 태스크 큐를 중심으로 정리하기

앤더손씨·2025년 6월 10일
0

👀 0. 작성의 계기

JavaScript를 오래 써왔지만, 솔직히 이벤트 루프, 그리고 마이크로태스크/매크로태스크 개념에 대해 제대로 정리해 본 적은 드물다. 특히, 나는 React 코드에서 스크롤의 이동과 같은 동작에 늘 비동기인 "setTimeout" 함수를 이벤트 루프에 넣어 후순위에 처리하는걸 확정하는 방식으로 사용하다가, 브라우저의 주사율이나 상황에 따라서 후순위로 실행하는 것이 실패하는(?!) 경험을 하고 나서 무언가 단단히 정리를 해야할 필요가 생겼다는 사실을 깨닫고 글을 적게 되었다.

✅ 1. 이벤트 루프와 태스크 큐의 기초 이해

우선 이벤트 루프를 설명하는 이유는 명확하다. 두 API 모두 "비동기적"으로 동작하지만, 언제 실행되는지가 다르기 때문이다.

JS는 단일 스레드 언어지만 비동기 처리를 위해 이벤트 루프(Event Loop)라는 구조를 채택한다. 이를 통해 다음 두 가지 큐를 중심으로 작업이 분배된다:

큐 종류설명예시
마이크로태스크 큐 (Microtask Queue)현재 실행 중인 작업이 끝나면 즉시 처리되는 우선순위 높은 큐Promise.then(), queueMicrotask()
매크로태스크 큐 (Macrotask Queue)이벤트 루프의 한 사이클 단위마다 하나씩 처리됨setTimeout(), setInterval(), requestAnimationFrame()

🎯 이벤트 루프 동작 순서

  1. Call Stack이 비면,
  2. Microtask Queue의 작업을 모두 처리
  3. 그 후 Macrotask Queue에서 하나를 꺼내 실행
  4. 위 과정을 반복
console.log('A');

setTimeout(() => console.log('setTimeout'), 0);

Promise.resolve().then(() => console.log('Promise'));

console.log('B');

실행 결과:

A
B
Promise
setTimeout

[!TIP]
여기서 오해하기 쉬운 점 하나! fetch()axios() 같은 네트워크 요청이 비동기니까 "매크로태스크"로 실행되겠다고 생각할 수 있다. 실제로 네트워크 요청은 JavaScript 엔진 자체가 처리하지 않고, 브라우저의 Web API 환경에서 비동기로 동작한다.
그 결과, 응답이 준비되었을 때 .then()에 등록된 콜백은 마이크로태스크 큐에 들어간다. 즉, 아래처럼 작성된 코드에서:

fetch('/api/data')
.then(() => console.log('fetch then'));

이 then() 콜백은 네트워크 요청 응답 도착 이후, 마치 Promise.resolve().then(...)처럼 마이크로태스크 큐에 들어가고, 다음 이벤트 루프에서 우선적으로 처리된다.
즉, 브라우저는 네트워크 요청을 백그라운드에서 처리 (JS 엔진과는 별도 스레드) 것이지 이벤트루프와는 무관하다.


✅ 3. 왜 마이크로태스크보다 매크로태스크가 늦게 실행되는가?

이건 브라우저의 실행 우선순위 정책 때문이다. 마이크로태스크는 실행 순서상 "더 빨리" 실행되며, 콜스택이 비워지면 바로 처리된다.

그에 비해 매크로태스크는 이벤트 루프의 사이클 단위로 한 번에 하나씩 실행되며, 매 루프마다 마이크로태스크 큐가 먼저 모두 비워지고 나서야 실행된다.

그래서 Promise.thensetTimeout(fn, 0)보다 항상 먼저 실행된다.


✅ 4. setTimeout vs requestAnimationFrame 비교표

항목setTimeout(fn, 0)requestAnimationFrame(fn)
실행 시점콜 스택이 비워진 후, 다음 이벤트 루프에서 실행브라우저의 다음 리페인트 직전에 실행
콜백 성격일반적인 타이머 기반 비동기 콜백렌더링 타이밍 최적화용 콜백
순서 제어여러 개가 있으면 순차적으로 실행되지만, 렌더링과 무관함layout/render가 끝난 후 정확한 타이밍에 실행
사용 타겟데이터 비동기 처리, 지연 실행스크롤, 애니메이션, DOM 위치 계산 등 렌더 이후 작업

✅ 5. 실제 사례 비교 - 스크롤 조작 시점

scrollContainer.scrollTop = 300;

❌ 잘못된 예 (렌더 전에 실행될 수도 있음)

setTimeout(() => {
  scrollContainer.scrollTop = 300;
}, 0);

✅ 정확한 예 (렌더 완료 후 실행 보장)

requestAnimationFrame(() => {
  scrollContainer.scrollTop = 300;
});

이처럼 requestAnimationFrame은 렌더링 이후, 즉 layout 계산이 완료된 시점에 실행되므로 DOM 상태에 의존한 조작에 훨씬 안정적이다.


✅ 6. requestAnimationFrame의 동작 방식

rAF는 브라우저가 화면을 그리기 직전에 콜백을 실행할 수 있도록 예약해주는 API다.

function animate() {
  updateDOM();
  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

이처럼 애니메이션 루프를 만들면, 60fps 기준 16.67ms 단위로 최적 타이밍에 호출되며 GPU와도 자연스럽게 동기화된다. DOM 리렌더링과 완벽하게 맞물리는 이 시점은 DOM 크기 계산이나 좌표 측정, 애니메이션 프레임 동기화에 매우 적합하다.


✅ 7. 최종 요약

항목setTimeout(fn, 0)requestAnimationFrame(fn)
실행 시점다음 이벤트 루프다음 리페인트 직전
우선순위낮음 (매크로태스크)높음 (렌더링 직전)
DOM 조작 신뢰성낮음높음
적합한 용도네트워크 요청 후 지연 처리렌더 후 위치 계산, 애니메이션 등

렌더링 타이밍과 DOM 상태를 고려한 비동기 제어가 필요한 상황이라면, requestAnimationFrame은 단순한 대안이 아니라 거의 필수에 가깝다(확실한 후순위 처리 로직의 보장).

특히 Next.js, React처럼 렌더 타이밍에 민감한 환경에선 이 차이를 알고 쓰는 것만으로도 꽤 많은 문제를 사전에 예방할 수 있다.

profile
자라나라 프론트엔드 개발새싹!

0개의 댓글