본문으로 건너뛰기

성능, 사용자가 느끼는 속도 - 사용자 대상 소프트웨어 테스트 가이드 ep.07

빠른 것과 빠르게 느껴지는 것은 다르다. 사용자에게는 후자가 전부다.

사용자가 마주하는 문제

페이지가 열리긴 했는데 글자가 한참 뒤에 뜨거나, 버튼이 그려졌는데 막상 누르니 반응이 없습니다. 광고가 늦게 들어오면서 글 위치가 갑자기 밀려, 누르려던 링크가 다른 곳으로 옮겨가기도 합니다.

이런 경험은 “느리다”는 한 단어로 묶이지만, 원인은 제각각입니다. 일부는 서버 응답이 느려서, 일부는 큰 이미지를 다 받기 전까지 빈 자리이기 때문에, 일부는 자바스크립트 실행이 메인 스레드를 잡고 있어서 일어납니다. 성능 테스트는 이 원인들을 측정 가능한 지표로 분해하고, 그 지표가 어제보다 나빠지지 않았다는 보장을 만드는 일입니다.


무엇을 측정하나 - Core Web Vitals

구글이 정리한 Core Web Vitals는 사용자가 느끼는 세 가지 측면을 객관적 수치로 옮긴 지표입니다.

  • LCP (Largest Contentful Paint) — 가장 큰 콘텐츠가 그려질 때까지. “페이지가 떴다”의 체감
  • INP (Interaction to Next Paint) — 상호작용 후 화면이 반응할 때까지. “반응이 빠르다”의 체감. 2024년부터 FID를 대체
  • CLS (Cumulative Layout Shift) — 화면이 의도치 않게 얼마나 흔들리는가. “안정적이다”의 체감

각 지표에는 권장 기준선이 있습니다. LCP는 2.5초 이내, INP는 200ms 이내, CLS는 0.1 이하가 “Good”입니다. 모든 페이지가 항상 만족할 필요는 없지만, 회귀를 추적할 때 기준선으로 쓰기에 좋습니다.

Core Web Vitals 세 지표의 의미를 보여주는 구조도. 가운데에 사용자가 느끼는 속도라는 공통 주제가 있고, 그것이 세 가지 체감 영역으로 나뉜다. 첫 번째는 페이지가 떴다는 체감으로 LCP가 측정하며 2.5초 이내가 권장 기준이다. 두 번째는 반응이 빠르다는 체감으로 INP가 측정하며 200ms 이내가 기준이다. 세 번째는 안정적이라는 체감으로 CLS가 측정하며 0.1 이하가 기준이다. 각 지표가 어떤 사용자 경험에 대응하는지가 함께 표시되어 있다.


합성 측정과 실사용 측정

성능 측정은 두 갈래로 나뉩니다. 둘 다 필요합니다.

  • 합성 측정(Synthetic) — 정해진 환경에서 페이지를 시뮬레이션해 측정. Lighthouse, WebPageTest. 회귀 추적과 비교에 유용
  • 실사용 측정(RUM, Real User Monitoring) — 진짜 사용자가 페이지를 쓸 때의 지표를 수집. 운영 환경의 실태를 보여줌

합성은 같은 조건에서 반복 측정하기 좋아 회귀 추적에 강합니다. 다만 평균 사용자의 실제 환경(느린 네트워크, 오래된 기기)을 다 담지는 못합니다. 실사용은 진짜를 보여주지만 변수가 많아 단일 PR의 영향을 분리하기 어렵습니다. 두 축이 함께 있어야 “지표가 나빠졌다, 그게 새 코드 때문이다”를 말할 수 있습니다.

합성 측정과 실사용 측정의 차이를 비교한 그림. 왼쪽은 합성 측정으로 정해진 환경에서 반복 측정해 회귀를 추적하기 좋지만 실제 사용자 환경의 다양성은 잡지 못한다. 오른쪽은 실사용 측정으로 진짜 사용자의 다양한 환경과 패턴을 수집하지만 단일 변경의 영향을 분리하기는 어렵다. 두 측정이 서로의 약점을 보완하며 함께 운영되어야 한다는 점이 강조되어 있다.


도구와 예시

합성 측정

Lighthouse CI는 PR마다 자동으로 페이지를 측정하고, 기준선에서 벗어나면 빨간불을 띄웁니다.

# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [pull_request]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci && npm run build
      - run: npm install -g @lhci/cli
      - run: lhci autorun
// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: ["http://localhost:3000/", "http://localhost:3000/pricing"],
      numberOfRuns: 3,
      startServerCommand: "npm run start",
    },
    assert: {
      assertions: {
        "categories:performance": ["error", { minScore: 0.9 }],
        "largest-contentful-paint": ["error", { maxNumericValue: 2500 }],
        "cumulative-layout-shift": ["error", { maxNumericValue: 0.1 }],
      },
    },
  },
};

