개발자
류준열
setTimeout의 한계와 reqeustAnimationFrame
타이머 구현
setTimeout으로 타이머를 걸어서 1분 후 모달이 뜨는 기능을 만들었다. 그런데 조금 찾아보니 setTimeout은 call Stack이 비는것을 대기하다가 다른 태스크에 의해 순서가 밀려 타이밍을 못맞추게 되는 경우가 있다고 한다.
setTimeout이 타이밍을 못맞추는 것과 이를 해결하기 위한 방법을 찾아보았다.
setTimeout이 타이밍을 못맞추는 경우
싱글스레드인 JS에서는 setTimeout을 비동기로 실행시키기 위해 WebAPI를 이용한다.
- Call Stack에 setTimeout이 들어오면 Web API에 setTimeout을 위임하고
- n초후 Web API는 Queue로 setTimeout을 보내고
- Call Stack이 모두 완료되기만을 기다린다.
- 그 이후 Call Stack이 비었을때 Task Queue에서 대기하던 setTimeout을 처리한다.
이때 3~4 과정에서 n초가 지났는데도 Call Stack에 무언가 남아있으면 setTimeout은 Queue에서 계속 대기하게 되면서 타이밍을 놓칠 수 있다. (참고)
즉, 렌더링 타이밍을 setTimeout으로 맞출 경우, 개발자의 의도와 다르게 작동할 수 있다.
requestAnimationFrame
requestAnimationFrame은 시스템이 프레임을 그릴 준비가 되었을때 호출되는데 원래 목적은 효율적인 애니메이션 구현이다.
requestAnimationFrame은 화면이 갱신되는 주기에 따라 repaint 전 호출되기 때문에 타이밍과 무관하게 프레임 시작시 실행되는것이 보장된다.
requestAnimationFrame도 비동기로 분류되어 이벤트 루프에 의해 처리된다. 하지만 setTimeout과 다르게 task queue가 아니라 Animation Frame이라는 별개의 queue에서 처리된다.
이벤트 루프의 3가지 Queue
이 글에 자세히 나와 있는데 각 Queue들을 요약하면 다음과 같다.
Microtask Queue
Promise, async/await 등을 처리한다. 우선순위가 가장 높다.
Animation Frames
requestAnimationFrame과 같이 브라우저 렌더링에 관련된 task를 처리한다.
Macrotask Queue(Task Queue)
우리가 흔이 아는 Task Queue이다. setTimeout, setInterval등을 처리한다.
이벤트 루프에서 Queue 처리 우선순위
이벤트 루프에서 큐 처리 우선순위는 Microtask Queue -> Animation Frame -> Task Queue 이다.
그리고 Microtask Queue, Animation Frames는 한번에 처리하지만, Macrotask Queue는 한번에 하나의 작업만 callstack으로 전달후 다른 Queue를 순회한다.
requestAnimationFrame의 작동방식 이해와 스크롤 최적화
아래 html처럼 rAF로 스크롤 이벤트를 구현했다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body style="height: 500vh">
ㅎㅇㅎㅇ
</body>
<script>
let ticking = false;
function onScroll() { // 1. Trigger Scroll
if (!ticking) {
window.requestAnimationFrame(function () { // 2.rAF -> Animation Frame에 저장
console.log("tick1", ticking); // 4. 리페인트전 실행
ticking = false; // 5. unblock
});
ticking = true; // 3. block
} else {
console.log("tick2", ticking); // block 되었을 때 log
}
}
window.addEventListener("scroll", onScroll);
</script>
</html>
- 사용자가 스크롤 내리면 onScroll 실행
- 비동기 Queue중 하나인 Animation Frame에 callback 저장후 리페인트 직전에 실행되도록 예약
- 곧바로 ticking = true로 변경하여 조건문에 진입 불가하도록 함
- 사용자가 스크롤을 빠르게 내려서 다음 리페인트 전에 스크롤 이벤트가 발생
- onScroll이 실행되지만 ticking이 true 이기 때문에 if문에 진입 못함
- repaint 직전에 실행되도록 예약했던 Animation Frame의 callback이 실행된다.
- ticking = false로 변경하여 조건문에 진입 가능해짐
- 렌더링 실행
참고
https://www.fronttigger.dev/2022/react/infinity-scroll
https://iborymagic.tistory.com/142
https://blog.naver.com/dndlab/221633637425
https://inpa.tistory.com/entry/%F0%9F%8C%90-requestAnimationFrame-%EA%B0%80%EC%9D%B4%EB%93%9C
https://ethansup.net/blog/deal-with-scroll-event