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

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

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

  • 테스트할 가치가 높은 코드를 고르는 기준
  • 커버리지 숫자의 진짜 의미와 함정
  • 우리 데모 앱에 적용할 테스트 전략
커버리지 숫자는 높지만 실제 검증은 비어 있는 상황을 대비해 보여주는 다이어그램
커버리지 숫자는 높지만 실제 검증은 비어 있는 상황을 대비해 보여주는 다이어그램

무엇을 먼저 테스트할까

모든 코드가 같은 무게를 갖지는 않아요. 자주 바뀌고, 틀리면 크게 아픈 코드가 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 붙이기 — 첫 초록불까지

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