프로젝트에서 Jest 스냅샷 테스트를 도입했는데, 코드를 조금만 수정해도 자꾸 실패한다는 불평을 팀원들에게서 자주 듣는다. 심지어 실제로 버그가 아닌데도 스냅샷만 갱신하는 데 시간을 쓰고 있진 않나. 스냅샷 테스트는 컴포넌트 렌더링 결과나 API 응답을 자동으로 기록했다가, 이후 변경을 감지하는 강력한 도구다. 다만 올바른 사용법을 모르면 거짓 양성(false positive)으로 인한 노이즈만 늘어난다. 이번에는 Jest 스냅샷 테스트가 실패하는 원인별로 어떻게 대응할지, 그리고 의도하지 않은 변경을 걸러내면서 진짜 버그는 빠뜨리지 않는 전략을 완벽하게 정리해서 소개하겠다.

 

1단계. Jest 스냅샷 테스트 기초 이해하기

Jest 스냅샷은 특정 시점의 출력값(보통 JSX 렌더 결과나 JSON 응답)을 파일로 저장해두고, 나중에 코드가 실행되었을 때 그 결과가 저장된 값과 일치하는지 비교하는 테스트 방식이다. React 컴포넌트 테스트에서 자주 쓰인다.

예를 들어 Button 컴포넌트를 스냅샷 테스트하면, 처음 실행 시 Button이 렌더링한 HTML 구조가 `__snapshots__/Button.test.js.snap` 파일에 저장된다. 그 다음부터는 Button 코드가 변경되어 HTML이 달라지면 테스트가 실패한다.

 

스냅샷 테스트의 장점과 한계

장점은 명확하다. 손으로 일일이 assertion을 작성하지 않아도 UI 변경을 자동으로 추적할 수 있다. 리팩토링할 때 의도하지 않은 렌더링 변화를 빨리 발견하게 된다. 반대로 한계는, 스냅샷 파일에 저장된 내용이 정말 올바른 상태인지 검증하지는 않는다는 점이다. 첫 스냅샷이 잘못되면 그 이후로 계속 틀린 값을 기준으로 테스트하게 된다.

 

2단계. 스냅샷 테스트 실패의 주요 원인별 대응

 

원인 1. 의도한 UI 변경 (정상 상황)

버튼 텍스트를 '제출' → '등록'으로 바꾸거나, 컴포넌트 구조를 개선한 경우가 여기에 해당한다. 이 경우는 코드 변경이 맞다고 확인한 후 스냅샷을 업데이트하면 된다.

✓ 올바른 대응: 터미널에서 jest --updateSnapshot 또는 jest -u로 스냅샷을 갱신한다. 하지만 갱신 전에 변경 사항을 한 번 더 검토하는 습관이 중요하다. 스냅샷 파일(.snap)에서 무엇이 바뀌었는지 diff를 확인해야 한다.

 

원인 2. 라이브러리 업그레이드로 인한 출력 변화

React 버전을 올리면 렌더링 HTML 포맷이 미묘하게 달라질 수 있다. 또는 prettier를 업데이트하면 코드 포매팅이 변경된다. 이런 경우는 실제 버그가 아니지만 스냅샷만 계속 실패한다.

✓ 올바른 대응: 라이브러리 업그레이드의 영향을 먼저 파악하자. 변경 로그(changelog)를 확인해 의도적인 출력 변경이 맞는지 확인한 후, 스냅샷을 일괄 갱신한다. 이때 git diff __snapshots__로 변경 범위를 미리 검토해야 한다.

 

원인 3. 타임스탬프나 난수 같은 동적 값 포함

현재 시간을 표시하는 컴포넌트나, API 응답에 난수가 포함된 경우 매번 다른 값이 생성된다. 그러면 스냅샷이 계속 실패한다.

✗ 잘못된 대응: 그냥 jest -u를 반복 실행하면서 스냅샷을 계속 갱신하기. 이렇게 하면 테스트의 의미가 없어진다.

✓ 올바른 대응: Jest의 `expect.any()` 또는 `expect.stringMatching()`을 사용해 동적 값을 무시하거나 정규표현식으로 검증한다.

// ✗ 잘못된 코드: 매번 바뀌는 시간 때문에 스냅샷 실패
test('현재 시간 표시', () => {
  const component = render(<Clock />);
  expect(component).toMatchSnapshot();
});

// ✓ 올바른 코드: 시간 값을 무시하고 나머지만 검증
test('현재 시간 표시', () => {
  const component = render(<Clock />);
  expect(component).toMatchSnapshot({
    currentTime: expect.any(String)
  });
});

 

원인 4. 외부 데이터(API 응답)가 변경되었을 때

API 응답을 스냅샷으로 저장했는데, 백엔드에서 필드를 추가하거나 포맷을 바꾸면 스냅샷이 실패한다. 이 경우는 백엔드 변경이 의도된 것인지, 아니면 하위 호환성을 깨뜨리는 버그인지 먼저 확인해야 한다.

✓ 올바른 대응: API 응답 스냅샷을 저장할 때는 동일한 데이터로 테스트를 고정하거나, 핵심 필드만 검증하는 부분 스냅샷 매칭을 사용한다.

