# 커버리지 읽는 법과 품질 지표

> 커버리지 리포트의 Stmts·Branch·Funcs·Lines, 이 네 숫자 중 뭘 믿어야 할까요? 라인은 후하고 브랜치가 정직한 이유를 실제 리포트로 확인하고, flaky 비율·MTTR 같은 커버리지 너머의 팀 건강 지표까지 다룹니다.

**Published:** 2026-08-04 | **Updated:** 2026-08-04

---


> 「프론트엔드 테스트 제대로 하기」 시리즈의 **18편**입니다. [전체 목차 보기](/series/프론트엔드-테스트-제대로-하기/) · [용어집](/posts/frontend-testing-glossary/)

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

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

> 실습 코드: 이 편의 상태는 [`step-18` 태그](https://github.com/IsaacEryn/frontend-testing-lab/tree/step-18)에 고정돼 있습니다. 커버리지 설정은 [환경 세팅 편](/posts/frontend-testing-setup-vitest/)에서 이미 들어갔기 때문에, 코드는 `step-17`과 같고 `npm run coverage`만 돌려보면 됩니다.

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

- 라인·브랜치·함수 커버리지의 차이
- 리포트에서 '빠진 곳' 찾기
- 커버리지 외의 건강 지표

{{< img src="images/contents/coverage-report.png" alt="커버리지 리포트 터미널 출력에 Branch 열이 파랗게 강조된 그림 - All files는 Stmts·Funcs·Lines가 100인데 Branch만 86.36이고, 오른쪽에는 네 숫자가 각각 무엇을 묻는지(구문·분기·함수·라인) 설명이 붙어 있습니다. 아래에는 100%인 파일이 표에서 생략된다는 설명과 coverage/index.html 안내가 있습니다" >}}

---

## 네 숫자부터 통성명

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

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

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

---

## 커버리지 켜기

[환경 세팅 편](/posts/frontend-testing-setup-vitest/)의 설정에 이미 준비돼 있어요. 단, 한 가지 함정이 있습니다 — **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%.

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

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

---

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

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

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

- **58행** — 검색 결과가 0건일 때의 "검색 결과가 없습니다." 화면. 어라, **이 화면을 검증하는 테스트가 없었네요.** 사용자가 실제로 자주 만나는 경로인데요. → **채울 가치가 있습니다**
- **16~22행** — 컴포넌트가 사라진 뒤 서버 응답이 도착했을 때를 막는 가드(`if (active)`)의 "이미 사라진" 쪽. 재현하려면 언마운트 타이밍을 인위적으로 조작해야 하고, 로직은 한 줄이에요. → **당장은 안 채워도 됩니다**

{{< img src="images/contents/coverage-gaps.png" alt="빈 곳 두 군데를 나란히 비교한 그림 - 58행 검색 결과 없음 화면은 사용자가 자주 만나고 테스트도 쉬워서 '지금 채울 가치가 있음', 16~22행 if (active) 가드의 뒷면은 드물게 발생하고 언마운트 타이밍을 맞춰야 해서 '당장은 안 채워도 됨'으로 판정됩니다" >}}

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

---

## 빠진 곳 찾기용 + 숫자 너머

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

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

---

## 한 장 요약

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

---

## 마치며

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

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

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

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

> 낯선 용어가 있었다면 — [용어집](/posts/frontend-testing-glossary/)에 전부 한 줄씩 정리돼 있어요.

