페이지 이탈을 측정하는 새로운 방법, Pending Beacon API

sejin kim·2023년 3월 11일
2
post-thumbnail

페이지 이탈 측정하기

사용자의 행동 패턴과 경험을 측정하고 분석하고자 할 때, 대표적인 수집 항목 중 하나로 '페이지 이탈'을 꼽을 수 있습니다.

탭/창을 닫거나, 링크를 클릭하여 다른 페이지로 이동한다거나, 에러로 인해 브라우저가 강제 종료되는 등 어떤 형태로든 세션이 종료되는 시점에 트래킹 코드를 실행, 로깅을 요청하는 것입니다.

비즈니스적으로 상당히 중요하게 활용될 만한 데이터이지만, 개발자의 입장에서는 의외로 이것을 정확히 측정할 만한 방법이 마땅치가 않았습니다.

아래에서 설명하겠지만, Page Visibility API 등을 활용해 일반적으로 알려진 몇 가지 방법으로 구현해 볼 수는 있으나 완벽한 솔루션이 되진 못하기 때문입니다.

페이지 이탈을 정확하게 측정하기 어려운 근본적인 원인은 크게 두 가지가 있습니다.


  • 세션 종료 직전에 발생하므로, 요청이 완료되기도 전에 측정이 중단되어 실패할 수 있음
  • 웹 페이지의 라이프사이클에서 이러한 요청을 시도할 만한 타이밍 자체가 애매함

이러한 문제들은 실제로 경험한 부분이기도 했는데, 예를 들어 첫 번째 문제의 경우에는 아래와 같은 상황을 마주하면서 확인할 수 있었습니다.



위 메트릭은 로그를 수집하는 API 서버의 로드 밸런서(AWS ELB)에서 발생한 HTTP 460 에러를 나타냅니다.

이 에러는 로드 밸런서가 클라이언트의 요청을 수신하기는 했지만, 타임아웃이 발생하기도 전에 클라이언트가 로드 밸런서와의 연결을 먼저 종료해버린 상황을 나타냅니다. AWS, Application Load Balancing Troubleshooting

API 요청 및 응답까지는 보통 10ms 정도밖에 소요되지 않았기 때문에, 페이지가 닫히기 직전의 짧은 순간이라고 해도 충분한 여유가 있을 거라 단순히 생각했습니다.

하지만 막상 사용자들의 브라우저에서는 요청이 실패하고 있었고, 비율이 그렇게 높은 것은 아니어도 그만큼 로그가 유실되는 문제가 발생한 것입니다. 네트워크 요청은 기본적으로 코스트가 높고, 여러 변수로 인해 항상 성공을 장담할 수 없는 행위라는 점을 간과했던 셈입니다.


어쨌든 이후 Navigator.sendBeacon() API를 사용하는 방법으로 해당 문제는 거의 해결할 수 있었지만, 사용자의 페이지 이탈을 정말로 정확하게 측정할 수 있는가에 대한 의문은 여전히 남아 있었습니다.

특히, Google Analytics 같은 경우 페이지 이탈 측정을 세션 종료 이벤트로 집계하는 것이 아닌, '단일 페이지에서 사용자의 상호작용이 단 한 번만 일어난 세션의 카운트(페이지 진입 이후 다른 요청이 발생하지 않았던 세션)' 라거나, '30분 동안 상호작용이 없었던 세션'과 같이 다소 우회적인(?) 방법으로 집계하고 있다는 점에서 애초에 접근을 잘못한 게 아니었을까 하는 생각이 들기도 했습니다.

하지만, 그럼에도 불구하고 다른 접근 방법을 시도해볼 수 있지 않을까 하는 생각에 관련 자료들을 더 조사해보게 되었습니다.






불안정한 Beaconing

특정한 응답을 기대하지 않으면서, 백엔드 서버에 단방향으로 데이터를 송신하는 것을 Beaconing 이라고 합니다.

세션이 종료되기 직전 간단한 데이터를 일방적으로 보내기만 하면 되기 때문에, 여러 방법으로 구현해 볼 수 있습니다.

