architecture
테스팅 철학 — 무엇을, 어떻게, 왜 테스트하는가
"테스트 코드를 작성하시나요? 어떤 전략으로 테스트하시나요?"는 중급 이상 면접의 필수 질문입니다. "단위 테스트 100% 커버리지"가 목표가 아닌 이유, React Testing Library가 Enzyme을 대체한 이유, E2E 테스트를 적게 쓰는 이유를 설명할 수 있어야 합니다. 테스트를 "귀찮은 것"이 아니라 "설계 도구"로 바라보는 관점이 중요합니다.
학습 개요
탄생 배경
해결하려 했던 문제
빠르게 변하는 프론트엔드 코드에서 "이 기능이 여전히 동작하는가?"를 수동으로 확인하는 것은 스케일하지 않습니다. 새 기능을 추가할 때마다 기존 기능이 깨지는 "회귀(regression)" 문제가 반복됐습니다. 코드가 복잡해질수록 리팩터링이 두려워지고, 결국 기술 부채가 쌓입니다. 테스트는 코드 변경에 대한 자신감을 주는 안전망입니다.
역사적 맥락
Kent Beck이 1999년 Extreme Programming에서 TDD(Test-Driven Development)를 제시했습니다. JUnit이 자바 테스팅 표준이 되고, Jest(2014)가 JavaScript에서 같은 역할을 했습니다. 초기 React 테스트는 Enzyme(내부 구현 테스트)을 사용했으나, Kent C. Dodds가 RTL(React Testing Library, 2018)을 출시하면서 "사용자 행동 중심 테스트"로 패러다임이 전환됐습니다. Playwright, Cypress가 E2E 테스트 표준으로 부상했습니다.
이전에는 어떻게 했나
프로덕션에 카나리 배포하고 실제 트래픽으로 검증하는 방식(피처 플래그 + 점진적 배포)이 대안으로 제시되기도 합니다. TypeScript의 타입 시스템이 일부 런타임 오류를 빌드 타임에 잡아줍니다. 하지만 타입 체킹과 테스팅은 보완적이지 대체 관계가 아닙니다.
멘탈 모델
동작 원리
테스팅 피라미드 (Testing Pyramid): /----E2E---- ← 적게: 느리고 비용 큼, 최종 사용자 흐름 /---통합 테스트-- ← 중간: 컴포넌트 조합, API 연동 /----단위 테스트---- ← 많이: 빠르고 저렴, 순수 함수/컴포넌트 단위 테스트 (Unit Test): - 독립적인 함수/컴포넌트 테스트 - 의존성 Mock으로 격리 - 빠른 실행, 높은 커버리지 - 도구: Jest, Vitest 통합 테스트 (Integration Test): - 여러 컴포넌트/모듈의 상호작용 테스트 - 실제 의존성 사용 (Mock 최소화) - RTL(React Testing Library) 적합 - "사용자가 실제로 하는 것"을 시뮬레이션 E2E 테스트 (End-to-End Test): - 실제 브라우저에서 전체 흐름 테스트 - 회원가입 → 로그인 → 결제 같은 핵심 시나리오 - 느림, 비용 큼, 유지보수 어려움 - 도구: Playwright, Cypress "무엇을 테스트해야 하는가": 행동(behavior)을 테스트, 구현(implementation)을 테스트 X
핵심 구성 요소
단위 테스트
순수함수, 유틸리티, 독립 컴포넌트. 빠르고 많을수록 좋음
통합 테스트
컴포넌트 조합, 사용자 인터랙션. RTL의 핵심 영역
E2E 테스트
전체 사용자 흐름. 핵심 시나리오만 선택적으로
Mock
외부 의존성(API, 모듈)을 제어 가능한 가짜로 교체
커버리지
실행된 코드 비율. 100%보다 "올바른 테스트"가 중요
RTL 철학
"DOM 구조가 아닌 사용자 행동 중심 테스트" — getByRole, getByText
흐름 설명
[RTL 테스트 사고방식]
// ❌ Enzyme 방식 — 구현 테스트 (내부 state, 메서드 이름)
const wrapper = shallow(<Counter />);
wrapper.instance().increment();
expect(wrapper.state('count')).toBe(1);
// 내부 구현이 바뀌면 테스트도 깨짐
// ✅ RTL 방식 — 행동 테스트 (사용자가 보는 것)
render(<Counter />);
userEvent.click(screen.getByRole('button', { name: '증가' }));
expect(screen.getByText('1')).toBeInTheDocument();
// 내부 구현이 바뀌어도 사용자 경험이 같으면 테스트 통과
[테스트 작성 순서 — AAA 패턴]
test('로그인 성공 시 대시보드로 이동', async () => {
// Arrange: 테스트 환경 준비
render(<LoginPage />);
// Act: 사용자 행동 수행
await userEvent.type(screen.getByLabelText('이메일'), 'test@test.com');
await userEvent.type(screen.getByLabelText('비밀번호'), 'password123');
await userEvent.click(screen.getByRole('button', { name: '로그인' }));
// Assert: 결과 확인
expect(screen.getByText('대시보드')).toBeInTheDocument();
});
코드 예제
// ✅ 단위 테스트 — 순수함수
// utils/formatDate.ts
export function formatDate(date: Date, locale = 'ko-KR') {
return new Intl.DateTimeFormat(locale).format(date);
}
// utils/formatDate.test.ts
describe('formatDate', () => {
it('한국어 날짜 형식으로 포맷', () => {
const date = new Date('2024-01-15');
expect(formatDate(date)).toBe('2024. 1. 15.');
});
});
// ✅ 통합 테스트 — RTL
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TodoApp } from './TodoApp';
test('할 일을 추가하고 완료 표시', async () => {
render(<TodoApp />);
// 할 일 추가
const input = screen.getByPlaceholderText('할 일 입력');
await userEvent.type(input, '리액트 공부');
await userEvent.keyboard('{Enter}');
// 추가됐는지 확인
const todoItem = screen.getByText('리액트 공부');
expect(todoItem).toBeInTheDocument();
// 완료 표시
await userEvent.click(screen.getByRole('checkbox', { name: '리액트 공부' }));
expect(todoItem).toHaveClass('completed');
});
// ✅ API Mock — MSW (Mock Service Worker)
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());
비교 분석
테스팅
vsTDD vs 테스트 나중에 작성
테스팅
vsRTL vs Enzyme
트레이드오프
이상적인 사용 사례