Skip to content

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

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

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

신입 개발자에게 물어보면 절반은 "당연하죠", 나머지 절반은 "시간 없어서..."라고 답한다. 그런데 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)

  • 하단: Unit (많이) — 단위 함수·로직
  • 중간: Integration (적당히) — 모듈 간 통합
  • 상단: E2E (적게) — 사용자 관점 전체 흐름

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

         E2E
       ████████
     Integration
   ████████████████
      Component
   ████████████████
        Unit
      ████████
      Static
   ████████████
  • Static (가장 많이): TypeScript·ESLint·컴파일러가 잡는 에러 (무료)
  • Unit: 순수 함수, utility
  • Component/Integration (중심): 컴포넌트 행동 기반 테스트
  • E2E (적게, 하지만 결정적): 주요 비즈니스 플로우

왜 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의 숨은 강점

  • Inline snapshot — 결과를 소스 코드 옆에 바로
  • Workspace — 모노레포에서 여러 패키지 병렬 테스트
  • Browser Mode — Playwright 기반 실제 브라우저
  • UI Dashboardvitest --ui로 시각적 리포트

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

Jest — "죽지 않은 거장"

  • Meta(Facebook) 메인테이너
  • 생태계 가장 거대 (플러그인·가이드)
  • ESM·TS 지원 여전히 번거로움
  • 2025년 새 프로젝트에 선택 이유는 "기존 생태계 강제" 뿐

Bun Test

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

bun test
  • 설치·설정 불필요
  • TypeScript·JSX·Decorator 기본 지원
  • Watcher·Coverage 내장
  • 다만 Jsdom·Testing Library 특정 기능에서 호환 이슈 일부 잔존

2025 선택 가이드

  • 새 프로젝트: Vitest 먼저, 속도가 절실하면 Bun Test
  • 기존 Jest 프로젝트: 전환 ROI가 높으면 Vitest로 (Jest API 호환)
  • Bun 런타임 전환 중: Bun Test 고려

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

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

철학

  • 쿼리 우선순위: getByRole > getByLabelText > getByPlaceholderText > getByText > getByDisplayValue > getByTestId
  • 접근 가능한 방식으로 먼저 탐색: 스크린 리더와 같은 경로
  • internal state·props 검증 금지: "쓰는 사람이 볼 수 없는 건 테스트 안 함"

기본 예시

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

  • fireEvent.click: 즉시 이벤트 발생 (fast, 비현실적)
  • userEvent.click: 실제 사용자 행동 시뮬레이션 (focus·hover·click) — 거의 항상 권장

role 쿼리의 힘

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


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

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

왜 혁명적인가

  • 하나의 mock 정의모든 환경(브라우저·Jest·Vitest·Playwright·Storybook) 동작
  • fetch·axios 코드를 수정할 필요 없음
  • 네트워크 레벨 가로채기 → 실제 요청과 가장 가까운 테스트

핸들러 정의

// 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을 앞서는 주류.

강점

  • 모든 주요 브라우저 (Chromium·Firefox·WebKit)
  • 모바일 에뮬레이션 (iPhone·Galaxy 프리셋)
  • Trace Viewer — 실패한 테스트의 전체 타임라인 확인
  • Codegen — 행동을 기록해 테스트 코드 자동 생성
  • Auto-wait — 명시적 wait 불필요
  • Parallel & Shard — CI 병렬 실행

설치·기본

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 기능

  • UI Mode (npx playwright test --ui) — 시간여행·watch·trace 통합 GUI
  • Component Test — React·Vue·Svelte 컴포넌트 단위 브라우저 테스트
  • Visual ComparisontoHaveScreenshot 내장
  • API Mockingpage.route로 직접 가로채기 또는 MSW 통합

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의 새 기능

  • Interaction Test 기본 내장 (@storybook/test)
  • Vitest 통합 (Storybook과 Vitest가 같은 엔진 공유)
  • Visual Test + A11y Test + Interaction 올인원

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

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

Chromatic (Storybook 팀 운영)

  • Storybook 기반. 각 스토리의 스크린샷을 저장·비교
  • PR에 시각적 diff 자동 코멘트
  • 유료지만 스타트업 무료 tier 존재

Percy (BrowserStack)

  • 프레임워크 무관
  • Playwright·Cypress·Storybook 통합
  • 기업용

Lost Pixel

  • 오픈소스 대안
  • Self-host 가능
  • Storybook·Playwright 통합

Playwright 내장 Visual

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

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

Visual Test의 함정

  • Flaky 스크린샷: 폰트·애니메이션·비동기 이미지로 픽셀 변동
  • 해결책: animations: "disabled", 폰트 고정, 이미지 placeholder, mock 데이터 시간 고정

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

  • Fixture: 고정 JSON 파일 (user-1.json) — 읽기 쉽지만 변형 불편
  • Factory: 함수로 생성, overrides 가능 — 유연성·재사용성 우위

Snapshot Test

  • Vitest·Jest의 toMatchSnapshot/toMatchInlineSnapshot
  • 편하지만 남용 금지: "왜 이 snapshot이 이 모양인지" 설명할 수 없으면 테스트로서 가치가 낮음
  • 의도가 명확한 소수의 곳에만

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

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

주요 원인

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

해결

  • MSW로 네트워크 모킹
  • 시간 고정: vi.useFakeTimers() + vi.setSystemTime(new Date("2025-01-01"))
  • 테스트 격리: beforeEach 초기화
  • Auto-wait 활용: Playwright getByRole + expect(...).toBeVisible()
  • 재시도(retry)는 신중히: retries: 2로 숨기면 근본 원인 못 찾음

Flaky 감지 자동화

  • Buildkite·CircleCI·GitHub Actions: Test Analytics
  • Datadog Test Visibility: Flakiness score 추적

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

도구

  • GitHub Copilot — 코드 옆에 "test generation" 자동
  • CodiumAI / Qodo — 함수·컴포넌트 단위 테스트 스위트 생성
  • Cursor / Windsurf — 에이전틱 모드로 전체 모듈 테스트 작성
  • Claude Code / Sonnet — 테스트 코드 생성 + 리팩터

실전 사용 패턴

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

품질 관리

  • AI 생성 테스트를 그대로 머지 금지 — 사람이 "이 테스트가 실제로 무엇을 보장하나" 확인
  • 과적합 테스트 경계: AI는 현재 구현과 너무 밀착한 테스트를 쓸 수 있음
  • Mutation Testing으로 테스트 실효성 검증 (Stryker)

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·배포.

  • GitHub Actions·CircleCI·Buildkite 2025 비교
  • Turborepo·Nx·Bazel로 모노레포 빌드 가속
  • Vercel·Netlify·Cloudflare Pages·AWS Amplify
  • Preview Deployment 문화
  • Progressive Delivery: Canary·Blue-Green·Feature Flag
  • Rollback 전략 (자동 vs 수동)
  • CDN Invalidation·ISR 재검증
  • Edge·Regional·Serverless 배포 차이
  • Artifact 관리·Build Cache (Remote Cache)
  • Supply Chain Security (Sigstore·SLSA·SBOM)
  • AI 에이전트가 릴리스 PR을 만드는 2025

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

다음 글에서 만나자.


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

현재 단락 (1/279)

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

작성 글자: 0원문 글자: 10,667작성 단락: 0/279