왜 실제 사용자 성능 로그가 필요한가

프론트엔드 성능 문제는 개발자 PC나 Lighthouse 한 번으로 끝나지 않습니다. 사무실의 빠른 네트워크에서는 멀쩡해 보이지만, 현장의 오래된 노트북, 모바일 데이터망, 사내 보안 프로그램이 붙은 브라우저에서는 첫 화면이 늦게 뜨거나 버튼 클릭 후 반응이 밀릴 수 있습니다. 그래서 운영 중인 웹 서비스는 서버 로그와 별도로 브라우저에서 직접 관측한 성능 지표를 남겨야 합니다. 특히 LCP, INP 후보, 긴 작업 로그를 함께 보면 사용자가 느끼는 지연의 원인을 훨씬 빠르게 좁힐 수 있습니다.

LCP는 가장 큰 주요 콘텐츠가 화면에 표시되는 시점을 뜻합니다. 목록 페이지에서는 첫 번째 큰 이미지나 제목 영역이, 관리자 화면에서는 큰 테이블 또는 대시보드 카드가 대상이 됩니다. INP는 사용자의 클릭, 탭, 키 입력에 대한 반응성을 보는 지표입니다. 긴 작업은 메인 스레드를 오래 잡아먹는 JavaScript 실행을 말합니다. 세 지표를 같이 수집하면 “이미지가 늦다”, “초기 번들이 무겁다”, “클릭 후 계산이 오래 걸린다”처럼 병목을 실무 언어로 바꿔 말할 수 있습니다.

수집 설계의 기본 원칙

성능 로그는 모든 동작을 세세하게 녹화하는 도구가 아닙니다. 개인정보나 입력값을 보내지 않고, 페이지 경로와 지표 이름, 수치, 대략적인 네트워크 상태 정도만 남기는 것이 안전합니다. 또한 성능 수집 코드가 서비스 성능을 다시 해치면 안 됩니다. 전송은 즉시 하지 말고 버퍼에 모은 뒤 일정 시간마다 보내고, 페이지를 닫을 때는 sendBeacon 또는 keepalive 요청을 사용합니다. 실패한 성능 로그를 사용자 흐름보다 우선해 재시도할 필요도 없습니다.

운영 적용 전에는 어떤 화면에서 어떤 지표를 볼지 먼저 정합니다. 예를 들어 쇼핑몰 상세 페이지는 LCP와 이미지 로딩 시간을, 업무 시스템 목록 화면은 첫 테이블 렌더링과 필터 클릭 지연을, 결제나 신청 화면은 버튼 입력 후 지연을 우선으로 볼 수 있습니다. 같은 지표라도 페이지 성격이 다르면 기준도 달라집니다. 모든 URL을 한 덩어리로 평균 내면 중요한 문제가 희석되므로, 경로 그룹과 브라우저 종류, 모바일 여부 정도로 나누어 봐야 합니다.

PerformanceObserver 수집 예제

아래 예제는 브라우저에서 LCP, 긴 작업, INP 후보 이벤트를 수집해 서버 API로 보내는 최소 구조입니다. 실제 프로젝트에서는 샘플링 비율, 사용자 동의 정책, 배포 환경 구분, 중복 전송 방지 로직을 추가하면 됩니다.

// browser-perf.js
const endpoint = '/api/client-metrics';
const buffer = [];
let flushTimer = null;

function enqueue(metric) {
  buffer.push({
    ...metric,
    path: location.pathname,
    ts: Date.now(),
    connection: navigator.connection?.effectiveType || 'unknown'
  });

  if (!flushTimer) {
    flushTimer = window.setTimeout(flush, 5000);
  }
}

function flush() {
  flushTimer = null;
  if (buffer.length === 0) return;

  const payload = JSON.stringify(buffer.splice(0, buffer.length));
  if (navigator.sendBeacon) {
    navigator.sendBeacon(endpoint, new Blob([payload], { type: 'application/json' }));
    return;
  }

  fetch(endpoint, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: payload,
    keepalive: true
  }).catch(() => {});
}

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    enqueue({ name: 'lcp', value: entry.startTime, element: entry.element?.tagName || null });
  }
}).observe({ type: 'largest-contentful-paint', buffered: true });

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    enqueue({ name: 'longtask', value: entry.duration, start: entry.startTime });
  }
}).observe({ type: 'longtask', buffered: true });

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.interactionId) continue;
    enqueue({ name: 'inp-candidate', value: entry.duration, type: entry.name });
  }
}).observe({ type: 'event', buffered: true, durationThreshold: 40 });

