초기 네트워크를 가로채는 Coralogix RUM 막기

Coralogix RUM은 로드 중에 비콘을 전송하여 LCP의 네트워크를 가로챕니다. 이벤트를 버퍼링하고 page hide 시점에 플러시하십시오.

Arjen Karel Core Web Vitals Consultant
Arjen Karel - linkedin
Last update: 2026-06-06

초기 네트워크를 가로채는 Coralogix RUM 막기

RUM 도구의 역할은 단 하나다. 사용자가 실제로 겪는 경험을 측정하는 것이다. Coralogix는 로드 중에 그 반대로 동작한다. init을 호출하는 순간 비콘 전송을 시작한다. 이 호출이 메인 번들에 들어 있으면 SDK는 로드 및 hydration 윈도우에 첫 번째 배치를 전송하며, 정작 측정해야 할 LCP의 네트워크를 가로챈다. 

도구가 지표를 먼저 악화시키고 이를 보고한다. 완전히 거꾸로 됐다. 그래서 데이터를 버퍼링한 뒤 페이지를 떠날 때 Coralogix API로 전송하는 해결책을 작성했다(Core/Dash는 기본적으로 이렇게 동작하므로, 더 나은 RUM 도구를 찾고 있다면 전환을 고려해 보라).

페이지보다 자신을 더 중요하게 여기는 Coralogix

그 비콘이 무엇과 경쟁하는지 보라. 히어로 이미지, 폰트, 페이지를 hydration하는 스크립트다. 광랜 환경이라면 전혀 눈치채지 못할 수도 있다. 하지만 모든 사용자가 광랜을 쓰지는 않는다. 휴대폰을 쓸 때, 열차 안에서, 호텔 와이파이를 사용할 때 대역폭은 부족하며, 해당 비콘은 이를 차지하기 위해 실제 콘텐츠와 경쟁한다. 가장 열악한 연결 상태에 있는 방문자들은 이를 체감할 것이다. 도대체 무엇을 위해서인가?

그리고 진짜 의구심이 들어야 할 부분은 바로 이것이다. 그 초기 데이터가 당장 전송되어야 할 이유는 전혀 없다. 해당 데이터는 단지 아직 진행 중인 방문 정보일 뿐이다. 아무도 이를 실시간으로 보고 있지 않다(만약 보고 있다면 정말 소름 돋는 일이다). 데이터는 메모리에 두고 방문이 종료될 때 내보내면 된다. 추가 비용도 없고 유실될 위험도 없다. 로드 중에 전송하는 것은 단점뿐이다. 페이지 속도만 느려질 뿐, 얻는 이득은 전혀 없다. 이는 어리석은 방식이다. 그러니 주도권을 되찾아라. 모든 이벤트를 캡처하되, 방문이 끝날 때까지는 아무것도 전송하지 마라.

corelogix network and main thread

좋다, 한탄은 여기까지 하자. 이제 해결할 시간이다. Coralogix가 제대로 동작하게 만드는 방법은 다음과 같다!

모두 버퍼링하고 아무것도 전송하지 않기

Coralogix는 beforeSend 훅을 제공한다. 이벤트를 반환하면 전송되고, null을 반환하면 드롭된다. 따라서 이벤트를 먼저 배열에 넣고 null을 반환하라. 모든 이벤트가 캡처되지만, 네트워크에는 아무것도 전송되지 않는다.

import { CoralogixRum } from '@coralogix/browser';

const PUBLIC_KEY = '<YOUR_PUBLIC_KEY>';
const APPLICATION = 'my-app';

// 모든 이벤트를 캡처하고, 실시간으로 전송하지 않음.
const buffer = [];

CoralogixRum.init({
  public_key: PUBLIC_KEY,
  application: APPLICATION,
  version: '1.0.0',
  coralogixDomain: 'EU1',
  beforeSend: (event) => {
    buffer.push({ event, t: Date.now() }); // 현재 시간을 기록하고 나중에 플러시
    return null;                           // SDK 자체 비콘 전송을 무력화
  },
});

문서상 beforeSend는 에러 전용 필터처럼 보이지만, 실제로는 그렇지 않다. 중요한 데이터를 모두 잡아내는지 직접 확인했다. 이 훅은 init 스냅샷, 리소스 타이밍, 모든 Web Vitals, 사용자 상호작용, 그리고 에러 등 모든 이벤트 유형에서 호출된다. SDK는 페이지 전체를 측정하는 본래의 역할을 계속 수행하며, 개발자가 플러시를 지시하기 전까지 네트워크 전송만 멈출 뿐이다.

페이지를 벗어날 때 플러시하기

