requestAnimationFrame vs setTimeout — 부드러운 렌더링의 비밀

요즘 웹에서 애니메이션이나 스크롤 효과를 구현하다 보면, setTimeout 대신 requestAnimationFrame을 쓰라는 이야기를 자주 듣죠.
저도 처음엔 “둘 다 타이머 아니야?”라고 생각했는데, 직접 실험해보니 차이가 꽤 크더라고요.
오늘은 그 차이를 조금 더 체계적으로, 하지만 부드럽게 이해할 수 있게 정리해볼게요.
💡 기본 개념부터 간단히
| 비교 항목 | setTimeout | requestAnimationFrame |
|---|---|---|
| 호출 주기 | 지정한 시간(ms)마다 | 브라우저 화면 갱신 시점마다 |
| 실행 타이밍 | 일정하지 않음 (환경에 따라 지연 가능) | 디스플레이 리프레시 주기(보통 16.6ms)에 맞춰 실행 |
| 목적 | 일반적인 타이머 기능 | 화면 렌더링 최적화용 콜백 |
| CPU 효율 | 낮음 (백그라운드에서도 실행) | 높음 (탭 비활성 시 자동 중지) |
즉, requestAnimationFrame은 단순히 “더 빠른 타이머”가 아니라
**“브라우저 렌더링 사이클에 맞춰 작동하는 똑똑한 타이머”**예요.
🧪 직접 실험해보기
이 실험은 실제로 “얼마나 부드럽게 움직이는가”를 눈으로 확인할 수 있어요.
HTML 파일 하나만 만들어서 테스트해보면 됩니다.
<div id="box" style="width:50px;height:50px;background:skyblue;position:absolute;"></div>
<script>
const box = document.getElementById('box')
let pos = 0
// setTimeout 버전
function moveWithTimeout() {
pos += 2
box.style.left = pos + 'px'
if (pos < 500) setTimeout(moveWithTimeout, 16)
}
// requestAnimationFrame 버전
function moveWithRAF() {
pos += 2
box.style.left = pos + 'px'
if (pos < 500) requestAnimationFrame(moveWithRAF)
}
// 버튼으로 실행 선택
const btn1 = document.createElement('button')
btn1.textContent = 'setTimeout 실행'
btn1.onclick = moveWithTimeout
const btn2 = document.createElement('button')
btn2.textContent = 'requestAnimationFrame 실행'
btn2.onclick = moveWithRAF
document.body.append(btn1, btn2)
</script>🔍 직접 실행해보면 다음 차이를 바로 느낄 수 있어요.
| 항목 | setTimeout | requestAnimationFrame |
|---|---|---|
| 움직임 | 약간 끊김 있음 | 매우 부드러움 |
| CPU 점유율 | 높음 | 상대적으로 낮음 |
| 탭 비활성 시 | 계속 실행 | 일시 정지 |
| 모니터 주사율 대응 | X | ✅ (60Hz 기준 16.6ms 주기) |
⚙️ 왜 이런 차이가 날까?
브라우저는 내부적으로 **렌더링 루프(Render Loop)**를 가지고 있어요.
보통 초당 60프레임(16.6ms마다 한 번)씩 화면을 다시 그리죠.
이때 requestAnimationFrame은 이 루프에 “그림을 그리기 직전”에 콜백을 등록합니다.
반면 setTimeout은 그 루프를 무시하고 별도로 실행돼요.
그래서 프레임 사이에 불규칙하게 끼어들면서 Reflow/Repaint 타이밍과 어긋나는 현상이 발생하죠.
쉽게 말하면,
setTimeout은 “브라우저가 숨 쉬는 타이밍”을 무시하고 움직이는 반면,
requestAnimationFrame은 “브라우저의 호흡에 맞춰” 자연스럽게 움직입니다.
🧠 실무에서의 차이점
1️⃣ 스크롤 또는 드래그 애니메이션
// ❌ setTimeout 방식
window.addEventListener('scroll', () => {
setTimeout(() => {
console.log(window.scrollY)
}, 16)
})
// ✅ requestAnimationFrame 방식
let ticking = false
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
console.log(window.scrollY)
ticking = false
})
ticking = true
}
})requestAnimationFrame은 브라우저가 실제로 화면을 다시 그리는 시점에 맞춰 콜백을 실행하므로,
스크롤 이벤트처럼 자주 발생하는 동작에서도 부드럽고 효율적이에요.
2️⃣ React에서의 애니메이션 최적화
React에서도 상태 업데이트를 너무 자주 하면 리렌더링이 몰리면서 끊김이 생길 수 있죠.
이때 requestAnimationFrame을 이용하면 UI 업데이트 타이밍을 조절할 수 있습니다.
function SmoothCounter() {
const [count, setCount] = useState(0)
const handleIncrease = () => {
let current = count
const animate = () => {
current += 1
setCount(current)
if (current < 100) requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
}
return <button onClick={handleIncrease}>Count: {count}</button>
}이 방식은 실제로 UI를 ‘프레임 단위’로 부드럽게 변화시킵니다.
setTimeout으로 동일한 걸 하면, 프레임과 타이밍이 어긋나서 버벅임이 생겨요.
3️⃣ Canvas / Chart / 게임 루프
게임이나 실시간 차트 같은 경우에는 requestAnimationFrame이 사실상 필수예요.
function render() {
drawChartData() // 실제 그리기
requestAnimationFrame(render) // 프레임마다 갱신
}
render()이런 방식은 GPU가 그리는 타이밍과도 맞물리기 때문에,
전반적인 렌더링 효율이 훨씬 높습니다.
🔍 Chrome DevTools로 성능 확인하기
직접 비교하고 싶다면 Performance 탭에서 두 버전을 실행해보세요.
setTimeout: 호출 간격이 불규칙하고, 프레임 드랍 구간이 생김requestAnimationFrame: 호출 간격이 일정하고, Reflow/Repaint 패턴이 균일
초당 프레임(FPS) 기준으로 보면,
setTimeout은 40~50FPS 정도로 흔들리지만,
requestAnimationFrame은 거의 60FPS를 유지합니다.
⚡ 결론 정리
| 항목 | setTimeout | requestAnimationFrame |
|---|---|---|
| 동작 방식 | 단순 타이머 | 브라우저 렌더 루프와 동기화 |
| 애니메이션 부드러움 | 중간 | 매우 부드러움 |
| CPU 효율 | 낮음 | 높음 |
| 탭 비활성 시 | 계속 동작 | 자동 일시정지 |
| 실무 사용 예시 | 단순 알람, 대기 타이머 | 스크롤, 애니메이션, 차트, React UI |
💬 마무리하며
둘 다 비슷한 타이머처럼 보이지만, 목적이 완전히 달라요.
setTimeout은 “정해진 시간 뒤에 실행”이 목적이고,
requestAnimationFrame은 “렌더링 타이밍에 맞춰 실행”하는 최적화 도구예요.
React, Canvas, SVG, 차트, 스크롤 등 시각적 변화가 있는 작업이라면
requestAnimationFrame을 쓰는 게 거의 정답입니다.
부드러운 UI는 결국 “타이밍 싸움”이에요.
브라우저가 숨 쉬는 템포에 맞추면, 생각보다 훨씬 자연스러운 사용자 경험을 만들 수 있습니다. 😌
#requestAnimationFrame #setTimeout #렌더링성능 #웹애니메이션 #브라우저렌더링 #React성능 #FPS #프론트엔드최적화 #JavaScript