React 실무 가이드 · Part 12

테스트

리팩터링을 막지 않고 실제 버그를 잡는 테스트 작성하기

작성 기준2026년 7월버전과 지원 현황은 이후 달라질 수 있으니 공식 문서를 함께 확인하세요.

이 파트에서 다루는 내용

테스트 관점요소 찾기상호작용과 비동기무엇을 테스트할지
01

구현이 아니라 화면에 보이는 것을 테스트합니다

내부 상태값이나 함수 호출 여부를 검증하면, 결과가 같은데도 구현을 바꾸는 순간 테스트가 깨집니다. 그러면 테스트가 리팩터링을 막는 장애물이 됩니다.

React Testing Library는 사용자가 화면에서 보고 조작하는 것을 기준으로 테스트하도록 설계돼 있습니다. 사용자가 모르는 것은 테스트도 모르는 편이 낫습니다.

검증할 것
사용자 관점
  • 화면에 무엇이 보이는가
  • 버튼을 눌렀을 때 무엇이 달라지는가
  • 잘못 입력했을 때 어떤 안내가 나오는가
  • 로딩과 오류 상태가 제대로 표시되는가
검증하지 않을 것
구현 세부
  • 내부 state 값
  • 특정 훅의 호출 횟수
  • 컴포넌트가 몇 번 렌더링됐는지
  • CSS 클래스 이름
02

요소를 찾는 방법에는 우선순위가 있습니다

테스트에서 요소를 찾는 방법은 여러 가지인데, 아무거나 쓰면 안 됩니다. 사용자가 요소를 인식하는 방식에 가까운 것부터 씁니다.

이 순서를 지키면 접근성이 좋아지는 부수 효과도 있습니다. 화면 낭독기가 읽을 수 없는 요소는 테스트도 찾지 못하기 때문입니다.

1순위 · 역할과 이름
getByRole

버튼, 링크, 입력처럼 요소의 역할과 표시되는 이름으로 찾습니다. 사용자가 화면을 인식하는 방식과 가장 가깝습니다.

2순위 · 라벨과 텍스트
getByLabelText 등

폼 입력은 라벨로, 일반 텍스트는 내용으로 찾습니다.

마지막 · 테스트 아이디
getByTestId

위 방법으로 찾을 수 없을 때만 씁니다. 사용자에게 의미가 없는 값이라 접근성 문제를 가릴 수 있습니다.

쿼리 접두사 구분
동작 차이
  • getBy: 없으면 즉시 실패
  • queryBy: 없으면 null (없음을 검증할 때)
  • findBy: 나타날 때까지 기다림 (비동기)
폼 검증 테스트tsx
test("필수값이 비어 있으면 오류 메시지를 보여 준다", async () => {
  const user = userEvent.setup();
  render(<OrderForm onSubmit={vi.fn()} />);

  await user.click(screen.getByRole("button", { name: "주문하기" }));

  expect(await screen.findByText("받는 분을 입력해 주세요")).toBeInTheDocument();
});

test("입력 후 제출하면 값이 전달된다", async () => {
  const user = userEvent.setup();
  const handleSubmit = vi.fn();
  render(<OrderForm onSubmit={handleSubmit} />);

  await user.type(screen.getByLabelText("받는 분"), "홍길동");
  await user.click(screen.getByRole("button", { name: "주문하기" }));

  expect(handleSubmit).toHaveBeenCalledWith(
    expect.objectContaining({ receiver: "홍길동" })
  );
});

fireEvent보다 userEvent를 씁니다. 실제 사용자처럼 포커스와 키 입력 순서를 재현하므로, 그 과정에서만 드러나는 문제도 잡힙니다.

03

네트워크는 요청 단계에서 가로챕니다

데이터를 가져오는 화면을 테스트하려면 서버 응답이 필요합니다. 이때 우리 코드의 함수를 대체하는 방식보다, 네트워크 요청 자체를 가로채는 방식이 낫습니다.

함수를 대체하면 실제 호출 경로가 바뀌었을 때 테스트가 통과하는데도 화면은 깨질 수 있습니다.

요청 가로채기
권장

MSW 같은 도구로 네트워크 계층에서 응답을 정의합니다. 애플리케이션 코드는 그대로 두고 서버만 흉내 냅니다.

성공만 테스트하지 않기
자주 누락

오류 응답, 빈 목록, 느린 응답도 정의해 화면이 어떻게 보이는지 확인합니다. Part 9에서 설계한 상태들입니다.

비동기 대기
필수

데이터가 도착한 뒤를 검증하려면 findBy나 waitFor로 기다립니다. 바로 검증하면 아직 로딩 중이라 실패합니다.

04

무엇을 테스트할지가 어떻게 쓸지보다 중요합니다

우선 테스트할 것
가치 높음
  • 금액 계산, 할인, 수량 제한 같은 규칙
  • 폼 검증과 제출 흐름
  • 권한에 따라 화면이 달라지는 부분
  • 장애가 났던 지점
굳이 하지 않아도 되는 것
비용 대비 낮음
  • 단순 표시용 컴포넌트
  • 라이브러리가 보장하는 동작
  • 자주 바뀌는 화면 문구와 배치
테스트 종류 배분
구성

계산 로직은 함수 단위 테스트로, 화면 흐름은 컴포넌트 테스트로, 전체 시나리오는 소수의 종단 테스트로 나눕니다. 모든 것을 브라우저 테스트로 하면 느려서 아무도 돌리지 않습니다.

커버리지 함정
지표 주의

숫자를 목표로 삼으면 의미 없는 테스트가 늘어납니다. 바꾸기 무서운 코드가 어디인지가 더 좋은 기준입니다.

실무 기준

좋은 테스트는 리팩터링해도 깨지지 않고 버그가 생기면 깨지는 테스트입니다. 구현을 바꿀 때마다 테스트를 고치고 있다면, 테스트가 구현 세부에 붙어 있다는 뜻입니다.

버전

버전별 참고

본문은 React 19 기준입니다. 18과 달라지는 부분은 아래에 정리합니다.

  • React 18 이후 상태 변경이 자동으로 묶여 처리되면서, 테스트에서 별도 래핑 없이도 동작하는 경우가 늘었습니다.
  • Testing Library는 React 버전과 별개로 자체 버전을 따릅니다. React 19 사용 시 호환 버전을 확인합니다.
  • 테스트 실행 도구는 Vite 기반 프로젝트라면 Vitest가, CRA 계열이라면 Jest가 기본인 경우가 많습니다. 문법은 대부분 호환됩니다.
체크

이 파트 완료 기준