LabHub

블로그

프런트엔드 테스트 전략 2025 — Vitest·Jest·Bun·Testing Library·Playwright·Storybook·MSW·Visual Regression·AI 완전 가이드

한국어English日本語

프롤로그 — "테스트, 꼭 써야 하나요?"

신입 개발자에게 물어보면 절반은 "당연하죠", 나머지 절반은 "시간 없어서..."라고 답한다. 그런데 5년 차가 되면 질문 자체가 바뀐다. "왜 우리 코드는 리팩터링이 이렇게 무서울까?"

답은 단순하다. 테스트가 없기 때문이다.

테스트는 버그를 잡는 도구가 아니다. 버그는 QA·RUM·Sentry·프로덕션에서 잡힌다. 테스트의 진짜 가치는 "내가 방금 고친 게 다른 걸 깨뜨리지 않았다"는 확신을 주는 것, 그래서 리팩터링을 두려워하지 않게 하는 것이다.

2025년 프런트엔드 테스트의 전환점:

  1. Vitest의 승세 확정 — 2022년 등장 후 2024~2025년에 Jest를 실질적으로 대체. Vite·ESM·TypeScript 친화, Jest API 호환
  2. Bun Test의 경쟁 참여 — Bun 런타임에 내장된 초고속 테스트 러너. 단독 러너로는 아직 Vitest 대안에 못 미치지만 속도는 충격적
  3. Playwright의 E2E 1강화 — Cypress를 넘어 2024년 주간 다운로드 기준 1위. 모든 브라우저·모바일 에뮬레이션 + 트레이싱·Visual + AI Copilot 통합
  4. Storybook 9 — 테스트 러너 내장, Interaction Test·Visual Test·A11y Test 올인원
  5. AI 테스트 작성 — GitHub Copilot Tests·CodiumAI·Qodo·Cursor의 테스트 생성이 실용 단계 진입
  6. MSW(Mock Service Worker) 표준화 — 모든 테스트 레이어에서 단일 mock 소스

이번 글에서는 13개 챕터로 프런트엔드 테스트 전략을 정리한다.


1장 · 테스트 피라미드 vs Testing Trophy

전통적 테스트 피라미드 (Mike Cohn, 2009)

Testing Trophy (Kent C. Dodds, 2018) — 프런트엔드 현실 반영

         E2E
       ████████
     Integration
   ████████████████
      Component
   ████████████████
        Unit
      ████████
      Static
   ████████████

왜 Trophy가 프런트엔드에 맞나

프런트엔드의 복잡성은 대부분 컴포넌트들의 상호작용사용자 플로우에 있다. 순수 함수 단위 테스트 1,000개가 있어도 "이 폼에서 제출이 안 돼요"를 못 잡는다.

가장 높은 ROI는 Integration Test (=Component Test). 하나의 테스트로 여러 유닛의 협업을 한 번에 검증.


2장 · Vitest — Jest의 후계자

2022년 Anthony Fu가 만든 Vite 친화 테스트 러너. 2025년 주간 다운로드 2,000만+ 돌파.

왜 Vitest인가

  1. Vite 설정 재사용 — 별도 Babel·SWC 설정 필요 없음
  2. ESM 네이티브 — Jest의 고질병(ESM 지원) 없음
  3. HMR-style 테스트 watch — 변경된 테스트만 순식간에 재실행
  4. Jest API 호환describe·it·expect·vi.fn() 그대로
  5. Browser Mode — 실제 브라우저에서 컴포넌트 테스트

설치·기본 사용

npm i -D vitest @vitest/ui
// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    environment: "jsdom",
    setupFiles: ["./test/setup.ts"],
    globals: true,
    coverage: { reporter: ["text", "html"] },
  },
});
// src/sum.test.ts
import { describe, it, expect } from "vitest";
import { sum } from "./sum";

describe("sum", () => {
  it("두 수를 더한다", () => {
    expect(sum(1, 2)).toBe(3);
  });
});

Vitest의 숨은 강점


3장 · Jest·Bun Test — 여전한 대안

Jest — "죽지 않은 거장"

Bun Test

Bun 런타임에 내장. Jest 호환 + 2~10배 빠름.

bun test

2025 선택 가이드


4장 · Testing Library — "사용자처럼 테스트하기"

Kent C. Dodds의 2018년 작. "구현이 아닌 행동을 테스트하라"의 실천 도구.

철학

기본 예시

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

test("로그인 폼 제출", async () => {
  const user = userEvent.setup();
  render(<Login onSubmit={vi.fn()} />);

  await user.type(screen.getByLabelText(/이메일/i), "a@b.com");
  await user.type(screen.getByLabelText(/비밀번호/i), "secret123");
  await user.click(screen.getByRole("button", { name: /로그인/i }));

  expect(screen.getByText(/환영합니다/i)).toBeInTheDocument();
});

userEvent vs fireEvent

role 쿼리의 힘

getByRole("button", { name: "로그인" })은 접근성·마크업 모두 테스트. <div onClick> 남용 방지.


5장 · MSW — 서버 모킹의 표준