window.addEventListener('pagehide', flush);

서버에서 저장할 때의 주의점

클라이언트가 보낸 값은 항상 신뢰할 수 없는 입력으로 처리해야 합니다. 지표 이름은 허용 목록으로 제한하고, value와 duration은 숫자 범위를 검증하며, path는 너무 긴 문자열을 잘라 저장합니다. 사용자를 식별해야 하는 경우에도 이름, 이메일, 전화번호 같은 직접 식별자를 그대로 넣지 말고, 운영 정책에 맞게 익명화된 세션 키나 해시를 사용해야 합니다. 성능 로그는 분석 목적이므로 장기 보관 기간도 미리 정해 두는 편이 좋습니다.

데이터베이스에는 원본 이벤트 테이블과 일별 집계 테이블을 분리하는 구성이 실용적입니다. 원본 테이블은 장애 분석과 상세 확인에 쓰고, 대시보드는 일별 p75, p90, p95 집계값을 조회하도록 만듭니다. 평균값만 보면 일부 사용자의 심각한 지연이 가려질 수 있습니다. 반대로 최대값만 보면 일회성 이상치에 끌려다니기 쉽습니다. 운영 대시보드는 보통 p75를 기본 기준으로 두고, p95를 경고 신호로 함께 보는 방식이 안정적입니다.

병목을 해석하는 방법

LCP가 나쁘고 긴 작업은 적다면 이미지, 폰트, 서버 응답, 렌더링 차단 리소스를 먼저 봅니다. LCP와 긴 작업이 모두 나쁘다면 초기 JavaScript 번들이 크거나, 첫 화면에서 너무 많은 데이터를 파싱하고 있을 가능성이 큽니다. INP 후보가 특정 버튼에서 반복적으로 높게 나온다면 클릭 핸들러 안에서 동기 계산, 대량 DOM 변경, 무거운 상태 업데이트가 일어나는지 확인합니다. 같은 지표라도 원인이 다르기 때문에 숫자 하나만 보고 결론을 내리지 않는 것이 중요합니다.

개선 작업은 작은 단위로 나누어 배포해야 효과를 확인할 수 있습니다. 이미지 크기 조정, 동적 import, 불필요한 polyfill 제거, 목록 가상화, 디바운스 적용, 웹 워커 분리 같은 조치를 한꺼번에 넣으면 무엇이 성능을 바꿨는지 알기 어렵습니다. 변경 전후로 동일한 경로 그룹의 p75와 p95를 비교하고, 브라우저별로 악화된 구간이 없는지도 확인해야 합니다.

운영 체크리스트

  • 성능 로그에는 입력값, 이름, 연락처 같은 개인정보를 포함하지 않습니다.
  • 지표는 LCP, INP 후보, 긴 작업처럼 행동으로 이어질 수 있는 항목부터 수집합니다.
  • 전송은 버퍼링하고, 페이지 종료 시 sendBeacon 또는 keepalive로 마무리합니다.
  • 평균보다 p75와 p95를 우선 확인하고, 페이지 유형별로 나누어 비교합니다.
  • 개선 배포는 하나의 가설 단위로 작게 진행하고 전후 데이터를 남깁니다.
  • 성능 수집 코드 자체가 오류를 일으켜도 화면 기능을 막지 않도록 방어적으로 작성합니다.

정리하면, 브라우저 성능 운영은 “빠른가 느린가”를 감으로 판단하는 일이 아니라 사용자 환경에서 발생한 지연을 작게 기록하고 꾸준히 해석하는 일입니다. LCP로 첫 화면을, INP 후보로 입력 반응성을, 긴 작업으로 메인 스레드 병목을 나누어 보면 개선 우선순위가 분명해집니다. 처음에는 작은 수집 코드와 간단한 집계만으로도 충분합니다. 중요한 것은 배포 전 측정, 배포 후 비교, 그리고 문제가 반복되는 화면을 기준으로 개선 항목을 정하는 습관입니다.