# 무엇을 얼마나 테스트할까 — 커버리지 100%의 함정

> 테스트는 많이 짤수록 좋은 걸까요? 무엇을 먼저 테스트할지 판단하는 기준과, 커버리지 숫자에 속지 않는 법을 정리합니다.

**Published:** 2026-07-04 | **Updated:** 2026-07-05

---


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

지난 편에서 "왜 테스트인가"를 이야기했다면, 이번엔 현실적인 질문입니다. **"그래서 뭘, 얼마나 테스트해야 하죠?"** 시간은 유한하고, 모든 걸 테스트할 수는 없으니까요.

이번 편의 질문은 하나예요. 답을 세 조각으로 나눠봤습니다.

- 테스트할 가치가 높은 코드를 고르는 기준
- 커버리지 숫자의 진짜 의미와 함정
- 우리 데모 앱에 적용할 테스트 전략

{{< img src="images/contents/coverage-trap.png" alt="커버리지 숫자는 높지만 실제 검증은 비어 있는 상황을 대비해 보여주는 다이어그램" >}}

---

## 무엇을 먼저 테스트할까

모든 코드가 같은 무게를 갖지는 않아요. **자주 바뀌고, 틀리면 크게 아픈** 코드가 1순위입니다.

- 핵심 비즈니스 로직(계산·검증·상태 전이)
- 버그가 반복적으로 났던 곳
- 많은 곳에서 쓰이는 공용 유틸

반대로, 프레임워크가 보장하는 것(단순 렌더)이나 곧 사라질 코드는 후순위예요.

우리 데모 앱으로 말하면 — 검색 필터(`filterByQuery`)는 1순위, "제목이 렌더된다" 같은 건 **스모크 테스트**(연기가 나는지만 보는 것처럼, 일단 켜지고 뜨는지만 확인하는 최소한의 테스트) 하나면 충분합니다.

---

## 잠깐, 커버리지가 뭐죠?

잠깐 용어부터 살펴볼까요? **커버리지(coverage)는 *테스트를 돌렸을 때 실제로 실행된 코드의 비율*입니다.** 테스트 러너가 "당신의 테스트는 전체 코드의 몇 %를 지나갔습니다"라고 알려주는 숫자예요.

```ts
export function filterByQuery(users: User[], query: string) {
  const k = query.trim().toLowerCase()   // ① 실행됨
  if (!k) return users                   // ② 조건 평가됨 (false)
  return users.filter(/* ... */)         // ③ 실행됨
}

// 테스트가 filterByQuery(users, 'alice') 하나만 호출했다면?
// 세 줄을 모두 '지나갔으니' 라인 커버리지는 100%.
// 하지만 ②가 true가 되는 "빈 검색어" 갈래는 한 번도 안 탔습니다.
```

같은 코드인데 "몇 %냐"는 세는 방법에 따라 달라지죠. 이 라인/브랜치 구분과 리포트 읽는 법은 추후 자세히 다루고, 오늘은 그보다 먼저 — 이 숫자를 **어떻게 대해야 하는지**부터 봅니다.

---

## 커버리지 100%의 함정

커버리지는 "실행된 줄"을 셀 뿐, "제대로 검증했는지"는 모릅니다.

```tsx
// 커버리지는 올라가지만 아무것도 검증하지 못하는 테스트
it('renders', () => {
  render(<App />) // assert가 없다!
})
```

이 테스트는 컴포넌트의 모든 렌더 경로를 '실행'하므로 커버리지를 꽤 올려줍니다. 하지만 화면이 통째로 깨져도 통과하죠.

비슷한 사촌이 둘 더 있습니다.

```tsx
// 함정 2: 스냅샷 남발 — "뭔가 바뀌면 실패"일 뿐, 무엇이 옳은지는 모른다
it('matches snapshot', () => {
  expect(render(<UserSearch />).container).toMatchSnapshot()
})
// 실패하면? 대부분 내용 확인 없이 스냅샷만 갱신하고 넘어가죠.
```

```tsx
// 함정 3: 구현 박제 — 리팩터링만 해도 깨진다
it('calls setState twice', () => {
  const spy = vi.spyOn(React, 'useState')
  render(<UserSearch />)
  expect(spy).toHaveBeenCalledTimes(2)  // 사용자와 무관한 내부 사정
})
```

(스냅샷 테스트도, `vi.spyOn` 같은 스파이도 나중에 각각 제대로 배웁니다. 아직 안 배운 도구라도 괜찮아요 — 지금은 "이런 식의 함정이 있다"는 것만 눈에 담아두면 됩니다.)

셋의 공통점: **커버리지는 올리지만 사용자를 지키지 못합니다.** 숫자를 목표로 삼는 순간, 이런 테스트가 늘어납니다. **커버리지는 참고 지표지 목표가 아니에요.**

---

## 이 시리즈의 전략

우리는 아래(단위)부터 위(E2E)로 쌓되, 각 층에서 "사용자 관점의 검증"을 우선합니다.

| 층 | 대상 | 도구 | 분량 |
|---|------|------|------|
| 단위 | `src/lib`의 순수 함수 | Vitest | 많이 |
| 컴포넌트 | `UserSearch` 상호작용 | Testing Library + MSW | 핵심 흐름 |
| E2E | 검색 시나리오 + 접근성 | Playwright + axe | 적게, 굵게 |

커버리지는 '빠진 곳 찾기'용으로만 씁니다.

---

> 커버리지 100%를 달성한 팀이 정작 배포 후 장애를 겪는 건 드문 일이 아니에요. 숫자는 초록불인데 사용자는 빨간불인 거죠. **무엇을 검증했는가**가 몇 %인가보다 중요합니다.

---

## 정리하며

테스트는 양이 아니라 **어디에 힘을 주느냐**의 문제예요. 다음 편부터는 실제로 데모 앱을 세팅하고 첫 테스트를 초록불로 만들어봅니다.

"커버리지 100% 만들었어요!" 라는 PR을 열어보면 절반이 `expect(1).toBe(1)`인 경우가... 있습니다. 우리는 그러지 말기로 해요.

> **레벨업**: 무엇을 먼저 테스트할지 고르고, 커버리지 숫자에 속지 않는 눈이 생겼습니다.

> **다음 편**: 데모 앱에 Vitest 붙이기 — 첫 초록불까지

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