// ✓ 부분 스냅샷 매칭: 필드 추가/제거에 강함
test('사용자 정보 조회', () => {
  const userData = { id: 1, name: 'John', email: 'john@example.com', role: 'admin' };
  expect(userData).toMatchSnapshot({
    id: expect.any(Number),
    name: expect.any(String)
  });
});

 

3단계. 실전 예제: React 컴포넌트 스냅샷 테스트

 

예제 1. 기본 스냅샷 테스트
import React from 'react';
import { render } from '@testing-library/react';
import Button from './Button';

test('Button 렌더링 스냅샷', () => {
  const { container } = render(<Button label="클릭하세요" onClick={() => {}} />);
  expect(container.firstChild).toMatchSnapshot();
});

처음 실행하면 `Button.test.js.snap` 파일이 생성되고 Button HTML이 저장된다. 두 번째 실행부터는 그때의 렌더 결과와 비교한다.

 

예제 2. 동적 값을 제외한 스냅샷
import React from 'react';
import { render } from '@testing-library/react';
import UserProfile from './UserProfile';

test('사용자 프로필 카드 렌더링', () => {
  const mockUser = {
    id: 123,
    name: 'Alice',
    createdAt: '2024-01-15T10:30:00Z'
  };
  const { container } = render(<UserProfile user={mockUser} />);
  
  expect(container.firstChild).toMatchSnapshot({
    createdAt: expect.any(String) // 시간은 동적이므로 무시
  });
});

 

예제 3. 인라인 스냅샷 (코드 자동 업데이트)
import { toMatchInlineSnapshot } from '@jest/globals';

test('상품 가격 포맷팅', () => {
  const price = formatPrice(29900);
  expect(price).toMatchInlineSnapshot(`"₩29,900"`);
});

인라인 스냅샷은 스냅샷 파일이 아니라 테스트 코드에 직접 저장된다. `jest --updateSnapshot`으로 자동 갱신할 때 테스트 코드 자체가 수정된다.

 

4단계. 주의사항과 흔한 실수

 

실수 1. 스냅샷 파일을 무분별하게 갱신하기

✗ 잘못된 것: 테스트가 실패할 때마다 검토 없이 `jest -u`만 치는 경우. 이렇게 하면 새로운 버그도 스냅샷으로 저장되어 버린다.

✓ 올바른 것: 스냅샷 갱신 전에 항상 `git diff __snapshots__`로 무엇이 바뀌었는지 확인한 후 결정하기.

 

실수 2. 스냅샷 테스트만으로 충분하다고 생각하기

스냅샷은 시각적 회귀(visual regression)를 감지하는 도구일 뿐, 로직 검증은 아니다. 예를 들어 버튼을 클릭했을 때 실제로 상태가 변경되는지는 스냅샷으로 확인할 수 없다.

✓ 올바른 것: 스냅샷과 함께 상호작용 테스트(userEvent, fireEvent)를 조합해서 사용한다.

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('카운터 증가 시 렌더링과 상태 모두 확인', async () => {
  const { container } = render(<Counter />);
  
  // 초기 렌더링 스냅샷
  expect(container.firstChild).toMatchSnapshot();
  
  // 실제 상호작용 검증
  const button = screen.getByText('증가');
  await userEvent.click(button);
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

 

실수 3. 스냅샷 파일이 너무 커지는 문제

전체 페이지를 한 번에 스냅샷으로 저장하면 파일이 수백 줄이 되어 코드 리뷰도 어렵고, 소소한 변경에도 전체 diff가 터진다.

✓ 올바른 것: 컴포넌트를 작은 단위로 쪼개서 각각 스냅샷 테스트한다.

 

5단계. 팀 차원의 스냅샷 관리 전략

 

스냅샷 파일도 버전 관리에 포함하기

스냅샷 파일(`__snapshots__` 폴더)은 반드시 Git에 커밋해야 한다. 스냅샷이 없으면 다른 개발자가 테스트를 실행할 수 없다.

 

Code Review에서 스냅샷 검증하기

Pull Request에서 스냅샷이 변경되면, 리뷰어는 반드시 `__snapshots__` 파일의 diff를 확인해야 한다. 의도한 변경인지, 아니면 실수로 넘어간 버그인지 구분할 수 있는 마지막 기회다.

 

스냅샷 갱신 CI/CD 정책

CI 환경에서는 `jest --updateSnapshot`을 절대 자동 실행하지 말아야 한다. 항상 명시적으로 개발자가 갱신 명령을 실행하고 변경을 검토한 후 Push해야 한다.

 

마무리

Jest 스냅샷 테스트는 UI 변경을 효율적으로 추적할 수 있는 강력한 도구지만, 제대로 관리하지 않으면 거짓 양성으로 인한 노이즈만 늘어난다. 동적 값은 `expect.any()` 같은 매처로 제외하고, 스냅샷 갱신 전에는 항상 변경 사항을 검토하는 습관이 중요하다. 특히 팀 프로젝트에서는 스냅샷 파일을 명확하게 버전 관리하고, Code Review 과정에서 의도하지 않은 변경을 걸러내야 한다. 이 글의 동적 값 처리와 부분 스냅샷 매칭 기법을 참고해서, 스냅샷 테스트를 올바르게 도입하면 리팩토링 시 의도하지 않은 버그를 조기에 발견할 수 있을 것이다.