「프론트엔드 테스트 제대로 하기」 시리즈의 18편입니다. 전체 목차 보기 · 용어집

커버리지 리포트를 열면 숫자가 네 종류나 나옵니다 — Stmts, Branch, Funcs, Lines. 뭘 봐야 할까요? 이 글은 그 리포트를 읽는 법입니다: 어떤 숫자가 정직한지(스포일러: 브랜치), 빨간 줄에서 무엇을 채울지, 그리고 숫자가 못 보여주는 팀 건강은 어디서 볼지까지.

시리즈로 따라오셨다면 — 전략 편에서 “커버리지는 목표가 아니라 참고 지표"라고 했던 그 이야기의 실전편입니다. 검색으로 처음 오셨어도 괜찮아요. Vitest 기준이지만 리포트 읽는 법 자체는 어떤 도구든 같습니다.

실습 코드: 이 편의 상태는 step-18 태그에 고정돼 있습니다. 커버리지 설정은 환경 세팅 편에서 이미 들어갔기 때문에, 코드는 step-17과 같고 npm run coverage만 돌려보면 됩니다.

숫자를 읽는 눈을 기르는 게 오늘의 목표입니다.

  • 라인·브랜치·함수 커버리지의 차이
  • 리포트에서 ‘빠진 곳’ 찾기
  • 커버리지 외의 건강 지표
커버리지 리포트 터미널 출력에 Branch 열이 파랗게 강조된 그림 - All files는 Stmts·Funcs·Lines가 100인데 Branch만 86.36이고, 오른쪽에는 네 숫자가 각각 무엇을 묻는지(구문·분기·함수·라인) 설명이 붙어 있습니다. 아래에는 100%인 파일이 표에서 생략된다는 설명과 coverage/index.html 안내가 있습니다
커버리지 리포트 터미널 출력에 Branch 열이 파랗게 강조된 그림 - All files는 Stmts·Funcs·Lines가 100인데 Branch만 86.36이고, 오른쪽에는 네 숫자가 각각 무엇을 묻는지(구문·분기·함수·라인) 설명이 붙어 있습니다. 아래에는 100%인 파일이 표에서 생략된다는 설명과 coverage/index.html 안내가 있습니다

네 숫자부터 통성명

리포트의 네 기둥은 이렇게 읽습니다.

  • Stmts(구문) — 전체 실행 단위 중 테스트가 지나간 비율. Lines와 거의 같이 움직여요
  • Branch(분기)if/else, 삼항, &&/|| 같은 갈림길의 양쪽을 다 가봤는지
  • Funcs(함수) — 정의된 함수 중 한 번이라도 불린 비율
  • Lines(라인) — 코드 줄 기준 실행 비율. 가장 직관적이지만, 곧 보듯 가장 후합니다

기억할 건 하나예요 — 넷 중 셋은 “지나갔느냐"를 묻고, 브랜치만 “양쪽 다 가봤느냐"를 묻습니다.


커버리지 켜기

환경 세팅 편의 설정에 이미 준비돼 있어요. 단, 한 가지 함정이 있습니다 — exclude를 안 걸면 mocks·셋업 파일까지 분모에 들어가서 숫자가 후해집니다. 앱 코드만 세도록 걸러주세요. (repo vite.config.ts)

ts
coverage: {
  provider: 'v8',
  reporter: ['text', 'html'],
  // 앱 코드만 측정: 목·셋업·엔트리는 커버리지 대상이 아니다
  exclude: ['node_modules/', 'dist/', 'e2e/', 'src/mocks/', 'src/test/', 'src/main.tsx', '*.config.ts'],
}

돌려보면 — 데모 repo의 실제 리포트입니다.

bash
npm run coverage

 % Coverage report from v8
-----------------|---------|----------|---------|---------|-------------------
File             | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------|---------|----------|---------|---------|-------------------
All files        |     100 |    86.36 |     100 |     100 |
 src/components  |     100 |    78.57 |     100 |     100 |
  UserSearch.tsx |     100 |    78.57 |     100 |     100 | 16-22,58
-----------------|---------|----------|---------|---------|-------------------

“파일이 이것뿐이야?” 싶을 텐데 — 텍스트 표는 가득 찬 파일을 생략합니다. users.ts, debounce.ts 등은 전부 100%라 행이 안 보이는 것뿐, 위 All files 합계에는 들어 있어요. 그러니 이 표는 사실상 ‘덜 채워진 파일 목록’입니다. 파일 전체를 훑고 싶으면 html 리포터를 여세요(coverage/index.html) — 실행 안 된 줄이 빨갛게 표시됩니다.


브랜치 커버리지를 보라

라인만 보면 속기 쉬워요. 데모의 filterByQuery로 실험해봅시다.

ts
export function filterByQuery(users: User[], query: string): User[] {
  const k = query.trim().toLowerCase()
  if (!k) return users          // ← 갈림길
  return users.filter(...)
}

“bob을 검색하면 걸러진다” 테스트 하나만 있다고 치면 — 위의 거의 모든 줄이 실행되니 라인은 후하게 높습니다. 하지만 if (!k)의 갈림길에서 우리는 늘 “아니오” 쪽으로만 걸었어요. 빈 검색어라는 “예” 경로는 한 번도 안 가본 거죠. 브랜치 커버리지만 이걸 50%라고 정직하게 신고합니다. 빈 검색어 테스트를 하나 더하면 그제야 양쪽 다 100%.

