웹 애플리케이션을 개발하면서 사용자가 페이지를 닫거나 다른 사이트로 이동할 때 체류 시간, 사용자 행동 분석 로그, 혹은 작성 중이던 데이터의 상태를 서버로 전송해야 하는 상황을 경험해봤을 것이다.
다만 대부분의 개발자들은 unload 이벤트 핸들러 내부에서 일반적인 fetch()나 axios를 호출했다가, 브라우저가 요청을 강제로 중단시켜 데이터가 서버에 전달되지 않는 현상을 겪고 당황하는 경우가 많다.
이번에는 페이지 이탈 시 데이터가 유실되는 정확한 원인부터 이를 깔끔하게 해결해 주는 Beacon API(navigator.sendBeacon)의 개념, 왜 필요한지, 그리고 실무에서 안전하게 적용하는 방법까지 완벽하게 정리해서 소개하겠다.
사용자가 페이지를 이탈하거나 탭을 닫으면 브라우저는 해당 문서의 프로세스와 메모리 자원을 즉시 정리(Unload)하기 시작한다.
이 과정에서 beforeunload나 unload 이벤트 내에 작성된 일반적인 비동기 HTTP 요청(fetch, XMLHttpRequest)은 완료되기도 전에 브라우저에 의해 강제로 취소(Canceled)된다.
과거에는 이를 막기 위해 동기식(Synchronous) XHR을 사용하거나 의미 없는 반복문으로 브라우저 스레드를 강제로 잡아두는 편법을 쓰곤 했다. 하지만 이는 사용자 경험을 심각하게 해치므로 최신 브라우저에서는 동기식 네트워크 요청 생성을 아예 금지하고 있다.
Beacon API는 페이지가 언로드되는 상황에서도 브라우저가 백그라운드에서 비동기적으로 HTTP POST 요청을 보낼 수 있도록 보장하는 전용 API다.
웹 페이지의 수명 주기와 독립적으로 작동하기 때문에, 브라우저가 창을 닫더라도 백그라운드 큐에 전송 작업을 등록해 두고 서버로 전송을 완료한다. 또한 메인 스레드를 블로킹하지 않으므로 다음 페이지 이동이나 창 닫기 동작이 지연되지 않는다.
페이지 이탈 시점에 데이터를 전송하기 위한 대표적인 기술들을 비교해 보면 Beacon API의 확실한 장점을 확인할 수 있다.
| 비교 항목 | 동기식 XHR (deprecated) | fetch() + keepalive | Beacon API (sendBeacon) |
|---|---|---|---|
| 페이지 이탈 후 전송 보장 | 가능하지만 스레드 차단 | 조건부 보장 (최신 스펙) | 브라우저가 백그라운드 전송 완전 보장 |
| 메인 스레드 영향 | 페이지 닫힘을 멈춤 (UX 저해) | 영향 없음 | 영향 없음 (비동기 처리) |
| 사용 가능 HTTP 메소드 | GET / POST 등 | GET / POST / PUT 등 | POST 전용 |
| 헤더 커스텀 여부 | 자유로움 | 자유로움 | 제한적 (ArrayBuffer, Blob 등) |
과거에는 unload나 beforeunload 이벤트를 많이 사용했으나, 모바일 브라우저나 최신 크롬 환경에서는 해당 이벤트가 항상 신뢰성 있게 호출되지 않는다.
따라서 현재 W3C 표준 가이드에서는 페이지의 가시성 상태가 변경되는 visibilitychange 이벤트를 사용하고, document.visibilityState === 'hidden' 일 때 데이터를 전송하는 것을 강력히 권장한다.
실제 서비스에서 사용자가 페이지에 진입한 후 이탈할 때까지의 체류 시간과 세션 데이터를 서버에 유실 없이 전달하는 스크립트를 구현해 보자.
<?php
// api/log.php
header('Content-Type: application/json');
// Beacon API는 기본적으로 'application/x-www-form-urlencoded' 또는 'text/plain'으로 전송됨
$rawData = file_get_contents('php://input');
$data = json_decode($rawData, true);
if ($data) {
// 로그 파일 저장 또는 DB 저장 로직 수행
$logMessage = sprintf("[%s] User: %s, Duration: %ds\n", date('Y-m-d H:i:s'), $data['userId'], $data['duration']);
file_put_contents(__DIR__ . '/analytics.log', $logMessage, FILE_APPEND);
echo json_encode(['status' => 'success']);
exit;
}
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Invalid Payload']);
// analytics.js
(function() {
const startTime = Date.now();
const LOG_ENDPOINT = '/api/log.php';
function sendAnalyticsData() {
// 이미 전송했거나 가시성 상태가 hidden이 아니면 중단
if (document.visibilityState !== 'hidden') {
return;
}
const duration = Math.floor((Date.now() - startTime) / 1000);
const payload = JSON.stringify({
userId: 'user_12345',
page: window.location.pathname,
duration: duration,
timestamp: new Date().toISOString()
});
// JSON 데이터 전송을 위한 Blob 객체 생성
const blob = new Blob([payload], { type: 'application/json' });
// Beacon API를 통한 백그라운드 전송
const success = navigator.sendBeacon(LOG_ENDPOINT, blob);
console.log('Beacon sent status:', success);
}
// unload 대신 visibilitychange 이벤트 감지
document.addEventListener('visibilitychange', sendAnalyticsData);
})();
개발 과정에서 흔히 발생하는 잘못된 구현 방식과 올바른 접근 방식을 살펴보자.
✗ 잘못된 코드(왜 안 되는가): 아래 코드는 탭을 닫거나 다른 페이지로 넘어갈 때 fetch 요청이 즉시 Canceled 되어 서버에 도착하지 않는다.
// ✗ 잘못된 방식: unload 이벤트와 일반 fetch 조합
window.addEventListener('unload', function() {
fetch('/api/log.php', {
method: 'POST',
body: JSON.stringify({ duration: 100 })
}); // 브라우저 종료 시점에 이 비동기 요청은 즉시 취소됨!
});
✓ 올바른 코드(왜 이게 맞는가): visibilitychange 이벤트를 사용하여 모바일/데스크톱 환경 모두에서 가시성 변경을 정확히 감지하고, sendBeacon을 이용해 브라우저 큐에 전송을 위임한다.
// ✓ 올바른 방식: visibilitychange와 sendBeacon 조합
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') {
const data = new Blob([JSON.stringify({ duration: 100 })], { type: 'application/json' });
navigator.sendBeacon('/api/log.php', data);
}
});
브라우저 개발자 도구(F12)의 Network 탭에서 관찰하면, 페이지를 이탈하거나 다른 탭으로 전환하는 순간 /api/log.php로 POST 요청이 전달되는 것을 볼 수 있다.
전송 타입은 ping 또는 fetch 형태로 표기되며, 페이지를 완전히 닫아도 서버의 analytics.log 파일에는 정상적으로 아래와 같은 로그가 기록된다.
[2026-03-31 14:20:05] User: user_12345, Duration: 42s
페이지 이탈 시점의 안정적인 데이터 수집은 통계의 정확도와 서비스 분석에 있어 아주 중요한 요소다. 그동안 unload 이벤트 안에서 수많은 fetch 요청이 공중으로 날아가며 누락되었던 로그들이 있다면, 지금 즉시 visibilitychange와 Beacon API 조합으로 교체해야 한다.
Beacon API는 데이터 유실 방지와 사용자 경험 향상이라는 두 마리 토끼를 모두 잡는 가장 깔끔한 표준 기술이다. 작은 최적화와 올바른 이벤트 선택이라는 습관이 모여 서비스 전체의 데이터 신뢰성을 완성한다는 점을 잊지 말자. 이 글의 실전 자바스크립트 예제 부분을 참고해 프로젝트의 분석/로깅 로직을 교체해 본다면, 단 하나의 이탈 로그도 놓치지 않는 견고한 서비스를 구축할 수 있을 것이다.