프론트엔드에서 본인 인증 페이지를 작업 중인데, 본인 인증 요청 후 브라우저에 표시되는 인증 만료 시간과 서버에서 체크하는 인증 만료 시간이 일치하지 않는 문제가 발생했습니다. 처음에는 서버 쪽 이슈라고 생각했지만, 테스트 결과 브라우저가 유휴 상태가 될 때 두 시간이 어긋나는 것을 확인할 수 있었습니다.
창을 최소화하거나 다른 탭으로 이동한 뒤 일정 시간이 지나면 브라우저가 유휴 상태로 진입하는데, 이때 JavaScript의 setInterval이 지연되는 것이 원인이었습니다.
setInterval 함수란?
특정 코드를 일정 시간 간격으로 반복 실행할 때 사용하는 함수입니다. setTimeout과의 차이점은 한 번만 실행하느냐, 반복 실행하느냐입니다.
setInterval은 첫 번째 인자로 실행할 함수를, 두 번째 인자로 반복 주기(ms)를 받으며, 반환 값으로 id를 돌려줍니다.
// 1000ms마다 "안녕하세요." 라는 문구를 출력한다.
setInterval(() => {
console.log("안녕하세요.");
}, 1000);반복을 종료하려면 clearInterval에 setInterval의 반환 값(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로 구현한 샘플 코드를 아래에 남겨두겠습니다.
샘플 코드
참고 자료
Related Posts

Lighthouse 접근성 점수가 움직이지 않은 이유
Lighthouse 자동 검사가 잡는 영역과 잡지 않는 영역을 구분하고, 마크업 4건을 정리해 접근성 점수를 84점에서 95점까지 올린 과정을 다룹니다.

Vanilla JS 댓글 모듈 자체 개발기
프레임워크 독립적인 Vanilla JS 댓글 모듈을 설계·개발하고 기존에 서비스 중이던 Nuxt 3 프로젝트에 통합한 과정을 다룹니다.

Next.js GitHub Pages 배포 트러블슈팅: export, basePath, configure-pages
GitHub Pages에 Next.js를 정적 export로 배포하면서 겪은 문제점과 디버그/해결 과정을 정리했습니다.