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

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

도입 – 학습 목표 및 필요성

React Testing Library로 테스트 작성하기는 현대 프론트엔드 개발에서 필수적인 스킬입니다. 이 튜토리얼에서는 컴포넌트 테스트를 작성하는 방법을 처음부터 차근차근 배워보겠습니다. React Testing Library는 사용자 중심의 테스트를 작성할 수 있도록 도와주며, 실제 사용자가 애플리케이션을 사용하는 방식과 유사하게 테스트를 구성합니다. 이를 통해 더 신뢰할 수 있는 코드를 작성하고, 리팩토링 시 자신감을 가질 수 있으며, 버그를 조기에 발견할 수 있습니다. 이 가이드를 마치면 여러분은 실무에서 바로 활용 가능한 테스트 작성 능력을 갖추게 될 것입니다.

기본 개념 설명

React Testing Library는 Enzyme과 달리 컴포넌트의 내부 구현이 아닌 사용자 관점에서 테스트를 작성하도록 설계되었습니다. 핵심 철학은 “테스트가 소프트웨어 사용 방식과 유사할수록 더 많은 신뢰를 제공한다”는 것입니다.

주요 개념:

  • Query: DOM 요소를 찾는 방법 (getBy, queryBy, findBy)
  • User Events: 사용자 상호작용 시뮬레이션
  • Accessibility: 접근성을 고려한 쿼리 우선순위
  • Async Utilities: 비동기 작업 테스트

쿼리 우선순위는 다음과 같습니다: getByRole > getByLabelText > getByPlaceholderText > getByText > getByDisplayValue > getByAltText > getByTitle > getByTestId. 이 순서는 접근성과 사용자 경험을 우선시하며, 실제 사용자가 요소를 찾는 방식을 반영합니다.

단계별 구현 가이드

1단계: 환경 설정

Create React App으로 생성한 프로젝트는 이미 React Testing Library가 설치되어 있습니다. 그렇지 않다면 다음 명령어로 설치합니다:

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

setupTests.js 파일에 다음을 추가합니다:

import '@testing-library/jest-dom';

2단계: 첫 번째 테스트 작성

간단한 버튼 컴포넌트부터 시작해봅시다. 테스트는 AAA 패턴(Arrange-Act-Assert)을 따릅니다:

  • Arrange(준비): 컴포넌트를 렌더링하고 필요한 설정을 합니다
  • Act(실행): 사용자 상호작용을 시뮬레이션합니다
  • Assert(검증): 예상 결과를 확인합니다

3단계: 쿼리 메서드 이해하기

React Testing Library는 세 가지 쿼리 변형을 제공합니다:

  • getBy*: 요소를 즉시 찾고, 없으면 에러 발생 (동기)
  • queryBy*: 요소를 찾고, 없으면 null 반환 (존재하지 않음 검증용)
  • findBy*: Promise를 반환하며 비동기 요소 검색 (최대 1초 대기)

각 쿼리는 AllBy 변형도 제공합니다 (getAllBy*, queryAllBy*, findAllBy*).

4단계: 사용자 이벤트 처리

@testing-library/user-event는 fireEvent보다 실제 사용자 동작을 더 정확하게 시뮬레이션합니다. click, type, hover, upload 등의 메서드를 제공하며, 각 이벤트는 브라우저에서 발생하는 것과 동일한 순서로 트리거됩니다.

5단계: 비동기 테스트

API 호출, 타이머, 애니메이션 등 비동기 동작을 테스트할 때는 waitFor, findBy 쿼리, waitForElementToBeRemoved 등을 활용합니다. 이들은 자동으로 재시도하며 타임아웃까지 대기합니다.

실제 코드 예제와 설명

예제 1: 기본 컴포넌트 테스트

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

function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <h1>카운터: {count}</h1>
      <button onClick={() => setCount(count + 1)}>
        증가
      </button>
      <button onClick={() => setCount(0)}>
        초기화
      </button>
    </div>
  );
}

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

