현재 진행하는 서비스에 새로운 기능을 도입하면서 서버로 부터 세션 종료 시간을 받아오면 남은 시간을 화면에 보여줘야 하는 카운트다운 기능을 새롭게 만들어야 했다.
이런 식으로 남은 분과 초가 표시되게 할 것이다.
setInterval()
을 이용해 카운트타운을 구현하니 카운트 다운이 되긴 하지만 매초 버벅거리며 변경됐다.
아래는 처음 구현했던 코드이다.
const setCountDown = (targetDate: Date): string[] => {
const countDownTime = new Date(targetDate).getTime() - new Date().getTime();
const interval = useRef<NodeJS.Timeout | null>(null);
const [countDown, setCountDown] = useState(countDownTime);
useEffect(() => {
interval.current = setInterval(() => {
setCountDown((prevCountDown) => {
if (prevCountDown > 0) {
return prevCountDown - 1000;
} else {
clearInterval(interval.current!);
return 0;
}
});
}, 1000);
return () => {
if (interval.current !== null) clearInterval(interval.current);
};
}, [countDown]);
return returnCountTime(countDown);
};
setTimeout
/setInterval
로 구현한 애니메이션의 문제점setTimeout
과 setInterval
은 js에서 애니메이션을 구현할 때 자주 사용되는 타이머 함수이다. 하지만 이 두 방법을 사용한 애니메이션은 몇 가지 문제점을 가진다.
setTimeout
,setInterval
은 브라우저의 프레임 생성 시점에 콜백을 실행할 수 없다. 브라우저는 보통 60fps (1초당 60개 이상의 프레임이 그려지는 것)로 동작하며, 이는 한 프레임이 약 16.7ms 안에 그려져야 함을 의미한다. 하지만, 타이머 함수는 정확히 이 간격을 맞출 수 없다.
이 타이머 함수들은 브라우저의 다른 작업들 (layout→ paint → composite)과 맞물려 실행된다. 이러한 작업들이 16.7ms보다 오래 걸리면, 타이머 콜백이 지연될 수 있다. 이는 프레임이 지연되거나 유실되는 원인이 된다.
setTimeout
과 setInterval
모두 주어진 시간의 정확한 지연을 보장하지 않는다. 특히 setInterval
의 경우, 주어진 간격으로 콜백 함수를 호출하지만 실행 간격을 보장하지는 않는다.
이는 콜백 함수가 실행될 때마다 이벤트 루프에서 실행 대기열에 추가되는 것만 보장할 뿐, 이벤트 루프의 다른 작업들로 인해 콜백 함수가 제시간에 실행되지 못할 수 있다.
setTimeout
과 setInterval
은 프레임을 신경쓰지 않고 동작한다. 이는 애니메이션이 일정한 간격으로 코드가 실행되는 것이 아니라, 이벤트 루프에서 실행 대기열에 추가된 시점부터 일정 시간이 지난 후에 실행되는 것을 의미한다.
타이머 함수를 사용한 애니메이션은 이 프레임 생성 시점과 동기화되지 않기 때문에 매끄러운 애니메이션을 보장하기 어렵다.
requestAnimationFrame
사용requestAnimationFrame(callback)
requestAnimationFrame
는 다음 리페인트를 위해 애니메이션을 업데이트는 할 때 호출할 함수이다.
여러 콜백이 단일 프레임에서 실행되기 시작하면 이전 콜백의 작업 부하를 계산하는 동안 시간이 지났더라도 각 콜백은 동일한 타임스탬프를 받는다. requestAnimationFrame
은 브라우저가 프레임을 렌더링할 준비가 되었을 때 프레임 생성 시점마다 콜백을 실행시켜 프레임 유실을 방지할 수 있고, 16.7ms 내에 렌더링을 완료하는 것을 더 보장할 수 있다.
requestAnimationFrame
를 이용해 수정한 코드는 다음과 같다.
useEffect(() => {
const updateCountDown = () => {
const currentTime = Date.now();
const timeElapsed = currentTime - lastUpdatedTime.current;
lastUpdatedTime.current = currentTime;
setCountDown((prevCountDown) => {
if (prevCountDown > 0) {
return Math.max(prevCountDown - timeElapsed, 0);
} else {
return 0;
}
});
requestAnimationFrame(updateCountDown);
};
updateCountDown();
return () => {
lastUpdatedTime.current = Date.now();
};
}, []);
카운트 다운을 이와 같이 부드럽게 구현할 수 있었다.