C.W.K.
Stream
Lesson 04 of 04 · published

Snapshot 예절 (대체로: 하지 마)

~16 min · vitest-setup, snapshots, anti-pattern

Level 0테스트 호기심
0 XP0/32 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"읽지 않는 snapshot 은 테스트가 아냐. main 에 커밋된 diff 야."

Snapshot 테스트가 실제로 하는 일

Snapshot 테스트는 어떤 값을 (객체, 렌더된 DOM 트리, 문자열) 직렬화해서 저장하고, 다음 실행에선 live 값을 저장된 값과 비교해. 일치하면 통과. 다르면 실패 — Vitest 가 diff 를 보여줘.

그게 다야. 통상적 의미의 단언이 없어 — output 이 뭐여야 하는지 선언하는 게 아니라 지난번에 뭐였든 이번에도 그거여야 함 만 선언하는 거지. 매혹적인 부분은 코드를 얼마나 적게 쓰는지야. 위험한 부분도 같아.

두 모양 — Inline 과 File

Inline snapshot 은 테스트 파일 자체에 살아, toMatchInlineSnapshot 에 multi-line 문자열 인자로. 리뷰에서 살아남아 — diff 가 PR 안에 바로 보이니까. 짧고 안정적인 output 에 좋아.

File snapshot 은 테스트 파일과 별개의 sibling __snapshots__/ 디렉토리에 살아. 더 큰 captured output 에 확장되지만 리뷰 주의에서 잘 빠져 — 사람들이 안 읽고 PR 승인해. 아껴 써.

Snapshot 리뷰 테스트: 이 snapshot 이 바뀔 때, 누군가 실제로 diff 를 읽을까? 답이 '아니' 면 그 snapshot 은 도장이지 테스트가 아냐. 지우고 진짜 단언 짜.

업데이트 흐름 (그리고 그 함정)

Snapshot 이 정당하게 바뀐 output 때문에 실패하면 (refactor 했어, 필드 추가했어) 업데이트해: vitest -u 또는 vitest --update. 이게 현재 output 에서 snapshot 파일을 재생성하고, 너는 diff 를 커밋.

함정은 근육 기억이야. 'snapshot 실패 → U 눌러 → 커밋' 사이클 서너 번 후엔 뭐가 바뀌었는지 보는 행위가 사라져. 이제 snapshot 건드린 의도치 않은 regression 도 U 와 함께 망각으로 들어가. 테스트는 몇 달 전에 아무것도 잡지 못하게 됐는데, 실패는 항상 U 로 끝났으니까 아무도 눈치 못 챘어.

해법은 부분적으로 문화적 (snapshot-only diff 를 리뷰에서 코드 diff 와 같은 정밀로 다뤄) 이고 부분적으로 기술적 (테스트 의도와 무관한 이유로 바뀌는 걸 snapshot 하지 마).

Snapshot 이 값어치할 때

  • 에러 메시지: 짧고, 안정적이고, 정확한 텍스트가 중요. expect(formatError(input)).toMatchInlineSnapshot('"Expected number, got string"') 는 괜찮아.
  • 정규화된 JSON 계약: 잠가두고 싶은 작은 response 모양. 날짜 / id 를 벗기는 serializer 와 짝지어서 snapshot 이 결정적이게 만들어.
  • CLI output: 특정 라인을 출력하는 command-line 도구는 렌더된 output 에 대한 snapshot 테스트의 혜택을 봐.

안 될 때

  • React 컴포넌트의 큰 HTML 트리: Tailwind 클래스 변경마다 snapshot 깨져. 신호 대 노이즈 빠르게 떨어져.
  • 날짜, id, hash 가 들어간 output: default 로 비결정적. serializer 추가하거나 snapshot 말고 모양에 단언.
  • 집중된 단언을 짤 수 없는 거. Snapshot 은 뭐가 참이어야 하는지 생각하는 것의 대체가 아냐.
Snapshot 안 함이 default. expect(thing.foo).toBe('bar') 짤 수 있으면 그걸 써. Snapshot 은 output 이 진짜로 다루기 힘들고 진짜로 lock 다운이 필요한 fallback 이야. 두 절반 다 중요해 — 다루기 힘들기만 한 건 이유가 안 돼.

Code

Inline snapshot — 작고, 안정적이고, 리뷰에서 보임·typescript
import { describe, it, expect } from 'vitest';
import { formatValidationError } from './errors';

describe('formatValidationError', () => {
  it('renders the wrong-type case', () => {
    expect(formatValidationError({ field: 'age', expected: 'number', got: 'string' }))
      .toMatchInlineSnapshot('"age: expected number, got string"');
  });

  it('renders the out-of-range case', () => {
    expect(formatValidationError({ field: 'age', expected: '0..120', got: -5 }))
      .toMatchInlineSnapshot('"age: expected 0..120, got -5"');
  });
});
File snapshot — 별개 파일, 리뷰에서 무시하기 더 쉬움·typescript
// File snapshot — produces __snapshots__/format.test.ts.snap
import { describe, it, expect } from 'vitest';
import { renderInvoice } from './invoice';

describe('renderInvoice', () => {
  it('matches the canonical structure', () => {
    const invoice = { id: 'INV-001', total: 99.99, currency: 'USD' };
    // Stored in __snapshots__/. Inspect the file after the first run.
    expect(renderInvoice(invoice)).toMatchSnapshot();
  });
});

// __snapshots__/invoice.test.ts.snap (auto-generated)
//
// exports[`renderInvoice > matches the canonical structure 1`] = `
// {
//   "id": "INV-001",
//   "lineItems": [],
//   "subtotal": 99.99,
//   "tax": 0,
//   "total": 99.99
// }
// `;
Snapshot 업데이트 — 그리고 왜 `u` 누르는 근육 기억을 의심해야 하는지·bash
# When a snapshot test fails because output legitimately changed:
npx vitest -u

# Or update only a specific file:
npx vitest src/lib/invoice.test.ts -u

# Watch mode: press 'u' to update failed snapshots
#                press 'i' to update inline snapshots only
안티 패턴 — 컴포넌트 통째로 snapshot·typescript
// ANTI-PATTERN — don't do this
import { render } from '@testing-library/react';
import { Dashboard } from './dashboard';

it('renders the dashboard', () => {
  const { container } = render(<Dashboard />);
  expect(container.innerHTML).toMatchSnapshot();
  // ⚠️ This will break on every Tailwind class change, every text tweak,
  //    every prop reshuffling. Nobody will read the diff. They'll press U.
  //    Write focused assertions instead — toBeInTheDocument(), toHaveRole(),
  //    etc. (We'll cover those in the components track.)
});

External links

Exercise

너의 코드베이스에서 snapshot 테스트 하나 찾아 (아니면 어느 오픈소스 프로젝트 — GitHub 에서 toMatchSnapshot 검색). 읽어. 물어봐: 이 snapshot 이 바뀔 때 누가 diff 를 꼼꼼히 읽을까, 아니면 U 누르고 넘어갈까? 후자면 집중된 단언으로 대체 짜 — toBe, toContain, toMatchObject. 둘 비교해: 어느 게 네가 신경 쓸 regression 을 잡을까?
Hint
대체 단언이 보통 snapshot 보다 길어. 그게 핵심이야 — 명시 버전은 뭐가 중요한지 말하고, snapshot 은 '지난번에 뭐였든' 만 말해. 길이는 명확성의 비용이야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.