전체 세션 분석 및 원격 분석을 안정적으로 측정

페이지 로드뿐만 아니라 사용자의 웹페이지 방문 전체를 포괄하는 분석, Core Web Vitals, 원격 분석 데이터를 안정적으로 추적하려면 fetchLater() API를 사용하세요.

fetchLater() API는 응답이 필요하지 않고 전송 타이밍이 긴급하지 않은 경우 서버에 데이터를 전송하는 가장 안정적인 방법입니다. 이는 사용자 분석, 원격 분석, 오류 추적, Core Web Vitals와 같은 성능 측정항목과 같은 데이터에 적용됩니다.

unload 이벤트 리스너에서 <img> 픽셀을 만드는 것과 같은 이전 기술은 신뢰할 수 없는 것으로 알려져 있으며 (특히 모바일에서) 성능에 부정적인 영향을 미칠 수 있습니다 (페이지가 bfcache에 적합하지 않음).

구현 방법

  1. 요청 예약: 관련 데이터를 사용할 수 있게 되면 데이터 페이로드와 함께 fetchLater()를 호출합니다. 이렇게 하면 나중에 전송할 데이터가 큐에 추가됩니다.

  2. 필요에 따라 페이로드 업데이트: 사용자가 더 많은 데이터를 생성하거나 상태가 변경되면 이전에 예약된 요청을 취소하고 완전히 업데이트된 스냅샷으로 fetchLater()를 다시 호출합니다.

  3. 나머지는 브라우저에서 처리하도록 허용: 사용자가 다른 페이지로 이동하거나 탭을 닫으면 브라우저가 가장 최근의 fetchLater() 호출에서 페이로드를 안정적으로 전송합니다.

예시 코드

이 코드는 fetchLater()를 사용하여 업데이트된 세션 시간과 함께 10초마다 새 비콘을 큐에 추가하여 사용자의 페이지 방문 세션 시간을 측정합니다.

const ANALYTICS_ENDPOINT = '/path/to/analytics/endpoint';

const sessionData = {
  duration: 0,
  id: crypto.randomUUID(),
};

let fetchLaterController = null;

function queueBeacon() {
  // Abort any pending beacons before creating a new one.
  if (fetchLaterController) {
    fetchLaterController.abort();
  }
  fetchLaterController = new AbortController();

  // Update session duration to the current page time.
  sessionData.duration = performance.now();

  // Schedule a fetch for the data payload to be sent later.
  // IMPORTANT: wrap the call in a try/catch to handle quota errors.
  try {
    fetchLater(ANALYTICS_ENDPOINT, {
      method: 'POST',
      headers: {'content-type': 'application/json'},
      body: JSON.stringify(sessionData),
      signal: fetchLaterController.signal,
    });
  } catch (error) {
    // Handle errors as needed.
  }
}

// Update the session data and queue a new beacon every 10 seconds.
setInterval(queueBeacon, 10000);

권장사항

  • 응답이 필요하지 않고 데이터를 즉시 전송하는 것이 중요하지 않은 상황에서는 fetchLater()를 사용하여 서버에 데이터를 전송해야 합니다(DO).
  • 사용자가 페이지를 떠나기 전에 데이터를 업데이트해야 할 수 있는 경우 AbortController를 사용하여 대기 중인 가져오기를 취소해야 합니다.
  • 할당량 (현재 출처당 약 64KB)을 초과하지 않도록 페이로드 크기를 최소화해야 합니다.
  • fetchLater() 호출을 try/catch로 래핑하여 할당량 오류를 처리해야 합니다.
  • globalThis에서 fetchLater()의 존재를 감지하는 기능을 사용하고 API를 지원하지 않는 브라우저를 위한 대체 전략을 구현합니다.
  • 요청 본문에 ReadableStream 객체를 사용하지 마세요. 오류가 발생합니다.

브라우저 지원 및 대체 전략

fetchLater()이(가) 기준 목표를 충족하지 못하는 경우 대체 전략이 필요합니다. 하지만 이 API의 향상된 안정성과 성능 이점을 고려할 때 브라우저에서 지원하는 경우 fetchLater()를 사용해야 합니다.

권장되는 대체 전략은 아래의 폴리필을 사용하는 것입니다. 이 폴리필은 keepalive 또는 navigator.sendBeacon()와 함께 fetch()를 사용하여 지원되지 않는 브라우저를 내부적으로 처리합니다. 자체 코드에서 fetchLater()를 직접 호출해야 합니다(MUST). fetch(), sendBeacon() 또는 기타 비콘 API를 직접 호출해서는 안 됩니다.

fetchLater() 폴리필

지원되지 않는 브라우저에서 API를 최대한 가깝게 구현하는 다음 최소 fetchLater() 폴리필을 사용합니다.

이 폴리필의 유일한 주목할 만한 동작 차이는 브라우저의 기본 언로드 처리에 의존하는 대신 visibilitychange를 사용하여 사용자가 나가는 시점을 감지한다는 것입니다. 이는 내부 구현 세부정보입니다. 코드에서 visibilitychange 또는 다른 페이지 수명 주기 이벤트를 수신 대기할 필요가 없습니다. fetchLater()를 호출하기만 하면 폴리필이 전송을 처리합니다.

globalThis.fetchLater ??= function fetchLater(url, init = {}) {
  let timeoutHandle;
  let activated = false;

  function sendNow() {
    if (!(init.signal && init.signal.aborted)) {
      // Use fetch keepalive if the browser supports it or if custom fetch
      // parameters are specified (e.g. custom headers or methods).
      // Otherwise fall back to `navigator.sendBeacon()`.
      if (
        'keepalive' in Request.prototype ||
        init.method !== 'POST' ||
        init.headers
      ) {
        fetch(url, Object.assign({}, init, {keepalive: true}));
        activated = true;
      } else {
        activated = navigator.sendBeacon(url, init.body);
      }
    }
    destroy();
  }

  function destroy() {
    document.removeEventListener('visibilitychange', sendNow);
    clearTimeout(timeoutHandle);
  }

  if (document.visibilityState === 'hidden') {
    // If the beacon was created while the page is already hidden, send data
    // ASAP but wait until the next microtask to allow all sync code to run.
    queueMicrotask(sendNow);
  } else {
    document.addEventListener('visibilitychange', sendNow);

    if (typeof init.activateAfter === 'number' && init.activateAfter >= 0) {
      timeoutHandle = setTimeout(sendNow, init.activateAfter);
    }
  }

  if (init.signal) {
    init.signal.addEventListener('abort', destroy);
  }

  return {
    get activated() {
      return activated;
    },
  };
};