내가 하고있는 프로젝트는 타이머가 굉장히 중요하다..! 0초라는 타이밍에 많은 로직과 api 전송이 이뤄지는데 타이머가 느려지는 이슈가 있었다. 🤔 근데 이해가 안가는 것은 타이머는 버벅거리는데 왜 CSS animation은 정말 잘 움직이는 걸까? .. why..
그래서 기존 타이머의 문제점을 공부하면서 타이머 코어 로직을 다 바꿔버렸다. 🚀
자바스크립트 동작원리 (Stack, Queue, event loop)

일단 setInterval() 과 setTimeout()가 느려지는 이유를 알기 위해서는 자바스크립트의 동작원리를 알아야한다.
웹 브라우저 내부에는 스택이라는 공간이 있다. 우리가 작성한 코드 한줄한줄 이 요 스택에 들어가고 코드를 실행시켜 주는 곳이다. 그리고 그 옆에 힙이라는 공간이 있는데 여기선 변수가 저장되어 있다.
스택은 하나밖에 없다. 그렇기 때문에 자바스크립트는 싱글스레드언어(== 한번에 코드 한줄 밖에 실행 못함) 라고 하는것이다 !
하지만 setTimeout나 ajax 요청 ,이벤트 리스터 같은 코드는 바로 실행할 수 없다. 그래서 대기실(Web API)에 잠깐 있다가 큐라는 대기실로 순차적으로 들어간다. 그리고 처리가 완료된 코드들이 스택으로 하나씩 올려보낸다.
이렇게 대기실을 거치는 이유는 스택은 바쁜 공간이기 때문이다. 스택이 무작정 바쁘게 되는 것을 방지하고, 비동기 작업을 처리할 때 원활한 동작을 보장한다. 하지만 조건이 있다. 스택이 텅 비었을 때만 큐에 올려 보낸다.
따라서 스택과 큐를 너무 바쁘게 만들면 안된다! 브라우저가 느리거나 버벅이는 현상이 나타나게 된다.
setInterval() 와 setTimeout()은 시간을 보장하지 않는다.
나는 setInterval로 1초마다 타이머를 실행시켰다. 그럼 1초마다 함수 또는 코드 블록이 큐에 추가되고 스택이 비어 있을 때 실행되면서 주기적으로 작업이 반복 실행되게 된다.
이때, 이전 실행이 아직 완료되지 않아 스택(Stack)이 비어 있지 않다면, 다음 실행은 큐(Queue)에 추가되지만 스택이 비어질 때까지 대기하게 된다. 이때 타이머가 느려지게 되는것!
setInterval() 와 setTimeout()은 프레임을 신경쓰지 않는다.
우리가 영화나 애니메이션을 보는 것은 사실 짧은 시간 간격에 이어지는 장면을 보는 것이다. 이때 각각의 장면을 frame이라고 한다. setInterval() 와 setTimeout() 의 문제점은 주어진 시간내에 동작을 할 뿐 프레임을 신경 쓰지 않고 동작한다.
브라우저가 다른 작업 수행으로 인해 지연되어버리면 브라우저 렌더링 단계인 레이아웃 - 페인트 - 합성 과정이 다시 일어나게 된다. 이로 인해 프레임이 지연되나서 생성되지 못하고 누락되어 버려 1 프레임씩 깎이는 현상이 나타나게 된다.
프레임이 빠르게 생성되어야 웹 페이지가 부드럽게 보이는데 프레임이 깎여 결국 화면이 버벅이게 된다. 이러한 현상이 일어날 수 있는 이유는 자바스크립트의 콜 스택(call stack)은 싱글 스레드 이기 때문이다
이러한 지연(delay) 발생 문제 때문에 대안으로 탄생한 것이 바로 rAF(requestAnimationFrame) 이다.
requestAnimationFrame
requestAnimationFrame 함수는 setInterval() 함수에서 언제 호출할지 설정하는 것과는 달리 브라우저에서 시스템이 프레임을 그릴 준비가 되면 애니메이션 프레임을 호출하여 애니메이션 웹페이지를 보다 원활하고 효율적으로 생성할 수 있도록 해준다.
실제 화면이 갱신되어 표시되는 주기에 따라 함수를 호출해주기 때문에 자바스크립트가 프레임 시작 시 실행되도록 보장해주어 위와 같은 밀림 현상을 방지해준다.
requestAnimationFrame은 브라우저의 렌더링 주기에 맞춰 코드를 실행된다. 브라우저가 다음 프레임을 그리기 전에 코드를 실행하도록 예약하기 때문에 각 프레임이 정확히 16.6ms 간격으로 렌더링되게 되게 된다. 이로인해 각 프레임은 브라우저의 프레임 주기에 정확하게 맞게 된다.
setInterval에서는 준비가 되지 않아도 페인팅을 요청하는 것과 대비되면서, 보다 웹브라우저에 최적화 되어 있다고 말할 수 있다.
requestAnimationFrame을 활용한 타이머 코드 작성하기
const timerIdRef = useRef();
const [countdownSeconds, setCountdownSeconds] = useState();
//타이머 업데이트 함수
const updateTimer = (startTime, time) => {
const elapsedTime = Date.now() - startTime;
const remainingSeconds = Math.max(time - Math.floor(elapsedTime / 1000), 0);
setCountdownSeconds(remainingSeconds);
if (remainingSeconds > 0) {
timerIdRef.current = requestAnimationFrame(() =>
updateTimer(startTime, time),
);
}
};
//타이머 시작 함수
const startTimer = () => {
if (timerIdRef.current) {
cancelAnimationFrame(timerIdRef.current);
}
const startTime = Date.now();
updateTimer(startTime, countdownSeconds);
};
//타이머 정지 함수
const stopTimer = () => {
cancelAnimationFrame(timerIdRef.current);
};
//타이머 리셋 함수
const resetTimer = (time) => {
if (timerIdRef.current) {
cancelAnimationFrame(timerIdRef.current);
}
const startTime = Date.now();
updateTimer(startTime, time);
};
requestAnimationFrame로 타이머가 느려지는 이슈를 해결 할 수 있었다 😃
reference
https://inpa.tistory.com/entry/🌐-requestAnimationFrame-가이드#타이머함수의문제점