본문 바로가기
Frontend2026년 3월 20일9분 읽기

React 컴포넌트 테스트 전략 — Testing Library 실전

YS
김영삼
조회 434

React Testing Library 철학

React Testing Library는 "사용자가 컴포넌트를 사용하는 방식"으로 테스트합니다. 구현 세부사항이 아닌 동작을 테스트하여 리팩토링에 강한 테스트를 작성합니다.

기본 설정

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: './src/test/setup.ts'
  }
});

// src/test/setup.ts
import '@testing-library/jest-dom';

기본 렌더링과 쿼리

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

describe('LoginForm', () => {
  it('renders login form with email and password fields', () => {
    render(<LoginForm onSubmit={vi.fn()} />);

    expect(screen.getByLabelText('이메일')).toBeInTheDocument();
    expect(screen.getByLabelText('비밀번호')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '로그인' })).toBeInTheDocument();
  });

  it('shows validation error for empty email', async () => {
    const user = userEvent.setup();
    render(<LoginForm onSubmit={vi.fn()} />);

    await user.click(screen.getByRole('button', { name: '로그인' }));
    expect(screen.getByText('이메일을 입력해주세요')).toBeInTheDocument();
  });

  it('calls onSubmit with form data', async () => {
    const user = userEvent.setup();
    const handleSubmit = vi.fn();
    render(<LoginForm onSubmit={handleSubmit} />);

    await user.type(screen.getByLabelText('이메일'), 'test@example.com');
    await user.type(screen.getByLabelText('비밀번호'), 'password123');
    await user.click(screen.getByRole('button', { name: '로그인' }));

    expect(handleSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'password123'
    });
  });
});

비동기 테스트

import { render, screen, waitFor } from '@testing-library/react';

describe('UserProfile', () => {
  it('loads and displays user data', async () => {
    vi.spyOn(global, 'fetch').mockResolvedValueOnce({
      ok: true,
      json: async () => ({ name: '홍길동', email: 'hong@example.com' })
    });

    render(<UserProfile userId={1} />);
    expect(screen.getByText('로딩 중...')).toBeInTheDocument();

    await waitFor(() => {
      expect(screen.getByText('홍길동')).toBeInTheDocument();
    });

    expect(screen.queryByText('로딩 중...')).not.toBeInTheDocument();
  });

  it('handles API error gracefully', async () => {
    vi.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error'));
    render(<UserProfile userId={1} />);

    const errorMessage = await screen.findByText('데이터를 불러올 수 없습니다');
    expect(errorMessage).toBeInTheDocument();
  });
});

커스텀 훅 테스트

import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter', () => {
  it('increments counter', () => {
    const { result } = renderHook(() => useCounter(0));
    expect(result.current.count).toBe(0);

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

  it('resets to initial value', () => {
    const { result } = renderHook(() => useCounter(10));

    act(() => {
      result.current.increment();
      result.current.increment();
      result.current.reset();
    });
    expect(result.current.count).toBe(10);
  });
});

쿼리 우선순위

우선순위쿼리사용 시점
1getByRole접근성 역할로 검색 (권장)
2getByLabelText폼 요소
3getByPlaceholderTextplaceholder가 있는 입력
4getByText비대화형 요소
5getByDisplayValue입력의 현재 값
6getByTestId최후의 수단

MSW로 API 모킹

import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';

const server = setupServer(
  http.get('/api/users/:id', ({ params }) => {
    return HttpResponse.json({
      id: params.id,
      name: '홍길동'
    });
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
  • 구현 세부사항(state, props, 내부 메서드)이 아닌 사용자 행동을 테스트
  • getByRole을 우선 사용하면 접근성도 함께 검증됨
  • userEvent는 실제 사용자 상호작용을 더 정확히 시뮬레이션
  • MSW(Mock Service Worker)로 네트워크 레벨에서 API를 모킹하면 테스트 신뢰도 향상
  • 테스트 커버리지보다 중요한 것은 핵심 사용자 흐름의 테스트

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록