Testing/Snapshot tests

김동현·2026년 3월 22일

스냅샷 테스트 (Snapshot tests)

스냅샷 테스팅이란 특정 상태의 컴포넌트를 렌더링한 후, 그 결과물인 DOM이나 HTML의 '스냅샷'을 찍어서 이전에 찍어둔 스냅샷과 비교하는 테스트 방법입니다.

스냅샷 테스트는 만들기는 아주 쉽지만, 스냅샷 안에 너무 많은 정보가 담겨 있으면 나중에 유지보수하기가 까다로워지고 노이즈(불필요한 실패 알림)가 많이 생길 수 있습니다. 그래서 UI 컴포넌트를 테스트할 때는 직관적으로 리뷰하기 쉬운 시각적 테스트(visual tests)나 기능에 집중하는 인터랙션 테스트(interaction tests)가 더 적합한 경우가 많습니다.

하지만, '에러가 올바르게 잘 던져지는지(thrown)' 확인해야 하는 상황처럼, 반드시 스냅샷 테스트가 필요한 경우도 분명히 존재합니다.

스토리북으로 만든 스토리들은 Jest나 Vitest 같은 다른 테스트 환경에서도 스냅샷 테스트의 훌륭한 재료로 재사용할 수 있어요! 이를 위해 스토리북은 'Portable Stories API'라는 걸 제공합니다. 이 API는 여러분의 스토리를 다양한 어노테이션(args, decorators, parameters 등)과 잘 조합해서 테스트 환경에서 렌더링 가능한 형태로 예쁘게 만들어 줍니다. Portable Stories는 다음 환경에서 사용할 수 있어요:

ℹ️ 혹시 Storyshots를 사용한 스냅샷 테스팅을 찾고 계셨나요? Storyshots는 사용이 중단(deprecated)되었으며 더 이상 유지보수되지 않습니다. 대신 방금 말씀드린 Portable Stories API를 사용하시는 것을 강력히 권장합니다. 기존 테스트를 어떻게 마이그레이션해야 할지 궁금하시다면 Storyshots 마이그레이션 가이드를 확인해 주세요.

Portable Stories 시작하기 (Get started with Portable Stories)

만약 Storybook Test를 사용 중이시라면, 여러분의 프로젝트는 이미 Vitest 환경에서 Portable Stories를 사용하도록 완벽하게 설정되어 있습니다.

하지만 Storybook Test를 사용하지 않거나 완전히 다른 테스팅 환경을 사용하고 싶으시다면, 아래의 관련 문서를 꼭 참고해 주세요:

Portable Story로 스냅샷 테스트하기 (Snapshot testing a portable story)

재사용 가능한 스토리를 가져와 스냅샷 테스트를 하는 과정은 아주 직관적입니다. Portable Stories API가 제공하는 composeStories 함수를 사용해서 렌더링 가능한 엘리먼트를 얻은 다음, 그걸 렌더링하고 스냅샷을 찍어서 비교하면 끝이죠.

아래 예시는 Vitest 환경에서 Button 컴포넌트의 스토리 중 하나를 재사용하여 렌더링한 뒤, 그 HTML 스냅샷이 이전과 일치하는지 확인(assert)하는 코드입니다.

// test/Button.test.js|ts
// @vitest-environment jsdom
 
import { expect, test } from "vitest";
 
import { composeStories } from "@storybook/react";
 
import * as stories from "./Button.stories";
 
const { Primary } = composeStories(stories);
 
test("Button snapshot", async () => {
  await Primary.run();
  expect(document.body.firstChild).toMatchSnapshot();
});

테스트를 처음 실행하면 새로운 스냅샷 파일이 생성(삽입)됩니다. 이후 코드를 수정하고 테스트를 다시 돌렸을 때 스냅샷이 일치하지 않으면, 테스트가 실패하면서 대략 아래와 같은 결과 화면을 보게 될 거예요:

FAIL  src/components/ui/Button.test.ts > Button snapshot
Error: Snapshot `Button snapshot 1` mismatched
 
- Expected
+ Received
 
  <div>
    <button
-      class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive bg-primary text-primary-foreground shadow-xs hover:bg-primary/90 h-9 px-4 py-2 has-[>svg]:px-3"
+      class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive bg-primary text-primary-foreground shadow-xs hover:bg-primary/90 h-9 px-3 py-2 has-[>svg]:px-3"
       data-slot="button"
    >
      Button
    </button>
  </div>