핵심은 기준선을 정해 회귀를 차단하는 것입니다. 점수가 90에서 89로 내려가는 건 노이즈일 수 있지만, 80으로 내려가면 분명한 신호입니다.

실사용 측정

web-vitals 라이브러리로 직접 수집할 수도 있고, Sentry/Datadog RUM/Vercel Analytics 같은 서비스에 위임할 수도 있습니다.

// vitals.ts
import { onLCP, onINP, onCLS } from "web-vitals";

function sendToAnalytics({ name, value, id }: { name: string; value: number; id: string }) {
  // 분석 서버로 전송 (또는 Sentry/Datadog 등으로)
  navigator.sendBeacon("/analytics/vitals", JSON.stringify({ name, value, id }));
}

onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);

수집된 데이터는 분포로 봅니다. 평균이 아니라 75 백분위 또는 95 백분위를 봐야 합니다. 평균은 한두 명의 빠른 사용자에게 가려져 진실을 가립니다.


부하 테스트

여기까지가 프론트엔드 성능 이야기였다면, 백엔드 측에는 부하 테스트(Load Testing)가 있습니다. 평소 트래픽과 피크 트래픽에서 응답 시간이 어떻게 변하는지, 어디서 무너지는지를 확인하는 일입니다.

  • k6 — 자바스크립트로 시나리오 작성, 가장 널리 쓰임
  • JMeter — GUI 기반, 오래된 표준
  • Locust — 파이썬 기반
// load-test.js (k6)
import http from "k6/http";
import { check, sleep } from "k6";

export const options = {
  stages: [
    { duration: "30s", target: 50 },   // 50명까지 점진 증가
    { duration: "1m",  target: 50 },   // 1분 유지
    { duration: "30s", target: 0  },   // 정리
  ],
  thresholds: {
    http_req_duration: ["p(95)<500"], // 95%가 500ms 이내
  },
};

export default function () {
  const res = http.get("https://staging.example.com/api/products");
  check(res, { "status is 200": (r) => r.status === 200 });
  sleep(1);
}

부하 테스트는 매 PR마다 돌릴 종류는 아닙니다. 큰 릴리스 전이나 트래픽이 늘 것으로 예상되는 시점에 돌리는 게 보통입니다.


언제, 어디서 실행하나

  • 개발 중 — Chrome DevTools의 Performance 탭과 Lighthouse 패널로 즉시 측정
  • PR 단계 (CI) — Lighthouse CI로 핵심 페이지 회귀 차단
  • 운영 중 — RUM으로 실사용 지표 수집, 임계치 초과 시 알림
  • 릴리스 직전 — 부하 테스트 (큰 변경 시)

매일 모든 지표를 보지는 않습니다. 회귀 알림을 임계치 기반으로 자동화해 두는 게 핵심입니다.


도입 체크리스트

  • Core Web Vitals의 세 지표 의미 팀에 공유
  • Lighthouse CI를 PR 워크플로우에 추가 (핵심 페이지 2~3개부터)
  • 기준선 합의 (보통 LCP 2.5s, INP 200ms, CLS 0.1)
  • RUM 도입 (Sentry, Datadog, Vercel Analytics 등 한 가지)
  • 백분위 75 또는 95 기준으로 보기 (평균 금지)
  • 부하 테스트는 큰 릴리스 전에만 (k6로 시작)

흔한 함정

  • 평균만 본다 — 빠른 사용자 95명과 느린 사용자 5명의 평균은 “괜찮음”이 됩니다. 진실은 75~95 백분위에 있습니다.
  • 합성 측정 점수에만 매달린다 — Lighthouse 100점인 페이지가 실사용자에게는 느릴 수 있습니다. RUM과 함께 봐야 합니다.
  • 이미지·폰트 최적화를 놓친다 — 가장 큰 성능 이슈의 절반 이상이 여기서 옵니다. 이미지 포맷(WebP/AVIF), 적절한 크기, font-display 설정.
  • 레이아웃 시프트의 원인을 모른다 — CLS는 보통 폰트 로딩, 광고 삽입, 이미지의 width/height 누락에서 옵니다. 원인을 모르면 해결도 어렵습니다.
  • 부하 테스트로 프론트엔드를 검증한다 — k6는 백엔드의 부하를 보는 도구입니다. 프론트엔드 성능은 다른 영역입니다.

참고 자료


다음 편 예고

ep.08 - 운영 중 모니터링, 진짜 사용자가 마주하는 것

릴리스 전에 모든 것을 검증했다 해도, 실제 운영 환경에서 사용자가 마주하는 것은 또 다른 이야기입니다. 에러 추적, 실사용 분석, 합성 모니터링까지 - 배포 이후의 “테스트”를 어떻게 운영할지 다룹니다.