일반적으로는 아래와 같은 네 가지 방법이 알려져 있습니다.


  1. <img /> 태그 추가
  2. 동기식으로 XMLHttpRequest(AJAX) 요청
  3. Navigator.sendBeacon() API 사용
  4. keepalive: true 옵션을 주고 fetch API 사용

이때, 각각의 동작 원리는 아래와 같습니다.


  1. 일종의 트릭으로, 브라우저가 이미지를 로드하는 상황에서는 페이지 언로드를 지연시킬 수 있다는 점을 이용한 방법입니다.
  2. 1번과 비슷하게, 동기식 처리를 통해 요청이 완료될 때까지 블로킹을 발생시키는 방법입니다.
  3. 애초에 텔레메트리 목적으로 구현된 API이므로, 페이지 언로드를 차단하지 않으면서도 브라우저가 적절한 시점에 비동기적으로 POST 요청을 전송하도록 하는 방법입니다.
  4. 3번의 방법을 대체할 수 있으며, 요청이 페이지보다 오래 지속되도록 하는 방법입니다.

일단, 1번, 2번의 경우에는 강제적인 지연이 발생한다는 점에서 사용자 경험상 바람직하지 않은 방법입니다.

또한 사용자가 다른 페이지로 이동하는 상황이 아닌, 페이지를 닫는 상황이었다면 요청이 실패할 가능성도 있습니다.

반면 3번, 4번은 지연을 발생시키지 않으며, 페이지를 닫는 상황이었다고 해도 요청이 유지되면서 거의 안정적으로 성공할 수 있으므로 훨씬 적절한 방법이라고 할 수 있습니다.

하지만 이렇게 beaconing을 성공시킨다고 해도, 여전히 한 가지 근본적인 문제가 남게 됩니다. 위에서 언급한, 페이지 이탈을 정확하게 측정하기 어려운 두 번째 원인인 '웹 페이지의 라이프사이클에서는 beaconing을 수행할 만한 적당한 시점이 없다'는 문제입니다.






신뢰할 수 없는 unload, beforeunload, pagehide 이벤트

세션 종료를 감지할 수 있는 이벤트가 없는 것은 아닙니다. unloadbeforeunload, 그리고 pagehide 이벤트가 그것인데, 문제는 이 이벤트들의 신뢰성이 너무 낮다는 것입니다.

페이지가 언로드되는 순간에 적절히 이벤트가 트리거될 거라고 기대하게 되지만, 막상 실제로는 그렇지 않은 경우가 많아 의도대로 사용하기가 어렵습니다.

주로 모바일에서 문제가 되는데, 탭을 닫는다거나 브라우저 앱 자체를 종료하는 식으로 페이지가 닫히는 경우에는 세션 종료를 감지할 방법이 없어 이벤트가 발생하지 않게 됩니다. W3C의 통계에 따르면 이벤트가 정상적으로 발생하는 경우가 Chrome 모바일 기준 약 68% 수준에 불과합니다.

그래서 페이지 이탈을 측정한다고 해도, 누락되는 케이스가 많아 실질적으로 통계적인 의미가 있는 데이터를 얻을 수 없게 될 수 있습니다.



특히 unload 이벤트의 경우에는, 페이지에 추가되는 것만으로도 브라우저가 back/forward cache(bfcache)에 부적격한 페이지로 간주하게끔 만든다는 치명적인 문제가 있기도 합니다.

이는 unload 이벤트가 이벤트 발생 이후 더 이상 페이지가 존재하지 않을 것이라는 가정 하에 설계되었기 때문으로, Firefox에서는 bfcache를 비활성화하는 방법으로, ChromeSafari에서는 unload 이벤트를 트리거하지 않는(무시하는) 방법으로 bfcache와의 모순 문제를 회피하게 되면서 문제가 발생합니다.

그래서 MDN 또는 Chrome DevelopersPage Lifecycle API 문서 등에서는 unload 이벤트를 공식적으로 사용해서는 안 되는 레거시라고 설명하고 있으며, 특히 Chrome 같은 경우는 아예 지원을 중단(deprecate)하는 것을 계획하고 있는 상황입니다.


bfcache에 대한 자세한 설명은 필자가 작성한 다음 아티클을 참고해보실 수 있습니다 : Back/Forward Cache (A.K.A. bfcache)


