setInterval 함수와 타이머 이슈

커버 이미지

프론트엔드에서 본인 인증 페이지를 작업 중인데, 본인 인증 요청 후 브라우저에 표시되는 인증 만료 시간과 서버에서 체크하는 인증 만료 시간이 일치하지 않는 문제가 발생했습니다. 처음에는 서버 쪽 이슈라고 생각했지만, 테스트 결과 브라우저가 유휴 상태가 될 때 두 시간이 어긋나는 것을 확인할 수 있었습니다.

창을 최소화하거나 다른 탭으로 이동한 뒤 일정 시간이 지나면 브라우저가 유휴 상태로 진입하는데, 이때 JavaScriptsetInterval이 지연되는 것이 원인이었습니다.

setInterval 함수란?

특정 코드를 일정 시간 간격으로 반복 실행할 때 사용하는 함수입니다. setTimeout과의 차이점은 한 번만 실행하느냐, 반복 실행하느냐입니다.

setInterval은 첫 번째 인자로 실행할 함수를, 두 번째 인자로 반복 주기(ms)를 받으며, 반환 값으로 id를 돌려줍니다.

// 1000ms마다 "안녕하세요." 라는 문구를 출력한다.
setInterval(() => {
  console.log("안녕하세요.");
}, 1000);

반복을 종료하려면 clearIntervalsetInterval의 반환 값(id)을 넣어 호출하면 됩니다. 컴포넌트가 언마운트되거나 페이지 전환이 일어나는 등 더 이상 타이머가 필요하지 않은 경우에는 메모리 누수 방지를 위해 clearInterval을 반드시 호출해주셔야 합니다.

let intervalId = 0;
 
// id값을 별도의 변수에 저장
intervalId = setInterval(() => {
  console.log("안녕하세요.");
}, 1000);
 
...
 
// 저장한 id값을 인자로 넣어서 함수를 호출한다.
clearInterval(intervalId);
 

해결 과정

해당 문제를 보완하는 라이브러리도 있어 검토해봤지만, 브라우저가 유휴 상태일 때도 강제로 타이머를 돌리는 방식이라 메모리 누수 같은 사이드 이펙트가 발생할 가능성이 높았습니다. 따라서 별도 라이브러리를 사용하지 않고, 브라우저 탭이 다시 focus될 때마다 인증 만료 시간을 재계산하도록 구현하기로 했습니다.

let startTime = 0; // 타이머 시작 시간
let seconds = 0; // 브라우저에 표시할 시간
let intervalId = 0; // setInterval 함수의 반환 값을 저장할 변수
 
// 타이머 시작 함수
const startTimer = () => {
  startTime = Math.floor(Date.now() / 1000);
 
  // 현재 시간과 타이머 시작 시간을 비교합니다.
  seconds = Math.floor(Date.now() / 1000) - startTime.current;
 
  intervalId = setInterval(() => {
    setTimer((prev) => prev + 1);
  }, 1000);
};
 
// 타이머 종료 함수
const stopTimer = () => {
  clearInterval(intervalId);
  seconds = 0;
  intervalId = 0;
};

타이머 시작/종료 함수를 만든 뒤, 아래 코드처럼 브라우저가 focus될 때 사용할 이벤트를 등록합니다. 또한 페이지를 빠져나가는 시점에 등록된 이벤트를 해제해주셔야 합니다.

// 브라우저 focus in 이벤트 등록
window.addEventListener("focus", () => {
	clearInterval(intervalId);
	// 타이머 재시작
	startTimer();
});
 
...
 
// 페이지를 빠져나갈 때 등록된 이벤트를 제거한다.
window.removeEventListener("focus", null);

setInterval 대안 비교

setInterval의 한계를 알았으니, 비슷한 상황에서 쓸 수 있는 대안들도 정리해봤습니다.

setTimeout 재귀 호출

setInterval과 마찬가지로 탭 비활성화 시 쓰로틀링되지만, 콜백 실행이 끝난 뒤 다음 호출을 예약하기 때문에 콜백이 겹치는 문제가 없습니다. 1분 기준 누적 오차는 1~3초 정도로, 이전 작업 완료를 보장해야 할 때 유용합니다.

requestAnimationFrame

탭이 비활성화되면 호출 자체가 멈추므로 타이머 용도로는 적합하지 않습니다. 대신 시각적 애니메이션에는 브라우저 렌더링 사이클에 맞춰 가장 효율적으로 동작합니다.

Web Worker + setInterval

Web Worker 안에서 setInterval을 돌리면 메인 스레드의 쓰로틀링 영향을 받지 않아, 탭이 비활성화된 상태에서도 정상 실행됩니다. 1분 기준 누적 오차가 0.1~0.5초 수준이라 백그라운드에서도 정밀한 타이머가 필요하다면 사실상 유일한 선택입니다.

Date.now() 보정 방식

이번 프로젝트에서 채택한 방식입니다. setInterval 자체는 쓰로틀링되지만, 매 tick마다 Date.now()로 실제 경과 시간을 계산해 보정합니다. 1분 기준 오차가 0.01~0.1초 수준으로, 탭이 포커스된 상태에서의 시간 정확도가 중요할 때 충분합니다.

필요하신 분들을 위해 React로 구현한 샘플 코드를 아래에 남겨두겠습니다.

샘플 코드

github 저장소 이동

참고 자료