React Testing Library로 테스트 작성하기 – 초보자도 쉽게 따라하는 완벽 가이드

React Testing Library로 테스트 작성하기 – 초보자도 쉽게 따라하는 완벽 가이드

1. 도입 – 학습 목표 및 필요성

React Testing Library로 테스트 작성하기는 현대 웹 개발에서 필수적인 스킬입니다. 많은 개발자들이 테스트 작성을 어려워하지만, React Testing Library는 사용자 관점에서 컴포넌트를 테스트할 수 있도록 설계되어 있어 실제로는 매우 직관적입니다. 이 가이드를 통해 여러분은 기본적인 컴포넌트 테스트부터 사용자 상호작용, 비동기 작업까지 완벽하게 다룰 수 있게 됩니다. 테스트 코드를 작성하면 버그를 조기에 발견하고, 리팩토링 시 자신감을 가질 수 있으며, 코드의 품질을 크게 향상시킬 수 있습니다. 지금부터 단계별로 React Testing Library의 핵심 개념과 실전 활용법을 배워보겠습니다.

2. 기본 개념 설명

React Testing Library는 Kent C. Dodds가 만든 테스팅 라이브러리로, DOM Testing Library를 기반으로 합니다. 가장 중요한 철학은 “테스트는 소프트웨어가 사용되는 방식과 유사하게 작성되어야 한다”는 것입니다. 즉, 구현 세부사항이 아닌 사용자가 경험하는 방식으로 테스트를 작성합니다.

주요 개념은 다음과 같습니다:

  • Render: 컴포넌트를 가상 DOM에 렌더링합니다
  • Query: 렌더링된 요소를 찾는 방법으로, getBy, queryBy, findBy 등이 있습니다
  • User Event: 사용자 상호작용(클릭, 타이핑 등)을 시뮬레이션합니다
  • Assertion: 예상되는 결과를 검증합니다

React Testing Library는 Enzyme과 달리 컴포넌트의 내부 state나 props를 직접 접근하지 않습니다. 대신 사용자가 보고 상호작용하는 것과 동일한 방식으로 테스트를 작성하여, 더 견고하고 유지보수가 쉬운 테스트를 만들 수 있습니다.

3. 단계별 구현 가이드

3-1. 환경 설정

먼저 프로젝트에 필요한 패키지를 설치합니다. Create React App으로 생성한 프로젝트는 이미 설정되어 있지만, 수동으로 설정하는 경우 다음 명령어를 실행하세요:

npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event

package.json에 테스트 스크립트가 설정되어 있는지 확인합니다:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch"
  }
}

3-2. 첫 번째 테스트 작성

테스트 파일은 컴포넌트 파일과 같은 디렉토리에 생성하며, .test.js 또는 .spec.js 확장자를 사용합니다. 간단한 버튼 컴포넌트부터 시작해봅시다.

테스트는 AAA 패턴(Arrange-Act-Assert)을 따릅니다:

  • Arrange: 테스트 환경을 설정합니다 (컴포넌트 렌더링)
  • Act: 사용자 행동을 시뮬레이션합니다 (클릭, 입력 등)
  • Assert: 결과를 검증합니다

3-3. Query 메서드 이해하기

React Testing Library는 다양한 쿼리 메서드를 제공합니다:

  • getBy: 요소를 찾고, 없으면 에러 발생 (동기적)
  • queryBy: 요소를 찾고, 없으면 null 반환 (요소가 없음을 확인할 때 사용)
  • findBy: 비동기로 요소를 찾음 (Promise 반환)

각 쿼리는 다양한 방식으로 요소를 찾을 수 있습니다:

  • getByRole: 접근성 역할로 찾기 (권장)
  • getByLabelText: 라벨 텍스트로 찾기
  • getByPlaceholderText: placeholder로 찾기
  • getByText: 텍스트 내용으로 찾기
  • getByTestId: data-testid 속성으로 찾기 (최후의 수단)

3-4. 사용자 이벤트 시뮬레이션