Mock Service Worker. Service Worker 기술로 실제 fetch·XHR을 가로챔. 코드 변경 없이 개발·테스트·Storybook 모두 동일한 mock 사용.

왜 혁명적인가

핸들러 정의

// mocks/handlers.ts
import { http, HttpResponse } from "msw";

export const handlers = [
  http.get("/api/users/:id", ({ params }) => {
    return HttpResponse.json({
      id: params.id,
      name: "영주",
      email: "a@b.com",
    });
  }),

  http.post("/api/login", async ({ request }) => {
    const body = await request.json();
    if (body.email === "a@b.com" && body.password === "secret") {
      return HttpResponse.json({ token: "abc" });
    }
    return new HttpResponse(null, { status: 401 });
  }),
];

브라우저 개발 환경

// mocks/browser.ts
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";

export const worker = setupWorker(...handlers);

// main.tsx
if (process.env.NODE_ENV === "development") {
  const { worker } = await import("./mocks/browser");
  await worker.start();
}

Vitest·Jest 환경

// test/setup.ts
import { setupServer } from "msw/node";
import { handlers } from "../mocks/handlers";
import { beforeAll, afterEach, afterAll } from "vitest";

export const server = setupServer(...handlers);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Playwright 환경

MSW 2.x부터 Playwright 공식 통합. 브라우저 컨텍스트에서 Service Worker 자동 주입.

교훈: 세 환경(개발·테스트·E2E)에서 서로 다른 mock을 유지하는 건 테스트 신뢰도의 적. MSW로 통일하라.


6장 · Playwright — E2E의 1강

Microsoft가 2020년에 만든 E2E 도구. 2024~2025년 Cypress·Selenium을 앞서는 주류.

강점

설치·기본

npm init playwright@latest
import { test, expect } from "@playwright/test";

test("로그인 후 대시보드", async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel("이메일").fill("a@b.com");
  await page.getByLabel("비밀번호").fill("secret");
  await page.getByRole("button", { name: "로그인" }).click();
  await expect(page).toHaveURL("/dashboard");
  await expect(page.getByText(/환영합니다/)).toBeVisible();
});

주요 2024~2025 기능

Cypress는?

여전히 훌륭한 도구지만 Playwright의 빠른 발전에 점유율 하락. 이미 Cypress 기반이면 유지, 새 프로젝트는 Playwright 권장.


7장 · Storybook 9 — 컴포넌트의 집

Storybook은 단순한 "문서 툴"을 넘어 컴포넌트 개발·테스트·문서의 허브가 되었다. 2024~2025년의 Storybook 8·9는 기본 내장된 테스트 기능으로 한 단계 진화.

핵심 기능

  1. Stories — 컴포넌트의 각 상태(variant·props 조합)를 문서화
  2. Interaction Test@storybook/test로 Testing Library 문법 그대로
  3. Visual Regression — Chromatic·Loki 통합
  4. A11y Addon — axe-core 자동 실행
  5. Test Runnertest-storybook 커맨드로 모든 스토리를 CI에서 일괄 실행

스토리 + Play 함수

import type { Meta, StoryObj } from "@storybook/react";
import { within, userEvent, expect } from "@storybook/test";
import { LoginForm } from "./LoginForm";

const meta: Meta<typeof LoginForm> = { component: LoginForm };
export default meta;

export const Default: StoryObj<typeof LoginForm> = {};

export const FilledOut: StoryObj<typeof LoginForm> = {
  play: async ({ canvasElement }) => {
    const c = within(canvasElement);
    await userEvent.type(c.getByLabelText(/이메일/), "a@b.com");
    await userEvent.type(c.getByLabelText(/비밀번호/), "secret");
    await userEvent.click(c.getByRole("button", { name: /로그인/ }));
    await expect(c.getByText(/환영합니다/)).toBeVisible();
  },
};

play 함수는 문서에서 직접 실행되고, CI에서 자동 테스트. 개발자·디자이너·QA가 같은 스토리를 본다.

Storybook 9의 새 기능


8장 · Visual Regression — Chromatic·Percy·Lost Pixel

코드 변경이 UI를 의도치 않게 깼는지를 픽셀 단위로 검증.

Chromatic (Storybook 팀 운영)

Percy (BrowserStack)

Lost Pixel

Playwright 내장 Visual

await expect(page).toHaveScreenshot("dashboard.png", {
  maxDiffPixelRatio: 0.01,
});

기본 구현이 필요하면 Playwright 자체로도 충분. 팀 협업·PR 워크플로우가 중요하면 Chromatic.

Visual Test의 함정


9장 · A11y 테스트 자동화 — axe-core·Pa11y

axe-core

Deque Systems의 오픈소스 a11y 엔진. WCAG 위반을 프로그래매틱으로 검사.

Jest·Vitest 통합

import { axe, toHaveNoViolations } from "jest-axe";
expect.extend(toHaveNoViolations);

