Communitygithub.com

React 테스팅

React Testing Library, Vitest/Jest, MSW를 활용한 네트워크 모킹, axe 접근성 검사, Playwright/Cypress 종단간 테스트와의 결정 경계를 포함한 React 컴포넌트 테스팅 가이드. React 컴포넌트, 훅, 페이지 테스트 작성 또는 수정 시 사용합니다.

React 테스팅란 무엇인가요?

React 테스팅 스킬은 행동 중심의 React 컴포넌트 테스트 작성을 위한 포괄적인 가이드입니다. 이 스킬은 React Testing Library, Vitest/Jest, MSW(네트워크 레벨 모킹), axe 접근성 검사 도구를 지원하며, Playwright/Cypress 컴포넌트 테스트 및 종단간 테스트와의 명확한 결정 경계를 제시합니다. 핵심 원칙은 사용자가 보고 상호작용하는 것을 테스트하고, 구현 세부 사항을 테스트하지 않는 것입니다. 이를 통해 안정적이고 유지보수 가능한 테스트 스위트를 구축할 수 있습니다. 이 스킬은 쿼리 우선순위, userEvent 사용법, MSW 설정 및 테스트별 재정의, 프로바이더 래핑, 커스텀 훅 테스팅, 접근성 검사, 스냅샷 테스트 지양 시점, JSDOM의 한계 극복을 위한 Playwright/Cypress 사용 시점을 상세히 설명합니다. 또한 커버리지 목표, 안티패턴, TDD 워크플로우, 예제 코드를 제공하여 실제 프로젝트에 바로 적용할 수 있도록 돕습니다.

지원 대상✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/react-testing

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

React 테스팅은(는) 무엇을 하나요?

행동 중심의 컴포넌트 테스트, 커스텀 훅 테스트, 접근성 검사, 네트워크 레벨 모킹을 위한 종합적인 React 테스팅 패턴입니다.

활성화 시점

  • React 컴포넌트, 커스텀 훅 또는 페이지 테스트 작성
  • 레거시 미테스트 컴포넌트에 테스트 커버리지 추가
  • Enzyme 또는 클래스 컴포넌트 패턴에서 React Testing Library로 마이그레이션
  • 새 React 프로젝트에 Vitest 또는 Jest 설정
  • 테스트에서 HTTP 요청 모킹
  • 접근성 위반 검사
  • 어떤 테스트가 RTL에 속하고 어떤 테스트가 Playwright 컴포넌트 테스트 또는 전체 E2E에 속하는지 결정

핵심 원칙

사용자가 보고 수행하는 것을 테스트하고, 구현 세부 사항은 테스트하지 마세요.

테스트는 다음을 수행해야 합니다:

  • 프로덕션에서 사용하는 것과 동일한 프로바이더로 컴포넌트 렌더링
  • 접근 가능한 쿼리(role, label)와 userEvent를 통해 상호작용
  • 보이는 출력과 관찰 가능한 부수 효과(콜백 호출, 요청 전송) 검증

테스트는 다음을 수행해서는 안 됩니다:

  • 컴포넌트 상태, 자식에게 전달된 props, 호출된 훅 검사
  • React 자체 또는 프레임워크 훅 모킹
  • 사용자에게 영향을 미치지 않는 렌더링 횟수 또는 DOM 구조 검증

라이브러리 선택

실행기사용 시기참고
VitestVite, Remix, 최신 설정더 빠름, 네이티브 ESM, Jest 호환 API
JestNext.js, CRA, 기존 저장소많은 React 프로젝트의 기본값
Playwright 컴포넌트 테스팅실제 브라우저 엔진 필요JSDOM에 필요한 기능이 없을 때 사용
Cypress 컴포넌트 테스팅실제 브라우저, Cypress 이미 사용 중Playwright CT의 대안

하나를 선택하세요. 명확한 레인 분리가 없으면 동일한 저장소에서 RTL + Vitest와 Playwright CT를 함께 실행하지 마십시오.

쿼리 우선순위