@testing-library/user-event는 실제 사용자 상호작용을 더 정확하게 시뮬레이션합니다. fireEvent보다 user-event 사용을 권장합니다:

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

// 클릭
await userEvent.click(button);

// 타이핑
await userEvent.type(input, 'Hello World');

// 선택
await userEvent.selectOptions(select, 'option1');

3-5. 비동기 테스트

API 호출이나 비동기 작업을 테스트할 때는 waitFor, findBy 쿼리를 사용합니다:

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

// 요소가 나타날 때까지 대기
const element = await screen.findByText('Loaded Data');

// 조건이 만족될 때까지 대기
await waitFor(() => {
  expect(screen.getByText('Success')).toBeInTheDocument();
});

4. 실제 코드 예제와 설명

예제 1: 간단한 카운터 컴포넌트 테스트

// Counter.js
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      <button onClick={() => setCount(count - 1)}>
        Decrement
      </button>
    </div>
  );
}

export default Counter;
// Counter.test.js
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';

test('카운터가 0에서 시작한다', () => {
  render(<Counter />);
  const countElement = screen.getByRole('heading');
  expect(countElement).toHaveTextContent('Count: 0');
});

test('증가 버튼을 클릭하면 카운트가 증가한다', async () => {
  const user = userEvent.setup();
  render(<Counter />);
  
  const incrementButton = screen.getByRole('button', { name: /increment/i });
  await user.click(incrementButton);
  
  expect(screen.getByRole('heading')).toHaveTextContent('Count: 1');
});

test('감소 버튼을 클릭하면 카운트가 감소한다', async () => {
  const user = userEvent.setup();
  render(<Counter />);
  
  const decrementButton = screen.getByRole('button', { name: /decrement/i });
  await user.click(decrementButton);
  
  expect(screen.getByRole('heading')).toHaveTextContent('Count: -1');
});

예제 2: 폼 입력 테스트

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

test('사용자가 이메일과 비밀번호를 입력하고 제출할 수 있다', async () => {
  const user = userEvent.setup();
  const mockOnSubmit = jest.fn();
  
  render(<LoginForm onSubmit={mockOnSubmit} />);
  
  const emailInput = screen.getByLabelText(/email/i);
  const passwordInput = screen.getByLabelText(/password/i);
  const submitButton = screen.getByRole('button', { name: /login/i });
  
  await user.type(emailInput, '[email protected]');
  await user.type(passwordInput, 'password123');
  await user.click(submitButton);
  
  expect(mockOnSubmit).toHaveBeenCalledWith({
    email: '[email protected]',
    password: 'password123'
  });
});

예제 3: API 호출 테스트

// UserList.test.js
import { render, screen } from '@testing-library/react';
import UserList from './UserList';

test('사용자 목록을 비동기로 로드하고 표시한다', async () => {
  // API 모킹
  global.fetch = jest.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve([
        { id: 1, name: 'John Doe' },
        { id: 2, name: 'Jane Smith' }
      ])
    })
  );
  
  render(<UserList />);
  
  // 로딩 상태 확인
  expect(screen.getByText(/loading/i)).toBeInTheDocument();
  
  // 데이터가 로드될 때까지 대기
  const user1 = await screen.findByText('John Doe');
  const user2 = await screen.findByText('Jane Smith');
  
  expect(user1).toBeInTheDocument();
  expect(user2).toBeInTheDocument();
  expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});

5. 고급 활용 방법

커스텀 렌더 함수 만들기

Redux나 Context Provider를 사용하는 경우, 매번 Provider로 감싸기 번거롭습니다. 커스텀 렌더 함수를 만들어 해결할 수 있습니다:

// test-utils.js
import { render } from '@testing-library/react';
import { ThemeProvider } from './ThemeContext';
import { Provider } from 'react-redux';
import { store } from './store';

function customRender(ui, options) {
  return render(
    <Provider store={store}>
      <ThemeProvider>
        {ui}
      </ThemeProvider>
    </Provider>,
    options
  );
}

export * from '@testing-library/react';
export { customRender as render };