describe('Counter 컴포넌트', () => {
  test('초기 카운트는 0이다', () => {
    render(<Counter />);
    const heading = screen.getByRole('heading', { name: /카운터: 0/i });
    expect(heading).toBeInTheDocument();
  });

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

  test('초기화 버튼 클릭 시 카운트가 0으로 리셋된다', async () => {
    const user = userEvent.setup();
    render(<Counter />);
    
    const incrementButton = screen.getByRole('button', { name: /증가/i });
    const resetButton = screen.getByRole('button', { name: /초기화/i });
    
    await user.click(incrementButton);
    await user.click(incrementButton);
    await user.click(resetButton);
    
    expect(screen.getByRole('heading', { name: /카운터: 0/i })).toBeInTheDocument();
  });
});

예제 2: 폼 입력 테스트

// LoginForm.test.jsx
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(/이메일/i);
  const passwordInput = screen.getByLabelText(/비밀번호/i);
  const submitButton = screen.getByRole('button', { name: /로그인/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: 비동기 데이터 페칭 테스트

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

test('사용자 목록을 불러와서 표시한다', async () => {
  // API 모킹
  global.fetch = jest.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve([
        { id: 1, name: '김철수' },
        { id: 2, name: '이영희' }
      ])
    })
  );
  
  render(<UserList />);
  
  // 로딩 상태 확인
  expect(screen.getByText(/로딩 중/i)).toBeInTheDocument();
  
  // 비동기 데이터 로드 대기
  const user1 = await screen.findByText('김철수');
  const user2 = await screen.findByText('이영희');
  
  expect(user1).toBeInTheDocument();
  expect(user2).toBeInTheDocument();
  expect(screen.queryByText(/로딩 중/i)).not.toBeInTheDocument();
  
  global.fetch.mockClear();
});

고급 활용 방법

커스텀 렌더 함수

Provider(Redux, Router, Theme 등)가 필요한 경우 커스텀 렌더 함수를 만들면 편리합니다:

// test-utils.jsx
import { render } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { ThemeProvider } from 'styled-components';

const AllTheProviders = ({ children }) => {
  return (
    <BrowserRouter>
      <ThemeProvider theme={mockTheme}>
        {children}
      </ThemeProvider>
    </BrowserRouter>
  );
};

const customRender = (ui, options) =>
  render(ui, { wrapper: AllTheProviders, ...options });

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

MSW를 활용한 API 모킹

Mock Service Worker(MSW)를 사용하면 실제 네트워크 요청을 인터셉트하여 더 현실적인 테스트가 가능합니다:

import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/users', (req, res, ctx) => {
    return res(ctx.json([{ id: 1, name: '테스트 유저' }]));
  })
);

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

접근성 테스트

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();
});

마무리 및 추가 학습 자료

이 튜토리얼을 통해 React Testing Library로 테스트 작성하기의 기본부터 고급 기법까지 배웠습니다. 테스트는 처음에는 시간이 걸리지만, 장기적으로 개발 속도를 높이고 코드 품질을 향상시킵니다. 실전에서는 모든 것을 테스트하기보다는 중요한 사용자 플로우와 비즈니스 로직에 집중하세요.

추가 학습 자료:

지금 바로 여러분의 프로젝트에 테스트를 작성해보세요. 작은 컴포넌트부터 시작하여 점차 복잡한 기능으로 확장해나가면 자연스럽게 테스트 작성 능력이 향상될 것입니다!

📚 함께 읽으면 좋은 글

1

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

📂 React 튜토리얼
📅 2025. 10. 2.
🎯 React Testing Library로 테스트 작성하기

2

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

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

3

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

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

4

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

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

5

Python 머신러닝 라이브러리 활용법 – 초보자도 쉽게 따라하는 완벽 가이드

📂 Python 튜토리얼
📅 2025. 10. 3.
🎯 Python 머신러닝 라이브러리 활용법

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

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

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

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

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

여러분은 React Testing Library로 테스트 작성하기에 대해 어떻게 생각하시나요?

💡
유용한 정보 공유

궁금한 점 질문

🤝
경험담 나누기

👍
의견 표현하기

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

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

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

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

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

💡
최신 트렌드
2025년 기준

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

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

답글 남기기