React Testing Library는 세 가지 계층의 쿼리를 제공합니다. 위에서 아래로 사용하세요:

  1. 모든 사람이 접근 가능: getByRole, getByLabelText, getByPlaceholderText, getByText, getByDisplayValue
  2. 의미론적: getByAltText, getByTitle
  3. 테스트 ID (탈출구): getByTestId
// 가장 좋은 방법
screen.getByRole("button", { name: /save/i });

// 입력에 적합
screen.getByLabelText("Email");

// 최후의 수단
screen.getByTestId("save-btn");

변형:

  • getBy* — 일치하는 항목이 없으면 에러 발생
  • queryBy* — null 반환 ("부재 확인"에 사용)
  • findBy* — 비동기, Promise 반환 (비동기 작업 후 나타나는 요소에 사용)

userEvent를 사용한 사용자 상호작용

import userEvent from "@testing-library/user-event";

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

  await user.type(screen.getByLabelText("Email"), "[email protected]");
  await user.click(screen.getByRole("button", { name: /save/i }));

  expect(onSubmit).toHaveBeenCalledWith({ email: "[email protected]" });
});
  • 항상 await userEvent 호출
  • 테스트 당 한 번 userEvent.setup()을 호출하고 반환된 user 재사용
  • userEvent는 실제 브라우저 시퀀스를 시뮬레이션하고, fireEvent는 단일 합성 이벤트를 발송합니다. userEvent를 선호하세요.

비동기 패턴

// 비동기 작업 후 나타나는 요소
expect(await screen.findByText("Loaded")).toBeInTheDocument();

// 부수 효과 검증
await waitFor(() => expect(saveSpy).toHaveBeenCalled());

// 사라져야 하는 요소
await waitForElementToBeRemoved(() => screen.queryByText("Loading"));

setTimeout + 검증은 절대 사용하지 마세요. 불안정합니다. 위의 매처를 사용하세요.

MSW를 사용한 네트워크 모킹

Mock Service Worker는 네트워크 레이어에서 모킹합니다. 컴포넌트, 훅, fetch 라이브러리는 모두 프로덕션에서와 정확히 동일하게 동작합니다.

설정

// test/setup.ts
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";

export const handlers = [
  http.get("/api/users/:id", ({ params }) =>
    HttpResponse.json({ id: params.id, name: "Alice" }),
  ),
  http.post("/api/users", async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json({ id: "new-id", ...body }, { status: 201 });
  }),
];

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

beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

onUnhandledRequest: "error"로 설정하여 모킹되지 않은 요청이 테스트를 명확히 실패하게 만듭니다. 조용한 통과는 빨간불보다 더 나쁩니다.

테스트별 재정의

test("500 에러 렌더링", async () => {
  server.use(
    http.get("/api/users/:id", () => new HttpResponse(null, { status: 500 })),
  );
  render(<UserPage id="1" />);
  expect(await screen.findByText(/문제가 발생했습니다/i)).toBeInTheDocument();
});

프로바이더 래핑

test-utils.tsx에 프로바이더를 한 번 래핑하세요:

// test-utils.tsx
import { render, RenderOptions } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

export function renderWithProviders(
  ui: React.ReactElement,
  options?: RenderOptions,
) {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });

  return render(
    <QueryClientProvider client={queryClient}>
      <ThemeProvider theme={lightTheme}>
        <MemoryRouter>{ui}</MemoryRouter>
      </ThemeProvider>
    </QueryClientProvider>,
    options,
  );
}

export * from "@testing-library/react";

그런 다음 모든 테스트 파일에서 import { renderWithProviders, screen } from "test-utils"를 사용하세요.

커스텀 훅 테스팅

import { renderHook, act } from "@testing-library/react";

test("useCounter 증가 및 감소", () => {
  const { result } = renderHook(() => useCounter(0));

  expect(result.current.count).toBe(0);

  act(() => result.current.increment());
  expect(result.current.count).toBe(1);

  act(() => result.current.decrement());
  expect(result.current.count).toBe(0);
});

