stack-analysis
CHAPTER 11 / 90
읽기 약 2분
FUNCTION
테스트: Testing Library + Vitest
핵심 개념
컴포넌트 테스트·접근성 기반 쿼리·MSW 목 API — CRUD 컴포넌트 테스트.
본문
Vitest 설정
pnpm add -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: ['./test/setup.ts'],
globals: true,
},
});
// test/setup.ts
import '@testing-library/jest-dom/vitest';컴포넌트 테스트 — 접근성 기반
// SignupForm.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SignupForm } from './SignupForm';
describe('SignupForm', () => {
it('이메일·비밀번호 입력 후 가입 버튼 활성화', async () => {
const user = userEvent.setup();
render(<SignupForm />);
// 접근성 기반 쿼리 — role + name
const emailInput = screen.getByRole('textbox', { name: /이메일/i });
const passwordInput = screen.getByLabelText(/비밀번호/i);
const submitBtn = screen.getByRole('button', { name: /가입/i });
await user.type(emailInput, 'alice@example.com');
await user.type(passwordInput, 'Pass1234');
expect(submitBtn).toBeEnabled();
});
it('이메일 형식 오류 시 에러 표시', async () => {
const user = userEvent.setup();
render(<SignupForm />);
const emailInput = screen.getByRole('textbox', { name: /이메일/i });
await user.type(emailInput, 'invalid');
await user.tab();
expect(await screen.findByText(/올바른 이메일/i)).toBeInTheDocument();
});
it('성공 시 onSuccess 콜백', async () => {
const onSuccess = vi.fn();
const user = userEvent.setup();
render(<SignupForm onSuccess={onSuccess} />);
await user.type(screen.getByLabelText(/이메일/i), 'alice@example.com');
await user.type(screen.getByLabelText(/비밀번호/i), 'Pass1234');
await user.click(screen.getByRole('button', { name: /가입/i }));
expect(onSuccess).toHaveBeenCalledWith({
email: 'alice@example.com',
password: 'Pass1234',
});
});
});MSW (Mock Service Worker)
// pnpm add -D msw
// mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
]);
}),
http.post('/api/users', async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: 3, ...body });
}),
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({ id: Number(params.id), name: 'Test' });
}),
];
// mocks/node.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// test/setup.ts
import { server } from '../mocks/node';
import { beforeAll, afterEach, afterAll } from 'vitest';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());비동기 테스트
import { render, screen, waitFor } from '@testing-library/react';
it('사용자 목록을 가져와서 표시', async () => {
render(<UserList />);
// 로딩 상태
expect(screen.getByText(/로딩/i)).toBeInTheDocument();
// 데이터 로드 후
expect(await screen.findByText('Alice')).toBeInTheDocument();
expect(screen.getByText('Bob')).toBeInTheDocument();
// 또는 명시적 waitFor
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument();
});
});쿼리 우선순위 (접근성 우선)
1. getByRole (button, textbox, heading 등)
2. getByLabelText (form 입력)
3. getByPlaceholderText
4. getByText
5. getByDisplayValue (form 값)
6. getByAltText (이미지)
7. getByTitle
8. getByTestId (마지막 수단)
→ 사용자가 보는 방식대로 테스트 = 리팩터링 안전다음 챕터
CH.12 "Storybook" — 컴포넌트 문서화.
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 코드의 컴포넌트 테스트 부분을 분석해서 실전 분석 + 개선 우선순위를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
컴포넌트 테스트 관련 인기 라이브러리/패턴 5개를 비교 분석해서 패턴 추출를 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 프로젝트 전체에서 컴포넌트 테스트 최적화 가능 위치를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 프론트엔드 시장의 컴포넌트 테스트 트렌드를 솔직히 알려줘.
⭐ 이것만 기억하세요
테스트: Testing Library + Vitest는 이 3가지만 확실히 잡으세요
1.Testing Library는 접근성 기반 쿼리 — 사용자 관점 테스트로 리팩터링 안전
2.MSW로 fetch/axios 가로채기 + 환경 무관 (Node + Browser 모두)
3.다음 챕터 CH.12에서 Storybook — 디자인 시스템 문서화
공유하기
진행도 11 / 90