test("폼은 접근성 위반이 없다", async () => {
  const { container } = render(<Login />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

Playwright 통합 (@axe-core/playwright)

import AxeBuilder from "@axe-core/playwright";

test("대시보드 a11y", async ({ page }) => {
  await page.goto("/dashboard");
  const res = await new AxeBuilder({ page }).analyze();
  expect(res.violations).toEqual([]);
});

Pa11y CI

정적 사이트맵 기반. CI에서 전체 페이지 일괄 검사.

자동화의 한계

axe가 잡는 건 구조적 위반 30%. 나머지 70%(의미·맥락·실제 사용성)는 수동 테스트 + 스크린 리더로 보완해야 한다. 자동화는 회귀 방지에 가까운 역할.


10장 · 테스트 데이터 관리 — Faker·Factory·Fixture

Factory 패턴

// test/factories/user.ts
import { faker } from "@faker-js/faker/locale/ko";

export function userFactory(overrides: Partial<User> = {}): User {
  return {
    id: faker.string.uuid(),
    name: faker.person.fullName(),
    email: faker.internet.email(),
    createdAt: faker.date.past(),
    ...overrides,
  };
}

// 사용
const user = userFactory({ name: "영주" });

Fishery — 타입 안전 팩토리

import { Factory } from "fishery";

const userFactory = Factory.define<User>(() => ({
  id: faker.string.uuid(),
  name: faker.person.fullName(),
  email: faker.internet.email(),
}));

userFactory.build({ name: "철수" });
userFactory.buildList(10);

Fixture vs Factory

Snapshot Test


11장 · Flaky 테스트 — 원인과 해결

Flaky = 같은 조건에서 때로 통과, 때로 실패. CI의 신뢰를 갉아먹는 가장 큰 적.

주요 원인

  1. 비동기 타이밍await 누락, setTimeout 의존
  2. 순서 의존 — 한 테스트가 다른 테스트의 부산물에 의존
  3. 네트워크·시계 — 실제 API 호출, Date.now() 비결정
  4. 애니메이션 — 시각 회귀에서 픽셀 흔들림
  5. 폰트·이미지 로딩 — 초기 렌더에서 레이아웃 흔들림
  6. 병렬 실행 시 공유 DB·파일

해결

Flaky 감지 자동화


12장 · AI·Copilot이 테스트를 쓴다 — 2025 현황

도구

실전 사용 패턴

  1. "이 함수에 대한 Vitest 테스트 작성해줘" — 경계값·에러 케이스 자동 포함
  2. "이 컴포넌트의 Playwright E2E 시나리오 3개 만들어줘"
  3. "이 PR의 커버리지 부족 부분 찾아서 보강"

품질 관리

2025 현실

AI 테스트는 "없는 것보다 낫다" 단계를 넘어 "좋은 시작점" 단계. 다만 리팩터링 안전망 역할을 하려면 여전히 사람의 큐레이션이 필요.


13장 · 체크리스트·안티패턴·다음 글 예고

팀 테스트 전략 체크리스트 (14개)

  1. 테스트 피라미드/Trophy 합의 (Integration 중심)
  2. Vitest(또는 Jest·Bun) + Testing Library 기본 탑재
  3. MSW로 개발·테스트·Storybook mock 통합
  4. Playwright E2E: 주요 5-10개 플로우
  5. Storybook: Interaction·Visual·A11y 올인원
  6. axe-core 자동 a11y 회귀 방지
  7. Chromatic·Percy·Lost Pixel 중 하나로 Visual Regression
  8. Factory + Faker로 테스트 데이터 관리
  9. Flaky 감지 + 주간 리포트
  10. CI 병렬화 (Playwright shard, Vitest workspace)
  11. Coverage threshold (현실적 기준: 70~80%)
  12. Snapshot 남용 금지 — 의도가 명확한 곳에만
  13. AI 테스트 생성 활용 + 사람 리뷰
  14. Mutation Testing으로 테스트 자체의 품질 감사

테스트 안티패턴 TOP 10

  1. Implementation detail 테스트state.count·internal props 검증
  2. fireEvent 남용userEvent 써라
  3. getByTestId만 사용 — role·label 우선
  4. 실제 API 호출하는 테스트 — MSW 써라
  5. 테스트 순서 의존it.only로 하나만 돌리면 깨짐
  6. Snapshot 100개 자동 생성 — 아무도 안 봄
  7. E2E만 잔뜩, Unit/Component 없음 — 피드백 루프 느림
  8. Coverage 100% 목표 — 무의미한 테스트 양산
  9. Flaky 테스트를 retry로 숨김
  10. 디자인 변경 때마다 테스트 100개 깨짐 — 구현에 너무 밀착

다음 글 예고 — Season 6 Ep 12: "CI/CD·배포 전략의 진화"

테스트가 탄탄해지면 배포의 자신감도 따라온다. Ep 12는 2025년 프런트엔드 CI/CD·배포.

"배포는 축제가 아니라 일상이다. 하루 10번 배포해도 불안하지 않은 파이프라인을 만들자."

다음 글에서 만나자.


"테스트는 신뢰의 단위다. '이 변경이 안전하다'는 확신 없이 하는 리팩터링은 도박이고, 확신을 주는 테스트가 없는 팀은 혁신을 멈춘다. 코드의 미래를 사려면 테스트에 투자해야 한다."

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다