test("useCounter 초기값 받기", () => {
  const { result } = renderHook(() => useCounter(10));
  expect(result.current.count).toBe(10);
});

test("useUser 사용자 데이터 가져오기", async () => {
  // 래퍼 외부에서 테스트당 한 번 QueryClient를 인스턴스화하여 리렌더링 시에도 유지되도록 합니다.
  // 래퍼 클로저 내부에서 생성하면 모든 렌더링 시 캐시 상태가 재설정되어 불안정한 테스트가 발생합니다.
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  const wrapper = ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );

  const { result } = renderHook(() => useUser("1"), { wrapper });

  await waitFor(() => expect(result.current.isSuccess).toBe(true));
  expect(result.current.data).toEqual({ id: "1", name: "Alice" });
});
  • 상태를 변경하는 호출을 act로 감쌉니다.
  • 훅의 공개 API만을 통해 테스트합니다.
  • 컨텍스트를 사용하는 훅의 경우 wrapper를 전달합니다.

접근성 검사

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

test("UserCard에 a11y 위반이 없음", async () => {
  const { container } = render(<UserCard user={mockUser} />);
  expect(await axe(container)).toHaveNoViolations();
});

모든 상호작용 컴포넌트에 대해 컴포넌트 테스트에서 axe를 실행하세요. 다음을 발견합니다:

  • 양식 입력에 레이블 누락
  • 잘못된 ARIA 사용
  • 낮은 색상 대비 (제한됨 — JSDOM에는 실제 CSS 엔진이 없으므로 인라인 스타일에만 작동합니다. 시각적 대비는 Playwright에 속합니다)
  • 이미지에 대체 텍스트 누락
  • 제목 순서 위반

교차 링크: skills/accessibility/SKILL.md (광범위한 a11y 테스팅 플레이북)

스냅샷 테스트를 사용하지 말아야 할 때

렌더링된 출력의 스냅샷:

  • 모든 스타일 변경 시 실패
  • 리뷰 중에 도장 찍기 쉬움
  • 구현 세부 사항(DOM 구조)을 테스트하고, 동작을 테스트하지 않음

허용되는 스냅샷 사용:

  • 순수 데이터 직렬화 함수 (formatInvoice(invoice) -> 안정적인 문자열)
  • 생성된 설정 파일 (예: webpack 설정 출력)

컴포넌트의 시각적 회귀 테스트에는 Playwright/Cypress 스크린샷 또는 Percy/Chromatic을 사용하세요. DOM 문자열이 아닌 실제 시각적 diff를 사용해야 합니다.

Playwright / Cypress를 사용해야 할 때

JSDOM(Vitest/Jest에서 사용)은 다음을 할 수 없습니다:

  • 실제 레이아웃 렌더링(flexbox, grid, viewport 쿼리)
  • 네이티브 브라우저 애니메이션, CSS 전환 실행
  • 스크롤 동작, 드래그 앤 드롭, 클립보드 붙여넣기 테스트
  • iframe, 팝업, 다운로드, 교차 출처 흐름 처리
  • DevTools의 전체 지원을 받는 통제된 환경에서 실제 네트워크 실행

이러한 경우 Playwright 컴포넌트 테스트(실제 브라우저 내 컴포넌트 테스트) 또는 전체 E2E를 사용하세요. e2e-testing skill을 참조하세요.

결정 경계:

  • 훅, 프레젠테이셔널 컴포넌트, 로직이 있는 폼 -> RTL
  • 레이아웃이 중요하거나 JSDOM에 없는 브라우저 API를 사용하는 컴포넌트 -> Playwright CT
  • 여러 페이지에 걸친 전체 사용자 흐름 -> Playwright/Cypress E2E

커버리지 목표

레이어목표
순수 유틸리티>=90%
커스텀 훅>=85%
프레젠테이셔널 컴포넌트>=80% — 라인이 아닌 동작
컨테이너 컴포넌트>=70% — 주요 경로 + 에러 상태
페이지E2E에서 별도로 커버리지 확보, 최소 스모크 테스트

vitest.config.ts / jest.config.js를 통해 설정:

// vitest.config.ts
test: {
  coverage: {
    provider: "v8",
    reporter: ["text", "html", "lcov"],
    thresholds: {
      lines: 80,
      functions: 80,
      branches: 70,
      statements: 80,
    },
  },
}

안티패턴

  • container.querySelector("...") — 접근성 쿼리를 우회하여 실제 사용자가 실패할 때 테스트가 통과하도록 함
  • 렌더링 횟수 검증 — 구현 세부 사항
  • jest.mock("react", ...) — React를 절대 모킹하지 마십시오. 컴포넌트를 리팩터링하세요.
  • 기본적으로 자식 컴포넌트 모킹 — 격리가 아닌 통합을 테스트합니다. 자식에게 무거운 부수 효과가 있을 때만 모킹하세요.
  • act() 경고 무시 — 실제 버그를 알리는 신호입니다(마운트 해제 후 상태 업데이트, 비동기 래핑 누락).
  • 테스트 간에 변경 가능한 상태 공유 — 테스트 순서가 변경되면 불안정해집니다.
  • it.skip()을 제거했을 때 통과하는 테스트 — 테스트가 실제로 생각하는 대로 검증하지 않습니다.

TDD 워크플로우

RED     -> 다음 요구 사항에 대한 실패하는 테스트 작성
GREEN   -> 통과할 수 있는 최소한의 컴포넌트 코드 작성
REFACTOR -> 컴포넌트 개선, 테스트는 계속 통과
REPEAT  -> 다음 요구 사항

새로운 컴포넌트의 경우:

  1. 컴포넌트의 prop 타입과 시그니처 정의
  2. 가장 간단한 경우에 대한 첫 번째 테스트 작성
  3. 올바른 이유로 실패하는지 확인
  4. 통과할 수 있을 만큼만 구현
  5. 다음 테스트 케이스 추가
  6. 세 번째 유사한 테스트가 패턴을 드러낼 때 리팩터링

테스트 명령어

# Vitest
vitest                            # 감시 모드
vitest run                        # 한 번 실행
vitest run --coverage             # 커버리지 포함
vitest run path/to/file.test.tsx  # 단일 파일

# Jest
jest --watch
jest --coverage
jest path/to/file.test.tsx

# CI 모드
CI=true vitest run --coverage

관련 항목

예제

MSW 및 userEvent를 사용한 폼 제출

test("사용자 폼 제출 및 성공 메시지 표시", async () => {
  server.use(
    http.post("/api/users", () =>
      HttpResponse.json({ id: "1", name: "Alice" }, { status: 201 }),
    ),
  );

  const user = userEvent.setup();
  renderWithProviders(<UserForm />);

  await user.type(screen.getByLabelText("Name"), "Alice");
  await user.type(screen.getByLabelText("Email"), "[email protected]");
  await user.click(screen.getByRole("button", { name: /save/i }));

  expect(await screen.findByText(/성공적으로 저장되었습니다/i)).toBeInTheDocument();
});

에러 경계 테스트

function Broken() {
  throw new Error("boom");
}

test("에러 경계가 폴백 렌더링", () => {
  // 예상되는 에러에 대해 React의 console.error 소음을 억제한 후 복원하여
  // 스파이가 테스트 간에 누출되어 다른 곳의 실제 에러를 숨기지 않도록 합니다.
  const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
  try {
    render(
      <ErrorBoundary fallback={<div>문제가 발생했습니다</div>}>
        <Broken />
      </ErrorBoundary>,
    );

    expect(screen.getByText("문제가 발생했습니다")).toBeInTheDocument();
  } finally {
    errorSpy.mockRestore();
  }
});

Suspense 경계 테스트

test("로딩 후 콘텐츠 표시", async () => {
  renderWithProviders(
    <Suspense fallback={<div>로딩 중...</div>}>
      <UserDetail id="1" />
    </Suspense>,
  );

  expect(screen.getByText("로딩 중...")).toBeInTheDocument();
  expect(await screen.findByText("Alice")).toBeInTheDocument();
});

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

관련 스킬