beforeunload 이벤트는 '저장하지 않은 내용이 있습니다. 페이지를 떠나시겠습니까?' 같은 정당한 사용례가 있기는 하지만, unload 이벤트와 동일한 문제를 공유하고 있어 적절하지 않으며,

pagehide 이벤트는 bfcache에 악영향을 주진 않고, unload 이벤트에 비하면 상대적으로 더 일관된 동작을 하긴 하지만, 역시 모바일에서 유실되는 문제는 마찬가지이기 때문에 역시 적절하지 않습니다.

그래서 결국, 어떤 이벤트도 beaconing을 시도할 만한 지점이 되지 못합니다.






모호한 Page Visibility API

그래서 그동안은 이러한 문제에 대해 '마지막으로 신뢰할 수 있을 만한 대안'으로 Page Visibility API가 제시되곤 했습니다.

문서가 표시되거나 숨겨지는 시기를 감지하는 이벤트와 페이지의 가시성 상태를 확인할 수 있는 기능을 제공하며, 특히 데스크탑과 모바일 모두에서 안정적이고 일관된 방법으로 라이프사이클을 감지하고자 하는 목적으로 구현되었기 때문입니다.

예를 들어 아래와 같이 visibilityChange 이벤트가 트리거되면, visibilityStatehidden인지 확인하여 세션 종료를 판단할 수 있는 식입니다.


document.addEventListener('visibilitychange', function() {
    if (document.visibilityState === 'hidden') {
        // beaconing
    }
});

하지만 이 또한 결국 완벽한 대안이 되진 못했습니다.

visibilityState 속성을 통해 정의하고자 하는 '페이지의 가시성', '사용자가 뷰포트의 콘텐츠를 관찰할 수 있는 상태'라는 개념 자체가 상황에 따라 다소 불명확할 수 있어, 처음 도입된 이후 수 년의 시간이 지났음에도 여전히 플랫폼 및 디바이스에 따른 호환성 이슈가 존재하고 있기 때문입니다.

실제로 데스크탑 및 모바일 플랫폼에서 visibilityChange 이벤트가 발생하는 경우를 분석한 결과를 인용해 보겠습니다.


Desktop (macOS 10.15.2)

ActionChrome
(79.0.3945.130)
Firefox
(72.0.2)
Safari
(13.0.4)
페이지 새로고침
링크를 통한 탐색
뒤로/앞으로 가기 버튼을 통한 탐색
탭 전환
탭 닫기
브라우저 닫기

Mobile (iOS 13.3)

ActionChrome
(79.0.3945.136)
Firefox
(68.4.2)
Safari
(13.3)
페이지 새로고침
링크를 통한 탐색
뒤로/앞으로 가기 버튼을 통한 탐색
탭 전환
앱 전환
탭 닫기
앱 닫기