이제 페이지가 숨겨질 때 버퍼를 전송한다. 단순하게 접근한 구현 방식은 여기서 막힌다. unload 시점에 데이터를 전송하는 교과서적인 방법인 navigator.sendBeacon은 여기에서 작동하지 않는다. ingress가 Authorization: Bearer 헤더로 인증을 요구하지만, sendBeacon은 요청 헤더를 설정할 수 없기 때문이다. 호출 자체는 true를 반환하지만 소리 없이 403 에러가 발생한다. 대신 keepalive가 설정된 fetch를 사용하라. unload 상황에서도 동일하게 유실되지 않고 헤더 설정도 가능하다.

비콘이 감추고 있던 사실이 하나 더 있다. ingress는 원시 이벤트를 직접 받지 않는다. SDK는 각 이벤트를 span으로 래핑한 뒤 { logs: [...] } 형태로 전송한다. 전송 전에 이 형태로 직접 재구성해야 한다.

const INGRESS = 'https://ingress.eu1.rum-ingress-coralogix.com/browser/v1beta/logs';
// coralogixDomain에 맞춰 eu1을 사용 중인 리전(us1, us2, eu2, ap1...)으로 변경

// ingress는 원시 이벤트가 아닌 SDK의 span 형태를 요구하므로 재구성한다.
function toCxSpan({ event, t }) {
  const hasStack = !!(event.error_context
    && event.error_context.original_stacktrace
    && event.error_context.original_stacktrace.length);

  return {
    version_metadata: event.version_metadata,
    applicationName: APPLICATION,
    subsystemName: 'cx_rum',
    severity: (event.event_context && event.event_context.severity) || 3,
    isErrorWithStacktrace: hasStack,
    timestamp: t,
    text: { cx_rum: Object.assign({}, event, { timestamp: t }) },
  };
}

function flush() {
  if (!buffer.length) return;
  const logs = buffer.splice(0).map(toCxSpan); // 버퍼 비우기: 두 번째 호출 시에는 아무것도 전송하지 않음

  fetch(INGRESS, {
    method: 'POST',
    keepalive: true, // unload 상황에서도 유지되며, sendBeacon과 달리 헤더 설정이 가능함
    headers: {
      'Authorization': 'Bearer ' + PUBLIC_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ logs, skip_enrichment_with_ip: false }),
  }).catch(function () {});
}

document.addEventListener('visibilitychange', function () {
  if (document.visibilityState === 'hidden') flush();
});
window.addEventListener('pagehide', flush);

splice가 버퍼를 비워주므로, 두 번째 hidden 이벤트가 발생해도 아무것도 전송하지 않는다. unload가 실행되지 않는 모바일 환경에서도 visibilitychange의 hidden 상태는 신뢰할 수 있는 신호다. pagehide는 최후의 보루(backstop) 역할을 한다. 한 가지 주의할 점이 있다. keepalive는 현재 진행 중인 요청 전체에서 64KB 한도를 공유한다. 따라서 수천 개의 리소스 항목을 버퍼링하는 긴 세션의 경우 이 제한을 초과할 수 있다. 수집량이 많다면 logs 배열을 여러 개의 작은 단위로 나누어 전송해야 한다.

감수해야 할 부분

이제 Coralogix의 포맷을 수작업으로 재구성해야 하니 주의해야 한다! 전송되는 session_context는 SDK 자체 데이터보다 다소 빈약하다. Coralogix는 세션 ID, 사용자 에이전트, 디바이스 정보를 이후에 beforeSend 호출이 끝난 다음에야 채워 넣는다. 따라서 직접 빌드한 span에는 일반 비콘보다 그러한 세부 정보가 적게 담긴다. Core Web Vitals 수집에는 지장이 없지만, 심층적인 세션 분석 시에는 영향이 있을 수 있다. 첫 배포 후 네트워크 탭에서 200 응답이 뜨는지 확인하라. 잘못된 키나 엄격해진 CORS 규칙으로 인해 실패하더라도, 비콘과 동일하게 아무 소리 없이 실패하기 때문이다.

자신이 제어하지 않는 통신 포맷을 직접 유지관리하는 일이 번거롭게 느껴진다면, 간단한 대안으로 CoralogixRum.init()을 hydration 이후로 지연시켜 SDK가 평소대로 전송하도록 두는 방법도 있다. init 이전의 로딩 메트릭은 잃겠지만 정신 건강은 지킬 수 있다. 어느 쪽을 감수할지 선택하라.

About the author

Arjen Karel is a web performance consultant and the creator of CoreDash, a Real User Monitoring platform that tracks Core Web Vitals data across hundreds of sites. He also built the Core Web Vitals Visualizer Chrome extension. He has helped clients achieve passing Core Web Vitals scores on over 925,000 mobile URLs.

관리 안 하는 순간 퍼포먼스는 무너집니다.

모니터링, 퍼포먼스 버짓, 프로세스까지 세팅합니다. 일회성 수정과 진짜 해결의 차이가 바로 거기서 갈립니다.

한번 얘기해봐요
초기 네트워크를 가로채는 Coralogix RUM 막기Core Web Vitals 초기 네트워크를 가로채는 Coralogix RUM 막기