테스트 커버리지 확인

Jest의 커버리지 기능을 사용하여 테스트되지 않은 코드를 확인할 수 있습니다:

npm test -- --coverage

스냅샷 테스트

컴포넌트의 렌더링 결과가 예상과 일치하는지 확인할 수 있습니다:

test('컴포넌트가 올바르게 렌더링된다', () => {
  const { container } = render(<MyComponent />);
  expect(container).toMatchSnapshot();
});

접근성 테스트

jest-axe를 사용하여 접근성 문제를 자동으로 감지할 수 있습니다:

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

test('접근성 위반이 없어야 한다', async () => {
  const { container } = render(<MyComponent />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

6. 마무리 및 추가 학습 자료

React Testing Library로 테스트 작성하기를 마스터하면 더 안정적이고 유지보수하기 쉬운 React 애플리케이션을 만들 수 있습니다. 핵심은 사용자 관점에서 테스트를 작성하고, 구현 세부사항이 아닌 동작을 검증하는 것입니다.

추가 학습을 위한 자료:

이제 여러분도 자신감을 가지고 React 컴포넌트 테스트를 작성할 수 있습니다. 작은 컴포넌트부터 시작하여 점차 복잡한 시나리오로 확장해 나가세요. 테스트는 처음에는 시간이 걸리지만, 장기적으로 개발 속도와 코드 품질을 크게 향상시킵니다. 지금 바로 여러분의 프로젝트에 테스트를 추가해보세요!

📚 함께 읽으면 좋은 글

1

React Hooks 실전 활용 가이드 – 초보자도 쉽게 따라하는 완벽 가이드

📂 React 튜토리얼
📅 2025. 10. 2.
🎯 React Hooks 실전 활용 가이드

2

React Context API 마스터하기 – 초보자도 쉽게 따라하는 완벽 가이드

📂 React 튜토리얼
📅 2025. 10. 1.
🎯 React Context API 마스터하기

3

React 성능 최적화 완벽 가이드 – 초보자도 쉽게 따라하는 완벽 가이드

📂 React 튜토리얼
📅 2025. 9. 30.
🎯 React 성능 최적화 완벽 가이드

4

FastAPI로 REST API 만들기 – 초보자도 쉽게 따라하는 완벽 가이드

📂 Python 튜토리얼
📅 2025. 10. 1.
🎯 FastAPI로 REST API 만들기

5

Python 자동화 스크립트 작성하기 – 초보자도 쉽게 따라하는 완벽 가이드

📂 Python 튜토리얼
📅 2025. 9. 30.
🎯 Python 자동화 스크립트 작성하기

💡 위 글들을 통해 더 깊이 있는 정보를 얻어보세요!

📢 이 글이 도움되셨나요? 공유해주세요!

여러분의 공유 한 번이 더 많은 사람들에게 도움이 됩니다 ✨

🔥 공유할 때마다 블로그 성장에 큰 힘이 됩니다! 감사합니다 🙏

💬 여러분의 소중한 의견을 들려주세요!

이 글을 읽고 새롭게 알게 된 정보가 있다면 공유해주세요!

💡
유용한 정보 공유

궁금한 점 질문

🤝
경험담 나누기

👍
의견 표현하기

⭐ 모든 댓글은 24시간 내에 답변드리며, 여러분의 의견이 다른 독자들에게 큰 도움이 됩니다!
🎯 건설적인 의견과 경험 공유를 환영합니다 ✨

🔔 블로그 구독하고 최신 글을 받아보세요!

📚
다양한 주제
17개 카테고리

정기 업데이트
하루 3회 발행

🎯
실용적 정보
바로 적용 가능

💡
최신 트렌드
2025년 기준

🌟 React 튜토리얼부터 다양한 실생활 정보까지!
매일 새로운 유용한 콘텐츠를 만나보세요 ✨

📧 RSS 구독 | 🔖 북마크 추가 | 📱 모바일 앱 알림 설정
지금 구독하고 놓치는 정보 없이 업데이트 받아보세요!

답글 남기기