( 출처 : https://github.com/w3c/page-visibility/issues/59 )


20년 1월에 제기된 이슈여서 최신의 데이터는 아니지만, 이후로도 어떤 합의나 표준화가 이루어진 히스토리는 딱히 없었기 때문에 현재 시점에서도 유효한 이슈입니다.

브라우징 컨텍스트의 범위와 정의, 가시성(visibility) 상태를 판단하는 기준, 백그라운드에서의 JavaScript 실행 가능 여부 등, 세션 종료를 판단하는 것이 생각 이상으로 복잡한 요소들이 얽히고설킨 어려운 문제라는 사실을 알 수 있습니다.






마침내 제안된 Pending Beacon API


결국 개발자가 페이지의 라이프사이클 문제 자체를 고려하지 않아도 될 수 있도록, 선언적으로 beaconing을 수행할 수 있는 Pending Beacon API가 등장했습니다.

비교적 최근(22년 4분기) 시점에 설계 검토 및 명세가 제안되었는데, 글 작성 시점에서 확인된 내용으로는 Chrome 기준 3월 29일에 Stable 릴리즈 예정인 112 버전부터 실험적으로 지원하기 시작할 수 있을 것으로 보입니다.

그러나 FirefoxSafari 진영에서는 아직 논의가 진행중인 상황이고, 구체적인 명세 및 구현 형태가 확정된 것은 아니어서 근시일 내에 실무에서 활용하기는 어려울 것으로 보입니다.

아래 이슈에서 제기된 토론 내용을 살펴보면, 이미 sendBeacon이 존재하는 상황에서 굳이 새로운 API로 분리하여 구현하는 이유는 무엇인지, 기존의 문제들을 완벽하게 극복할 수 있는지에 대한 의문 등이 눈에 띕니다.



여하튼 Pending Beacon API의 핵심은 개발자가 명시적으로 직접 beaconing을 수행하는 것이 아니라, 페이지가 종료될 때 beaconing이 어떻게든 수행될 수 있도록 브라우저에 미리 등록해 두는 개념이라는 것입니다.

기존의 한계점들 중 하나가 안정성이었던 만큼, sendBeacon 또는 keepalive가 활성화된 fetch API에 비해서도 더욱 안정적으로 비콘이 전송될 수 있도록 보장된다는 점도 핵심적인 특징입니다.

또한 sendBeacon과는 달리 보안 컨텍스트(HTTPS)에서만 전체 API를 사용할 수 있다는 제약이 생겼고, POST뿐만 아니라 GET 요청이 가능해졌으며,

에러(crash), 강제 종료, 네트워크 연결 끊김 등으로 전송에 실패한 경우에도 다음 브라우저 실행 시점에 전송을 재시도할 수 있게 되었습니다.

PendingBeacon은 인터페이스이며, 아래와 같은 형태로 PendingGetBeaconPendingPostBeacon을 구현하여 사용합니다. 다만 아래의 내용은 추후 변경이 있을 수 있습니다.


const beacon = new PendingGetBeacon('/log', {
    // 페이지가 숨겨진 이후, 브라우저가 비콘을 전송하기까지 대기할 시간을 설정할 수 있음
    // 값이 0 미만일 경우 타이머를 설정하지 않은 것으로 간주하며, 페이지가 닫히거나 bfcache에서 제거된 페이지에서만 비콘을 전송함
    backgroundTimeout: 1000,
});
 
// 비콘 전송 설정, payload는 기존 sendBeacon() 메소드와 동일 (String, FormData, URLSearchParams, Blob, ArrayBuffer ...)
beacon.setData(data);
 
// 현재 설정된 비콘을 취소
beacon.deactive();
 
// 현재 설정된 비콘을 즉시 전송
beacon.sendNow();

🆕 Update
현재는 그동안의 논의 내용 등을 바탕으로, 새로운 API인 fetchLater()로 업데이트되어 제안된 상태입니다.






마치며

오랜 시간 해결되지 않았던 문제들을 해결할 수 있는 새로운 API가 구현된 건 반가운 일이지만, 아직은 실질적으로 활용할 수 없는 상황이라는 점이 아쉽습니다.

때문에 현재로서는 Google Analytics 등의 기존 솔루션들을 참고하면서, 특정한 이벤트 - 페이지 라이프사이클에 크게 의존하지 않으면서 비콘을 전송하여 최소한의 누락으로 집계할 수 있는 방안을 연구해보아야 할 것으로 생각됩니다.

직접 로그 수집 서버를 개발하고 데이터를 수집, 분석해야 하는 것은 아니지만, 프론트엔드 포지션에서 정확한 상황과 시점에 로그를 전송해주지 않으면 데이터의 의미와 가치 자체가 퇴색될 것이므로 생각보다 가볍지 않은 문제임을 체감하게 됩니다.

특히 외부 솔루션에만 의존하지 않고, 인하우스 툴을 개발하면서 본격적으로 데이터 웨어하우스를 구축하려는 청사진을 그리는 백엔드 개발자 동료들의 노력에도 호응하고 싶다는 욕심이 있어 다소의 부담감이 느껴지기도 합니다.

하지만 이런 경험을 통해 브라우저에 대해 조금 더 깊게 이해할 수 있고, Web API의 최신 동향과 발전 과정을 따라가볼 수 있으니 좋은 일이 아닐까 싶습니다.


📖 참고 문서

profile
퇴고를 좋아하는 주니어 웹 개발자입니다.

0개의 댓글