웹 서비스를 개발하다 보면 사용자가 브라우저 탭을 전환하거나 창을 최소화했음에도 백그라운드에서 실시간 API 요청(Polling)이나 타이머, 애니메이션이 계속 실행되는 상황을 경험해봤을 것이다.
다만 대부분의 개발자들은 탭의 활성화 상태를 감지하는 정확한 표준 기술을 모른 채 `window.onblur` 이벤트를 대충 쓰거나, 백그라운드에서 불필요하게 서버 리소스와 클라이언트 CPU를 고갈시키곤 한다.
이번에는 브라우저의 탭 활성화 상태를 정교하게 제어하는 Page Visibility API가 정확히 무엇인지, 왜 필요한지, 그리고 실무 동작 코드까지 완벽하게 정리해서 소개하겠다.
Page Visibility API는 현재 웹페이지가 사용자에게 실제로 보이는 상태(Visible)인지, 아니면 다른 탭에 가려지거나 최소화되어 숨겨진 상태(Hidden)인지를 자바스크립트로 감지할 수 있게 해주는 브라우저 표준 API다.
과거에는 탭 전환을 감지하기 위해 `window.onblur`나 `window.onfocus` 이벤트를 주로 사용했다. 하지만 이 이벤트들은 사용자가 개발자 도구를 클릭하거나 페이지 내부의 `iframe`을 클릭하는 등 단순 포커스 이동 시에도 발동하여 탭의 실제 노출 여부를 정확히 판단하지 못하는 한계가 있었다.
| 구분 | window.onblur / onfocus | Page Visibility API |
|---|---|---|
| 감지 대상 | 브라우저 창의 포커스 획득/상실 여부 | 실제 문서(Document)의 화면 노출 여부 |
| 오동작 위험 | 페이지 내 다른 요소(iframe, input) 클릭 시 발동됨 | 탭 전환, 창 최소화 시에만 정확히 발동됨 |
| 주요 속성 및 이벤트 | window.blur, window.focus | document.visibilityState, visibilitychange |
| 권장 사용처 | 단순 포커스 식별 (예: 입력 폼 활성화) | 백그라운드 통신 일시정지, 미디어 제어, 리소스 최적화 |
Page Visibility API에서 핵심이 되는 속성은 `document.visibilityState`와 `document.hidden`이며, 상태 변화는 `visibilitychange` 이벤트를 통해 수신한다.
- document.visibilityState: 페이지의 현재 노출 상태를 문자열로 반환한다. (`'visible'`, `'hidden'`)
- document.hidden: 페이지가 가려져 있으면 `true`, 보이고 있으면 `false`를 반환하는 불리언(Boolean) 값이다.
- visibilitychange 이벤트: 탭을 전환하거나 브라우저를 최소화/복원할 때 `document` 객체에서 발생하는 이벤트다.
실무에서 가장 흔히 접하는 상황은 일정 주기로 서버에 요청을 보내는 실시간 대시보드나 알림 시스템이다. 사용자가 탭을 보고 있지 않을 때도 요청을 계속 보내는 잘못된 코드와, Page Visibility API를 적용한 올바른 코드를 비교해보자.
✗ 잘못된 예: 탭을 이탈해도 setInterval이 멈추지 않아 서버와 클라이언트 리소스가 계속 소비된다.
// 탭 활성화 여부와 상관없이 3초마다 계속 서버 요청을 보냄
function startUnsafePolling() {
setInterval(() => {
console.log('서버로 API 요청 전송 중...');
// fetch('/api/live-data');
}, 3000);
}
startUnsafePolling();
✓ 올바른 예: 탭이 숨겨지면 타이머를 해제하고, 다시 탭으로 돌아왔을 때 안전하게 타이머를 재동작시킨다.
let timerId = null;
// API 요청 함수
function fetchLiveData() {
console.log('[API 통신] 실시간 데이터를 갱신합니다.');
}
// Polling 시작
function startPolling() {
if (!timerId) {
fetchLiveData(); // 즉시 1회 실행 후 주기적 요청
timerId = setInterval(fetchLiveData, 3000);
console.log('Polling이 시작되었습니다.');
}
}
// Polling 중지
function stopPolling() {
if (timerId) {
clearInterval(timerId);
timerId = null;
console.log('Polling이 일시 정지되었습니다.');
}
}
// Visibility Change 이벤트 리스너 등록
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
// 탭을 이탈하거나 최소화했을 때
stopPolling();
} else if (document.visibilityState === 'visible') {
// 다시 탭으로 돌아왔을 때
startPolling();
}
});
// 초기 실행
startPolling();
위의 올바른 코드를 실행한 후 탭을 이동했다가 다시 돌아왔을 때 브라우저 콘솔에 출력되는 결과는 다음과 같다.
[API 통신] 실시간 데이터를 갱신합니다.
Polling이 시작되었습니다.
[API 통신] 실시간 데이터를 갱신합니다.
(사용자가 다른 탭으로 이동)
Polling이 일시 정지되었습니다.
(다른 탭을 이용하는 동안 통신 중단)
(다시 원래 탭으로 복귀)
[API 통신] 실시간 데이터를 갱신합니다.
Polling이 시작되었습니다.
Page Visibility API를 실무에 도입할 때 자주 범하는 실수와 주의해야 할 포인트를 정리했다.
- 타이머 중복 생성 실수: `visibilityState === 'visible'`로 복귀할 때 기존 타이머 존재 여부를 확인하지 않고 `setInterval`을 호출하면 타이머가 중첩되어 메모리 누수가 발생한다. 반드시 기존 변수(`timerId`)를 검증 후 할당하자.
- 페이지 이탈 시 데이터 유실 방지: 페이지가 완전히 닫히는 시점에는 `visibilitychange` 이벤트 안에서 `fetch` 대신 `navigator.sendBeacon()`을 사용하는 것이 안전하다.
- 비디오/오디오 제어 시 유의점: 사용자가 음성을 듣기 위해 탭을 내리는 팟캐스트/음악 서비스의 경우, 단순히 `hidden` 상태라고 해서 미디어를 일시정지하면 사용자 경험을 해칠 수 있다. 비디오 요소에 한해 선택적으로 적용하는 지혜가 필요하다.
Page Visibility API는 불필요한 백그라운드 작업을 스마트하게 제어하여 웹 애플리케이션의 반응성과 비동기 성능을 극대화하는 표준 기술이다.
작은 최적화 습관 하나가 사용자의 기기 배터리를 절약하고 서버 프론트엔드 트래픽 부담을 획기적으로 낮춘다는 점을 잊지 말자.
이 글의 3단계 실전 예제 코드를 참고하여 현재 운영 중인 프로젝트의 Polling 기능이나 무거운 타이머 로직에 적용해 보면, 즉각적인 서버 트래픽 절감 효과와 향상된 사용자 경험을 얻을 수 있을 것이다.