💡 잠깐, 위 코드에서 도대체 뭐가 바뀌었는지 찾는 데 얼마나 걸리셨나요? (정답: px-4px-3으로 바뀌었습니다 😅)

이것이 바로 UI 컴포넌트의 '외관(appearance)'을 테스트할 때 시각적 테스트(visual tests)가 훨씬 더 좋은 이유입니다. 시각적 테스트를 사용하면 무엇이 바뀌었는지 한눈에 바로 알 수 있을 뿐만 아니라, 단순히 적용된 CSS 텍스트가 아니라 '사용자가 실제로 보게 될 화면' 그 자체를 테스트할 수 있거든요.

에러 발생 여부 검증하기 (Verifying an error is thrown)

일반적인 스냅샷 테스트 방법을 알았으니, 이제 아주 자주 쓰이는 활용 사례 중 하나를 살펴볼게요. 바로 '우리가 의도한 에러가 제대로 발생하는지' 확인하는 테스트입니다.

아래 예시에는 아주 단순한 React Button 컴포넌트가 있습니다. 이 컴포넌트는 어쩌다 보니 doNotUseThisItWillThrowAnError라는 무시무시한 이름의 prop을 받는데, 이름 그대로 이 prop을 사용하면 (당연하게도) 에러를 퉤! 하고 뱉어냅니다.

// Button.tsx
function Button(props) {
  if (props.doNotUseThisItWillThrowAnError) {
    throw new Error("I tried to tell you...")
  }
 
  return <button {...props} />
}

이제 이 prop을 args로 전달하는 스토리를 하나 만들어 봅니다. 이 스토리는 스토리북 사이드바에 나타나거나 Storybook Test에 의해 자동으로 테스트되는 걸 막기 위해, 기본 태그인 devtest를 제거하는 설정(!dev, !test)을 해두었습니다.

// Button.stories.js
export const ThrowError = {
  tags: ['!dev', '!test'],
  args: {
    doNotUseThisItWillThrowAnError: true,
  },
}

마지막으로 테스트 파일에서는, 이 스토리를 실행했을 때 특정 메시지를 담은 에러가 의도한 대로 잘 발생하는지 확인(assert)하는 코드를 작성합니다.

// Button.test.ts
// @vitest-environment jsdom
 
import { expect, test } from "vitest";
 
import { composeStories } from "@storybook/react";
 
import * as stories from "./Button.stories";
 
const { ThrowError } = composeStories(stories);
 
test("Button throws error", async () => {
  await expect(ThrowError.run()).rejects.toThrowError('I tried to tell you...');
});

설명을 위해 아주 간단하게 만든 예시지만, 이 테크닉은 잘못된 입력값이 들어간 폼(form)이나 네트워크 장애 상황을 시뮬레이션하는 등 훨씬 복잡한 시나리오에도 똑같이 적용할 수 있습니다!

테스트 러너로 스냅샷 테스트하기 (Snapshot testing with the test-runner)

만약 프로젝트 환경상 Portable Stories를 사용할 수 없다면, 테스트 러너(test-runner)를 사용해서 스냅샷 테스트를 돌리는 방법도 있습니다. 프로젝트에 테스트 러너와 스냅샷 테스트를 세팅하는 자세한 방법은 테스트 러너 문서를 따라 해 보세요.

자주 묻는 질문 (FAQ)

스냅샷 테스트(snapshot tests)와 시각적 테스트(visual tests)의 차이점이 무엇인가요?

시각적 테스트는 스토리가 렌더링된 화면의 '이미지'를 캡처해서 베이스라인 이미지와 비교합니다. 반면 스냅샷 테스트는 렌더링된 'DOM이나 HTML 텍스트'의 스냅샷을 찍어서 베이스라인 DOM/HTML 텍스트와 비교하죠.

결론적으로, 컴포넌트의 '겉모습(appearance)'이 제대로 나오는지 확인하는 데는 시각적 테스트가 훨씬 적합합니다. 스냅샷 테스트는 시각적인 부분 외의 데이터 출력값을 검증하거나, DOM 구조 자체가 변하지 않았음을 보장해야 할 때 아주 유용하게 쓰입니다.

더 유용한 테스팅 관련 자료들

profile
프론트에_가까운_풀스택_개발자

0개의 댓글