React Testing Library와 Vitest로 컴포넌트를 테스트하는 방법, 좋은 테스트의 기준, 사용자 관점 테스트 철학을 실전 코드로 배웁니다.
🧪 왜 컴포넌트 테스트가 필요한가
"테스트 없이도 앱이 잘 돌아가는데 왜 시간 들여 테스트를 작성해야 하나?" 이 질문의 답은 리팩토링 자신감에 있습니다. 코드를 변경할 때마다 손으로 모든 기능을 확인하는 건 비효율적이고 빠뜨리기 쉽습니다. 테스트가 있으면 실수를 즉시 발견할 수 있습니다.
리액트 컴포넌트 테스트의 핵심 철학: "사용자처럼 테스트하라." DOM 내부 구현보다 사용자가 보고 클릭하는 것을 테스트합니다.
📦 설치 및 설정
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
},
})
// src/test/setup.ts
import '@testing-library/jest-dom'
✅ 첫 번째 테스트 — Counter 컴포넌트
// Counter.tsx
import { useState } from 'react'
function Counter({ initialCount = 0 }: { initialCount?: number }) {
const [count, setCount] = useState(initialCount)
return (
<div>
<p data-testid="count">현재 값: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>증가</button>
<button onClick={() => setCount((c) => c - 1)}>감소</button>
<button onClick={() => setCount(0)}>초기화</button>
</div>
)
}
// Counter.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Counter from './Counter'
describe('Counter', () => {
it('초기값이 0으로 렌더링된다', () => {
render(<Counter />)
expect(screen.getByTestId('count')).toHaveTextContent('현재 값: 0')
})
it('initialCount prop을 받아 그 값으로 시작한다', () => {
render(<Counter initialCount={10} />)
expect(screen.getByTestId('count')).toHaveTextContent('현재 값: 10')
})
it('증가 버튼 클릭 시 값이 1 올라간다', async () => {
const user = userEvent.setup()
render(<Counter />)
await user.click(screen.getByRole('button', { name: '증가' }))
expect(screen.getByTestId('count')).toHaveTextContent('현재 값: 1')
})
it('초기화 버튼 클릭 시 0으로 돌아간다', async () => {
const user = userEvent.setup()
render(<Counter initialCount={5} />)
await user.click(screen.getByRole('button', { name: '증가' }))
await user.click(screen.getByRole('button', { name: '초기화' }))
expect(screen.getByTestId('count')).toHaveTextContent('현재 값: 0')
})
})
🔎 쿼리 우선순위 — 어떻게 요소를 찾을까
React Testing Library는 사용자가 요소를 찾는 방식을 모방합니다:
| 쿼리 | 예시 | 사용 시점 |
|---|---|---|
getByRole | getByRole('button', {name: '로그인'}) | 접근성 역할로 — 가장 권장 |
getByLabelText | getByLabelText('이메일') | 폼 입력 — label과 연결 |
getByPlaceholderText | getByPlaceholderText('검색...') | placeholder로 찾기 |
getByText | getByText('제출') | 텍스트 내용으로 |
getByTestId | getByTestId('count') | 최후 수단 — 구현에 의존적 |
📝 폼 테스트
it('로그인 폼을 올바르게 제출한다', async () => {
const handleSubmit = vi.fn()
const user = userEvent.setup()
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',
})
})
✅ 정리
- React Testing Library + Vitest: 컴포넌트를 사용자 관점에서 테스트합니다.
- 요소 찾기:
getByRole>getByLabelText>getByText>getByTestId순서로 우선합니다. userEvent: 실제 사용자 동작(타이핑, 클릭)을 시뮬레이션합니다.- 구현이 아닌 동작(behavior)을 테스트하세요 — 내부 state가 아닌 화면에 무엇이 보이는지.
관련 주제
- 에러 바운더리 개념
- 폼 검증
- 사용자 친화적 에러 표시
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 리액트 입문 — 컴포넌트로 만드는 웹
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
댓글
0/1000
불러오는 중...