라인 커버리지와 브랜치 커버리지의 차이를 보여주는 다이어그램 - 검색어가 있는 테스트 하나만 있으면 거의 모든 줄이 실행돼 라인 커버리지는 높지만, if문의 갈림길에서 빈 검색어 경로는 한 번도 가지 않아 브랜치 커버리지는 50%로 남습니다. 빈 검색어 테스트를 추가하면 양쪽 경로가 모두 방문되어 둘 다 100%가 됩니다
라인 커버리지와 브랜치 커버리지의 차이를 보여주는 다이어그램 - 검색어가 있는 테스트 하나만 있으면 거의 모든 줄이 실행돼 라인 커버리지는 높지만, if문의 갈림길에서 빈 검색어 경로는 한 번도 가지 않아 브랜치 커버리지는 50%로 남습니다. 빈 검색어 테스트를 추가하면 양쪽 경로가 모두 방문되어 둘 다 100%가 됩니다

단위 테스트 편에서 경계값(“비어 있으면? 없으면?")을 그토록 강조한 이유가 여기서 숫자로 확인돼요.


아까 그 표, 실제로 읽어봅시다

이론을 배웠으니 아까 실제 리포트로 돌아가죠. Stmts·Funcs·Lines는 전부 100%인데 Branch만 86.36% — 방금 배운 그대로입니다. 셋은 “지나갔다"고 만점을 줬지만, 브랜치만 “안 가본 갈림길이 있다"고 신고했어요.

Uncovered Line #s의 두 구간을 코드에서 찾아가 보면, 판단 연습으로 딱 좋습니다.

  • 58행 — 검색 결과가 0건일 때의 “검색 결과가 없습니다.” 화면. 어라, 이 화면을 검증하는 테스트가 없었네요. 사용자가 실제로 자주 만나는 경로인데요. → 채울 가치가 있습니다
  • 16~22행 — 컴포넌트가 사라진 뒤 서버 응답이 도착했을 때를 막는 가드(if (active))의 “이미 사라진” 쪽. 재현하려면 언마운트 타이밍을 인위적으로 조작해야 하고, 로직은 한 줄이에요. → 당장은 안 채워도 됩니다
빈 곳 두 군데를 나란히 비교한 그림 - 58행 검색 결과 없음 화면은 사용자가 자주 만나고 테스트도 쉬워서 '지금 채울 가치가 있음', 16~22행 if (active) 가드의 뒷면은 드물게 발생하고 언마운트 타이밍을 맞춰야 해서 '당장은 안 채워도 됨'으로 판정됩니다
빈 곳 두 군데를 나란히 비교한 그림 - 58행 검색 결과 없음 화면은 사용자가 자주 만나고 테스트도 쉬워서 '지금 채울 가치가 있음', 16~22행 if (active) 가드의 뒷면은 드물게 발생하고 언마운트 타이밍을 맞춰야 해서 '당장은 안 채워도 됨'으로 판정됩니다

같은 빨간 줄이라도 무게가 다릅니다. 리포트는 빈 곳을 알려줄 뿐, 어디를 채울지는 사람이 정해요 — 그리고 그 기준은 “사용자가 겪을 확률"입니다.


빠진 곳 찾기용 + 숫자 너머

커버리지의 올바른 용도는 ‘점수 자랑’이 아니라 ‘테스트 안 된 위험한 분기 찾기’입니다. 리포트의 빨간 줄을 훑고, 그중 중요한 것만 채우세요.

그리고 팀 건강은 다른 지표가 더 잘 보여줄 때가 많아요: flaky 비율, 평균 테스트 실행 시간, 실패 후 복구까지 걸리는 시간(MTTR). CI 편에서 세운 파이프라인의 리포트에서 이런 흐름을 관찰해보세요.


한 장 요약

  • 커버리지를 켜기 전에 exclude부터 — mocks·셋업·엔트리가 분모에 들어가면 숫자가 후해집니다
  • 네 숫자 중 브랜치가 가장 정직 — 라인은 if의 한쪽만 타도 채워지니까
  • 텍스트 표는 가득 찬 파일을 생략 — 사실상 ‘덜 채워진 파일 목록’이고, 전체는 html 리포트에서
  • html 리포트(coverage/index.html)의 빨간 줄 = 아무 테스트도 안 지나간 곳 — 같은 빨간 줄도 무게가 다르니(사용자가 겪을 확률) 위험한 분기만 채우면 됩니다
  • 올바른 용도는 점수 자랑이 아니라 빈 곳 찾기 — 100%를 목표로 삼는 순간 전략 편의 함정들이 자라납니다
  • 팀 건강은 커버리지 밖에도: flaky 비율, 실행 시간, 복구 시간(MTTR)을 CI 리포트에서 관찰

마치며

4장을 마쳤어요. 이제 이 시리즈의 하이라이트, AI 시대의 테스트로 들어갑니다.

커버리지 배지를 90%로 올리는 가장 빠른 방법은… 테스트를 늘리는 게 아니라, 테스트 안 된 파일을 리포트에서 빼는 겁니다. (그러지 맙시다.)

레벨업: 커버리지 리포트를 읽고 ‘진짜 빈 곳’을 찾아 채울 수 있습니다.

다음 편: AI로 테스트 생성·보강 — 검증의 몫은 사람

낯선 용어가 있었다면 — 용어집에 전부 한 줄씩 정리돼 있어요.