⚠️ 문제 발생
웹 서비스 개발 중 refreshToken 발급 후 일정 시간이 지나면 로그인 연장 모달을 띄우는 기능이 있다. 이때 '연장' 버튼을 누르면 이미 만료된 token으로 재발급했다는 에러를 서버에서 전달 받으며 재발급 요청에 실패하게 된다.
📃 현재 코드 분석
1. 첫 refreshToken 발급 성공 시 (만료시간 - 카운트 모달 노출 시간 - 여유 시간 - 현재 시간)ms 뒤 setTimeout을 실시한다.
// scheduleRefreshTokenAndModalAlert.ts
const timeOutTime = expireTime - sessionTime - bufferTime - currentTime;
const timeoutId = setTimeout(() => {
useScheduleTokenStore.getState().setIsOpenExpireSessionModal(true);
return true;
}, timeOutTime);
2. setTimeout 의 콜백함수가 발동되면 ExpireSessionModal이 렌더링되고 내부에서 바로 useEffect가 실행되어 미리 계산한 카운트 타임(만료시간 - 여유 시간 - 현재 시간) 을 setInterval로 1초씩 차감한다.
// ExpireSessionModal.tsx
const countTime = Math.floor(
(expireTime - bufferTime - currentTime) / SECOND
);
// 만료시간 기준 countdown
const [count, setCount] = useState(countTime);
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount((prevCount) => prevCount - 1);
}, 1000);
// 언마운트시 clearInterval, 모달 닫기
return () => {
resetModal();
};
}, []);
currentTime을 제외한 모든 시간은 고정된 값이다.
1. expireTime : 로컬스토리지에 저장된 token을 디코딩하여 상수에 할당한 값이다.
2. sessionTime / bufferTime : .env에 저장되어 있는 고정 값이다.
📍 문제 발생 원인
현재 세션 만료 모달에서는 위에서 설정한 카운트 시간를 렌더링시 계상하여 기준으로 잡고 타이머를 설정하여 카운트 다운을 실행한다.
문제는 사용자가 로그인 후 모달이 뜨기 전에 다른 브라우저 탭으로 이동한 상태에서 모달이 뜨는 시점 이후, 다시 해당 탭으로 돌아왔을 때 발생한다. 이때 브라우저는 비활성화된 탭에서의 setInterval 등 타이머 함수의 실행을 제한(throtting)하기 때문에, 타이머가 정확하게 작동하지 았는다.
throtting
throttling이란 브라우저가 자체적으로 판단했을 때 비활성화 된 탭에서 반복적으로 발생하는 이벤트가 있다면 그 이벤트에 delay등을 줘서 결국 해당 이벤트를 무시하고 메모리 자원을 다른 곳에 쓰는 등 메모리를 효율적으로 관리하는 방법이다.
이로 인해 사용자는 모달에 표시된 시간이 아직 남아 있는 것처럼 보이지만, 실제로는 이미 refreshToken이 만료된 상태이다.
✅ 문제 해결
웹 워커 사용
보통 브라우저에서 타이머를 제한하는 문제를 해결하기 위해서 웹 워커를 자주 사용한다. 웹 워커는 브라우저의 메인 실행 스레드와 분리된 백그라운드 스레드에서 자바스크립트를 실행할 수 있어 멀티스레딩의 장점을 제공하며, 타이머 제한 문제도 우회할 수 있다.
웹 워커의 한계
- 단순한 타이머 로직이라면 웹 워커를 사용하는 것이 오히려 불필요한 리소스 사용으로 이어질 수 있다.
- 웹 워커는 DOM 접근이 불가능 하며, setInterval, setTimeout같은 브라우저 API를 직접 사용할 수 없다.
- 타이머를 구현하려면 postMessage로 메인 스레드와 통신하며 직접 로직을 구성해야 하므로 복잡하다.
HackTimer
이러한 복잡함을 줄이기 위해 만들어진 웹 워커 기반의 타이머 라이브러리인 HackTimer이다.
- setInterval, setTimeout, clearInterval, clearTimeout 등을 재정의하여 Web Worker 기반으로 실행한다.
- 탭이 비활성화 되어도 타이머가 정확하게 동작한다.
- 애플리케이션의 최상단에서 import만 해주면 자동 적용된다.
- 내부적으로 즉시 실행 함수(IIFE)를 사용하여 전역 환경에 타이머를 덮어씌운다.
- 사용자는 기존의 타이머 API를 그대로 사용할 수 있어 React 등 프레임워크와도 완벽하게 호환한다.
<사용법>
1. HackTimer 설치
pnpm install hacktimer
2. 애플리케이션 최상위에 hackTimer import
// App.tsx
import 'hacktimer';
<참조>
Chrome 88부터 체인으로 연결된 JS 타이머의 과도한 제한
How t o Prevent the Timers from Stopping Due to Browser Tab Inactivity in JavaScript
댓글