React 컴포넌트 테스트의 두 도구가 사실상 표준 — Vitest (테스트 러너, Vite 와 짝) + React Testing Library (DOM 조작·검증). 둘 다 가벼우면서 모던 React 패턴을 잘 다룬다.
이번 21편은 첫 테스트 5분 셋업 + Testing Library 의 핵심 철학 (queryByRole 우선) + 이벤트 시뮬레이션 + async 검증 + TanStack Query 같은 fetch 모킹.
1. 셋업 — Vite 프로젝트면 3분
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
// vite.config.ts 에 test 옵션 추가
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/test-setup.ts',
},
});
// src/test-setup.ts
import '@testing-library/jest-dom/vitest';
// package.json scripts
"test": "vitest",
"test:ui": "vitest --ui"
npm test 면 watch 모드, npm run test:ui 면 브라우저 UI 가 뜸 (테스트 실패한 부분 시각화).
2. 첫 테스트 — render + queryByRole
// Button.test.tsx
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
test('버튼이 라벨을 표시한다', () => {
render(<Button label="저장" onClick={() => {}} />);
const button = screen.getByRole('button', { name: '저장' });
expect(button).toBeInTheDocument();
});
Testing Library 철학 — "사용자가 보는 방식으로 요소를 찾는다". 클래스명·테스트 ID 가 아니라 role (button·link·heading) 과 접근 가능한 이름 (label·text) 으로.
왜 role 우선인가 — ① 스크린리더 사용자가 인지하는 방식과 일치 → 접근성 자동 검증. ② class 이름 바꿔도 테스트 안 깨짐. ③ 디자인 리팩토링에 강함. testid 는 "다른 방법이 없을 때" 최후 수단.
3. 이벤트 시뮬레이션 — userEvent
import { userEvent } from '@testing-library/user-event';
test('버튼 클릭 시 onClick 호출', async () => {
const handleClick = vi.fn(); // Vitest mock
render(<Button label="저장" onClick={handleClick} />);
const user = userEvent.setup();
await user.click(screen.getByRole('button', { name: '저장' }));
expect(handleClick).toHaveBeenCalledTimes(1);
});
test('input 에 텍스트 입력', async () => {
render(<LoginForm />);
const user = userEvent.setup();
await user.type(screen.getByLabelText('이메일'), '
[email protected]');
await user.type(screen.getByLabelText('비밀번호'), 'pwd1234');
await user.click(screen.getByRole('button', { name: '로그인' }));
// 검증
expect(screen.getByText('로그인 중...')).toBeInTheDocument();
});
userEvent 는 실제 사용자처럼 — 클릭 전 hover, 키 입력 시 keydown/keyup 모두 발생. fireEvent.click 같은 저수준 API 보다 항상 우위.
4. async 검증 — findBy · waitFor
fetch 같은 비동기 결과는 즉시 안 나타남. findBy (있을 때까지 기다림) 또는 waitFor.
test('로그인 성공 시 환영 메시지', async () => {
render(<App />);
const user = userEvent.setup();
await user.type(screen.getByLabelText('이메일'), '
[email protected]');
await user.type(screen.getByLabelText('비밀번호'), 'pwd');
await user.click(screen.getByRole('button', { name: '로그인' }));
// findBy = 최대 1초 기다리며 폴링
const welcome = await screen.findByText(/환영합니다, admin/);
expect(welcome).toBeInTheDocument();
});
3 family — getBy (즉시, 없으면 throw), queryBy (즉시, 없으면 null), findBy (대기, 있을 때까지). async 결과는 무조건 findBy.
5. fetch 모킹 — MSW (Mock Service Worker)
실제 API 안 부르고 가짜 응답으로 테스트. MSW 가 React 테스트의 사실상 표준.
npm install -D msw
// src/test-setup.ts
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
const server = setupServer(
http.get('/api/users/:id', () => HttpResponse.json({ id: 1, name: '박준성' }))
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// 테스트 안에서 응답 변경
test('서버 에러 표시', async () => {
server.use(
http.get('/api/users/:id', () => HttpResponse.error())
);
render(<UserProfile id={1} />);
expect(await screen.findByText(/에러/)).toBeInTheDocument();
});
MSW 가 진짜 가치 — 네트워크 레이어에서 가로채기. fetch·axios·React Query 어느 도구로 호출해도 동일하게 가짜 응답. 개발 중에도 사용 가능 (서버 없이 프론트 개발).
테스트는 얼마나 써야 하나 — 100% 커버리지는 함정. 핵심 흐름 (로그인·결제·핵심 기능) 만 잘 테스트하는 게 80/20. 컴포넌트 단위보다 사용자 시나리오 단위 가 ROI 높다. e2e 테스트 (Playwright) 와 결합하면 더 완성.
21편으로 React 의 "신뢰 가능한 코드 작성" 까지 끝. 22편부터는 시각·구조 정리 — TypeScript 와 결합한 폼 검증 (Zod + React Hook Form) 으로 production 폼의 표준.
다음 글
React 교재 22편 — Zod + React Hook Form. 회사 stack 의 폼 검증 표준. 타입 안전 + 런타임 검증.