프론트엔드 테스트 전략 - 현대 웹 개발의 테스트 방법론 완벽 가이드
TL;DR
- 테스트 피라미드/트로피로 단위·통합·E2E 비중을 결정한다.
- React Testing Library, MSW, Storybook 등 현대 도구 조합을 설명한다.
- 시각 회귀와 접근성, 네트워크 시나리오까지 포괄한다.
1. 개념
프론트엔드 테스트 전략은 테스트 유형과 도구를 체계적으로 설계하는 접근이다.
2. 배경
브라우저·디바이스 다양성과 UI 회귀 위험이 테스트 요구를 키웠다.
3. 이유
품질과 배포 속도의 균형을 위해 체계적 자동화가 필요하다.
4. 특징
피라미드/트로피 모델, 테스트 더블, 시각 회귀, E2E 자동화가 포함된다.
5. 상세 내용
프론트엔드 테스트 전략 - 현대 웹 개발의 테스트 방법론 완벽 가이드
작성일: 2026-02-27 카테고리: Frontend / Testing / Best Practices 포함 내용: Test Pyramid, Testing Trophy, Kent C. Dodds, Vitest, Jest, Playwright, Cypress, React Testing Library, MSW, Storybook, E2E Test, Unit Test, Integration Test, Visual Regression
1. 프론트엔드 테스트가 왜 필요한가?
1.1 프론트엔드의 특수성 - 백엔드와 무엇이 다른가?
┌─────────────────────────────────────────────────────────────────┐
│ 프론트엔드 테스트가 특별한 이유 │
│ │
│ 비유: 레스토랑의 "홀 서비스" │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 백엔드 = 주방 (Kitchen) │ │
│ │ ├── 손님이 직접 보지 않는 영역 │ │
│ │ ├── 재료(데이터)를 가공하는 로직 │ │
│ │ ├── 동일한 레시피 → 동일한 결과 │ │
│ │ └── 환경이 통제됨 (서버 1대, OS 1개) │ │
│ │ │ │
│ │ 프론트엔드 = 홀 서비스 (Dining Hall) │ │
│ │ ├── 손님이 직접 보고, 만지고, 느끼는 영역 │ │
│ │ ├── 다양한 손님 (Chrome, Safari, Firefox, 모바일) │ │
│ │ ├── 같은 요리도 그릇 크기에 따라 다르게 보임 (반응형) │ │
│ │ ├── 손님이 예상치 못한 행동을 함 (더블 클릭, 뒤로 가기) │ │
│ │ └── "맛"뿐 아니라 "보이는 것"까지 신경 써야 함 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 핵심 차이: │
│ ├── 백엔드: Input → Output 검증 (결정적, deterministic) │
│ └── 프론트엔드: Input → 화면 + 인터랙션 + 시각적 결과 검증 │
│ │
│ 수동 테스트의 한계: │
│ ├── 기능 100개 × 브라우저 5개 × 해상도 3개 = 1,500가지 조합 │
│ ├── 코드 한 줄 수정할 때마다 전부 다시 확인? → 불가능 │
│ ├── 회귀 테스트(Regression) 비용 기하급수적 증가 │
│ └── 자동화 없이는 배포할 때마다 "기도 메타" 🙏 │
│ │
└─────────────────────────────────────────────────────────────────┘
1.2 프론트엔드 테스트의 고유 과제
┌─────────────────────────────────────────────────────────────────┐
│ 프론트엔드에서 테스트해야 할 영역들 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ UI │ │ 사용자 │ │ API │ │ 상태 │ │
│ │ 렌더링 │ │ 이벤트 │ │ 통신 │ │ 관리 │ │
│ │ │ │ │ │ │ │ │ │
│ │ HTML/CSS │ │ Click │ │ fetch() │ │ Redux │ │
│ │ 올바른 │ │ Scroll │ │ REST │ │ Zustand │ │
│ │ 출력? │ │ Input │ │ GraphQL │ │ React │ │
│ │ │ │ Drag │ │ WebSocket│ │ Query │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ └──────────────┼──────────────┼──────────────┘ │
│ │ │ │
│ ┌───────┴──────┐ ┌──┴──────────┐ │
│ │ 라우팅 │ │ 접근성 │ │
│ │ │ │ (a11y) │ │
│ │ URL 변경 │ │ │ │
│ │ 히스토리 │ │ 스크린리더 │ │
│ │ 딥 링크 │ │ 키보드 탐색 │ │
│ └──────────────┘ └─────────────┘ │
│ │
│ 이 모든 것을 "브라우저"라는 복잡한 환경에서 테스트해야 함 │
│ + 크로스 브라우저 (Chrome, Safari, Firefox, Edge) │
│ + 반응형 (모바일, 태블릿, 데스크톱) │
│ + 다크 모드 / 라이트 모드 │
│ + 네트워크 상태 (오프라인, 느린 3G) │
│ │
└─────────────────────────────────────────────────────────────────┘
1.3 프론트엔드 테스트가 주는 가치
┌─────────────────────────────────────────────────────────────────┐
│ 테스트가 없을 때 vs 있을 때 │
│ │
│ 테스트 없음: │
│ ├── "배포하면 뭔가 깨질 것 같아서 무서움" │
│ ├── 리팩토링 → 어디가 깨졌는지 모름 → 리팩토링 포기 │
│ ├── 코드 리뷰 → "동작하는지는... 배포해봐야 알아요" │
│ ├── 새 기능 추가 → 기존 기능 깨짐 → 핫픽스 → 야근 │
│ └── 신뢰도: ★☆☆☆☆ │
│ │
│ 적절한 테스트: │
│ ├── "이 변경이 안전한지 CI가 알려줌" │
│ ├── 리팩토링 → 테스트 통과 확인 → 자신감 있게 머지 │
│ ├── 코드 리뷰 → "테스트도 다 통과하네요" │
│ ├── 새 기능 추가 → 기존 테스트가 회귀(regression) 잡아줌 │
│ └── 신뢰도: ★★★★★ │
│ │
│ ROI (투자 대비 효과): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 시간 → ──────────────────────────────────► │ │
│ │ │ │
│ │ 테스트 없음: ──╱──╱──╱──╱ (버그 수정 비용 계속 증가) │ │
│ │ 테스트 있음: ────────────── (초기 투자 후 안정) │ │
│ │ │ │
│ │ 교차점: 약 3-6개월 후부터 테스트 있는 쪽이 더 빠름 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
2. 테스트 피라미드 (Test Pyramid) - Mike Cohn (2009)
2.1 원래 개념
┌─────────────────────────────────────────────────────────────────┐
│ 테스트 피라미드 (Test Pyramid) │
│ │
│ 2009년 Mike Cohn이 "Succeeding with Agile" 책에서 제안 │
│ "테스트를 피라미드 모양으로 구성하라" │
│ │
│ /\ │
│ / \ ← E2E (End-to-End) │
│ / E2E\ 적게, 느리고, 비싸다 │
│ /──────\ │
│ / \ │
│ / Integra- \ ← Integration │
│ / tion \ 중간 정도 │
│ /──────────────\ │
│ / \ │
│ / Unit Tests \ ← Unit │
│ / \ 많이, 빠르고, 싸다 │
│ /──────────────────────\ │
│ │
│ 핵심 원칙: │
│ ├── 아래로 갈수록 → 많이, 빠르게, 저렴하게 │
│ ├── 위로 갈수록 → 적게, 느리게, 비싸게 │
│ ├── 피라미드를 뒤집으면 "아이스크림 콘" → 안티패턴! │
│ └── 2009년 기준: 백엔드 중심의 모노리스 아키텍처에 최적화 │
│ │
│ 역사적 맥락: │
│ ├── 2009년은 jQuery 전성기, React/Vue 없던 시절 │
│ ├── 프론트엔드 = HTML + jQuery 약간 │
│ ├── "테스트" = 주로 서버 사이드 로직 테스트 │
│ └── 이 피라미드는 백엔드에 매우 잘 맞음 │
│ │
└─────────────────────────────────────────────────────────────────┘
2.2 계층별 비용/속도/신뢰도 비교
┌─────────────────────────────────────────────────────────────────┐
│ 테스트 계층별 특성 비교 │
│ │
│ ┌────────────┬──────────┬──────────┬──────────┬────────────┐ │
│ │ 계층 │ 속도 │ 비용 │ 신뢰도 │ 유지보수 │ │
│ ├────────────┼──────────┼──────────┼──────────┼────────────┤ │
│ │ Unit │ ★★★★★ │ ★☆☆☆☆ │ ★★☆☆☆ │ ★★★★★ │ │
│ │ │ ~1ms │ 매우 쌈 │ 낮음 │ 매우 쉬움 │ │
│ ├────────────┼──────────┼──────────┼──────────┼────────────┤ │
│ │ Integra- │ ★★★☆☆ │ ★★★☆☆ │ ★★★★☆ │ ★★★☆☆ │ │
│ │ tion │ ~100ms │ 보통 │ 높음 │ 보통 │ │
│ ├────────────┼──────────┼──────────┼──────────┼────────────┤ │
│ │ E2E │ ★☆☆☆☆ │ ★★★★★ │ ★★★★★ │ ★☆☆☆☆ │ │
│ │ │ ~10-30s │ 매우 비쌈│ 매우 높음│ 매우 어려움│ │
│ └────────────┴──────────┴──────────┴──────────┴────────────┘ │
│ │
│ 프론트엔드에서의 문제점: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Q: Unit Test를 많이 작성하면 사용자 경험이 보장되나? │ │
│ │ A: 아니오! 각 함수가 올바르게 동작해도, │ │
│ │ 조합했을 때 화면이 깨질 수 있다. │ │
│ │ │ │
│ │ 비유: 기타, 드럼, 베이스, 보컬 각각은 완벽한데 │ │
│ │ 합주하면 엉망인 밴드 → "합주 연습"이 중요하다! │ │
│ │ │ │
│ │ → 프론트엔드에서는 "Integration Test"가 가장 중요! │ │
│ │ → 이것이 Testing Trophy의 탄생 배경 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
2.3 피라미드의 한계 - 프론트엔드에서의 부적합성
┌─────────────────────────────────────────────────────────────────┐
│ 피라미드가 프론트엔드에 맞지 않는 이유 │
│ │
│ 문제 1: "Unit"의 정의가 모호 │
│ ├── 백엔드: Service 클래스의 메서드 = 명확한 단위 │
│ ├── 프론트엔드: 컴포넌트? 함수? 커스텀 훅? 무엇이 "단위"? │
│ └── React 컴포넌트는 UI + 로직 + 상태가 섞여있음 │
│ │
│ 문제 2: 내부 구현에 결합되기 쉬움 │
│ ├── "state가 3으로 변했는지" 테스트 │
│ │ → 리팩토링하면 깨짐 (false negative) │
│ ├── "onClick 핸들러가 호출되었는지" 테스트 │
│ │ → 사용자 경험과 무관한 검증 │
│ └── 사용자는 state 값을 모름 → 화면에 뭐가 보이는지만 중요 │
│ │
│ 문제 3: 단위 테스트 많아도 버그 발생 │
│ ├── 컴포넌트 A 테스트 통과 ✓ │
│ ├── 컴포넌트 B 테스트 통과 ✓ │
│ ├── A + B 조합하면? → 스타일 겹침, 이벤트 충돌 │
│ └── 단위 테스트는 "조합"을 검증하지 못함 │
│ │
│ 비유: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 부품 검사: │ │
│ │ ├── 왼쪽 서랍 열림 ✓ │ │
│ │ ├── 오른쪽 서랍 열림 ✓ │ │
│ │ └── 두 서랍 동시에 열면? → 부딪힘! 💥 │ │
│ │ │ │
│ │ → 통합(Integration) 테스트가 이런 문제를 잡아낸다 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
3. 테스트 트로피 (Testing Trophy) - Kent C. Dodds (2018)
3.1 왜 피라미드가 프론트엔드에 부족한가
┌─────────────────────────────────────────────────────────────────┐
│ Testing Trophy 탄생 배경 │
│ │
│ Kent C. Dodds (켄트 C. 도즈): │
│ ├── PayPal, Remix 등에서 활동한 React 생태계 핵심 인물 │
│ ├── React Testing Library 제작자 │
│ ├── Testing JavaScript (testingjavascript.com) 저자 │
│ └── 2018년 블로그 "Write tests. Not too many. Mostly │
│ integration."에서 Testing Trophy 제안 │
│ │
│ Guillermo Rauch (기예르모 라우치, Vercel CEO)의 유명 트윗: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ "Write tests. Not too many. Mostly integration." │ │
│ │ 테스트를 작성하라. 너무 많이 말고. 대부분 통합 테스트. │ │
│ │ │ │
│ │ — Guillermo Rauch, 2016년 12월 tweet │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Kent C. Dodds는 이 트윗을 발전시켜 │
│ 프론트엔드에 최적화된 테스트 모델을 만들었다. │
│ │
└─────────────────────────────────────────────────────────────────┘
3.2 Testing Trophy 구조
┌─────────────────────────────────────────────────────────────────┐
│ Testing Trophy (테스트 트로피) │
│ │
│ 피라미드가 아니라 "트로피" 모양: │
│ Integration 계층이 가장 넓다! │
│ │
│ ╱╲ │
│ ╱ ╲ ← E2E (End-to-End) │
│ ╱ E2E╲ 소수의 핵심 시나리오만 │
│ ╱──────╲ │
│ ╱ ╲ │
│ ╱ ╲ │
│ ╱ Integration ╲ ← Integration │
│ ╱ ╲ ★ 가장 많이 투자! │
│ ╱ (가장 넓은 영역!) ╲ ★ 최고의 ROI │
│ ╱────────────────────────────╲ │
│ ╲ ╱ │
│ ╲ Unit ╱ ← Unit │
│ ╲ ╱ 복잡한 로직만 │
│ ╲──────────────╱ │
│ ╲ ╱ │
│ ╲Static╱ ← Static Analysis │
│ ╲──╱ ESLint, TypeScript │
│ ╲╱ │
│ ▐▐ ← 트로피 받침대 │
│ ▐▐▐▐▐▐ │
│ │
│ 4계층 상세: │
│ ┌────────────┬──────────────────────────────────────────┐ │
│ │ Static │ ESLint, TypeScript, Prettier │ │
│ │ Analysis │ 코드를 실행하지 않고 잡는 오류 │ │
│ │ │ 오타, 타입 에러, 미사용 변수 등 │ │
│ ├────────────┼──────────────────────────────────────────┤ │
│ │ Unit │ 순수 함수, 유틸리티, 커스텀 훅 │ │
│ │ │ 복잡한 비즈니스 로직에만 집중 │ │
│ ├────────────┼──────────────────────────────────────────┤ │
│ │ Integration│ 컴포넌트 + 훅 + 상태 + API 모킹 │ │
│ │ ★ 핵심 │ 사용자 관점에서 컴포넌트 동작 검증 │ │
│ │ │ React Testing Library + MSW 조합 │ │
│ ├────────────┼──────────────────────────────────────────┤ │
│ │ E2E │ 실제 브라우저에서 전체 시나리오 테스트 │ │
│ │ │ 로그인 → 주문 → 결제 같은 핵심 플로우 │ │
│ └────────────┴──────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
3.3 핵심 철학 - “사용자처럼 테스트하라”
┌─────────────────────────────────────────────────────────────────┐
│ Kent C. Dodds의 Testing Philosophy │
│ │
│ 핵심 명언: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ "The more your tests resemble the way your software │ │
│ │ is used, the more confidence they can give you." │ │
│ │ │ │
│ │ "테스트가 소프트웨어의 실제 사용 방식과 닮을수록, │ │
│ │ 더 많은 자신감을 줄 수 있다." │ │
│ │ │ │
│ │ — Kent C. Dodds, 2018 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 나쁜 테스트 (내부 구현 의존): │
│ ├── "state.count가 3인지 확인" │
│ │ → 사용자는 state를 모른다! │
│ ├── "handleClick이 호출되었는지 확인" │
│ │ → 사용자는 함수 이름을 모른다! │
│ └── "컴포넌트가 re-render 되었는지 확인" │
│ → 사용자는 render를 모른다! │
│ │
│ 좋은 테스트 (사용자 관점): │
│ ├── "버튼을 3번 클릭하면 화면에 '3'이 보인다" │
│ ├── "이메일 입력 후 제출하면 '가입 완료' 메시지가 보인다" │
│ └── "로딩 중에는 스피너가 보이고, 완료 후 데이터가 보인다" │
│ │
│ 원칙 요약: │
│ ├── 사용자가 보는 것(텍스트, role)으로 요소를 찾아라 │
│ ├── 사용자가 하는 것(클릭, 입력)으로 상호작용하라 │
│ └── 사용자가 기대하는 것(화면 변화)을 검증하라 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.4 피라미드 vs 트로피 - 시각적 비교
┌─────────────────────────────────────────────────────────────────┐
│ 피라미드 vs 트로피 직접 비교 │
│ │
│ Test Pyramid (2009) Testing Trophy (2018) │
│ ───────────────── ────────────────── │
│ │
│ /\ ╱╲ │
│ / \ ╱ ╲ │
│ / E2E\ ╱ E2E╲ │
│ /──────\ ╱──────╲ │
│ / \ ╱ ╲ │
│ / Integra- \ ╱ Integration ╲ ← 핵심! │
│ / tion \ ╱ (가장 넓음!) ╲ │
│ /──────────────\ ╱────────────────────╲ │
│ / \ ╲ Unit ╱ │
│ / Unit Tests \ ╲ ╱ │
│/────────────────────\ ╲──────────╱ │
│ ╲Static╱ │
│ ╲──╱ │
│ │
│ 차이점: │
│ ┌──────────────┬──────────────────┬──────────────────┐ │
│ │ 항목 │ 피라미드 │ 트로피 │ │
│ ├──────────────┼──────────────────┼──────────────────┤ │
│ │ Static │ 없음 │ 기반에 포함 │ │
│ │ 가장 많이 │ Unit │ Integration │ │
│ │ 대상 도메인 │ 백엔드 중심 │ 프론트엔드 최적화│ │
│ │ 핵심 가치 │ 속도 + 비용 │ 신뢰도 + ROI │ │
│ │ 출처 │ Mike Cohn │ Kent C. Dodds │ │
│ │ 연도 │ 2009 │ 2018 │ │
│ └──────────────┴──────────────────┴──────────────────┘ │
│ │
│ 결론: 프론트엔드에서는 Testing Trophy를 따르라! │
│ ├── Static: TypeScript + ESLint로 무료 안전망 확보 │
│ ├── Unit: 복잡한 순수 로직에만 집중 │
│ ├── Integration: 컴포넌트 동작을 사용자 관점으로 ★ │
│ └── E2E: 핵심 사용자 플로우만 선별적으로 │
│ │
└─────────────────────────────────────────────────────────────────┘
4. Testing Library 혁명 (2018-2020)
4.1 Kent C. Dodds의 React Testing Library
┌─────────────────────────────────────────────────────────────────┐
│ React Testing Library의 탄생 │
│ │
│ 배경: │
│ ├── 2018년 이전: Enzyme이 React 테스트의 사실상 표준 │
│ ├── Enzyme: Airbnb가 만든 React 컴포넌트 테스트 라이브러리 │
│ ├── 문제: Enzyme은 컴포넌트의 "내부"를 테스트하는 데 집중 │
│ └── Kent C. Dodds: "이건 사용자 관점이 아니다!" │
│ │
│ React Testing Library 핵심 원칙: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ "The more your tests resemble the way your software │ │
│ │ is used, the more confidence they can give you." │ │
│ │ │ │
│ │ → 컴포넌트의 내부(state, props, lifecycle)가 아니라 │ │
│ │ 사용자가 보고 상호작용하는 것을 테스트하라! │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Enzyme vs React Testing Library: │
│ ┌──────────────────┬──────────────────────────────────────┐ │
│ │ Enzyme │ React Testing Library │ │
│ ├──────────────────┼──────────────────────────────────────┤ │
│ │ shallow render │ full render (사용자처럼) │ │
│ │ instance() │ screen.getByRole() │ │
│ │ state() 직접접근 │ 화면에 보이는 텍스트로 검증 │ │
│ │ simulate('click')│ userEvent.click() │ │
│ │ 내부 구현 테스트 │ 사용자 행동 테스트 │ │
│ │ 리팩토링 시 깨짐 │ 리팩토링에 강건 │ │
│ └──────────────────┴──────────────────────────────────────┘ │
│ │
│ 코드 비교: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // Enzyme (내부 구현 의존) │ │
│ │ const wrapper = shallow(<Counter />); │ │
│ │ wrapper.find('button').simulate('click'); │ │
│ │ expect(wrapper.state('count')).toBe(1); // state 직접! │ │
│ │ │ │
│ │ // React Testing Library (사용자 관점) │ │
│ │ render(<Counter />); │ │
│ │ await userEvent.click( │ │
│ │ screen.getByRole('button', { name: '증가' }) │ │
│ │ ); │ │
│ │ expect(screen.getByText('1')).toBeInTheDocument(); │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4.2 Enzyme의 몰락
┌─────────────────────────────────────────────────────────────────┐
│ Enzyme의 흥망성쇠 │
│ │
│ 타임라인: │
│ ├── 2015: Airbnb가 Enzyme 출시 → React 테스트 표준 등극 │
│ ├── 2018: React Testing Library 출시 → 대안 등장 │
│ ├── 2019: React Hooks 도입 → Enzyme의 shallow render 깨짐 │
│ ├── 2020: Enzyme 어댑터 업데이트 지연, 커뮤니티 이탈 │
│ ├── 2021: React 18 → Enzyme 공식 지원 불가 │
│ └── 2022+: Airbnb 유지보수 사실상 중단 │
│ │
│ 왜 Enzyme이 무너졌는가? │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 근본 원인: "내부 구현에 의존하는 테스트" │ │
│ │ │ │
│ │ 1. React Hooks (2019) │ │
│ │ ├── Class → Function 컴포넌트로 대전환 │ │
│ │ ├── state(), instance() → 더 이상 접근 불가 │ │
│ │ └── shallow render에서 Hooks 제대로 동작 안 함 │ │
│ │ │ │
│ │ 2. Concurrent Mode, Suspense │ │
│ │ ├── React의 렌더링 방식 근본 변화 │ │
│ │ └── Enzyme의 동기식 simulate()로는 대응 불가 │ │
│ │ │ │
│ │ 3. 철학적 문제 │ │
│ │ ├── Enzyme: "컴포넌트가 어떻게 동작하는가" │ │
│ │ └── RTL: "사용자에게 무엇이 보이는가" │ │
│ │ │ │
│ │ 교훈: 내부 구현에 결합된 테스트는 프레임워크 변화에 │ │
│ │ 취약하다. 사용자 관점 테스트는 영속적이다. │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 2025년 현재 npm 다운로드 추이: │
│ ├── @testing-library/react: 주간 ~12M 다운로드 │
│ ├── enzyme: 주간 ~1.5M (레거시 유지보수만) │
│ └── React Testing Library = 사실상 유일한 표준 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.3 Query Priority - 요소를 찾는 올바른 순서
┌─────────────────────────────────────────────────────────────────┐
│ Testing Library Query Priority (우선순위) │
│ │
│ "어떤 쿼리로 DOM 요소를 찾을 것인가?" │
│ → 사용자/접근성과 가까운 순서대로 사용하라! │
│ │
│ 우선순위 (높음 → 낮음): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1순위: getByRole('button', { name: '제출' }) │ │
│ │ ────────────────────────────────────────────── │ │
│ │ ├── 접근성(a11y) 트리 기반 │ │
│ │ ├── 스크린리더가 읽는 것과 동일 │ │
│ │ ├── 가장 권장되는 방법 │ │
│ │ └── 접근성이 좋은 코드 = 테스트하기 쉬운 코드 │ │
│ │ │ │
│ │ 2순위: getByLabelText('이메일') │ │
│ │ ────────────────────────────────────────────── │ │
│ │ ├── 폼 요소에 최적 │ │
│ │ └── <label>과 연결된 input을 찾음 │ │
│ │ │ │
│ │ 3순위: getByPlaceholderText('검색어 입력') │ │
│ │ ────────────────────────────────────────────── │ │
│ │ └── label이 없을 때 차선책 │ │
│ │ │ │
│ │ 4순위: getByText('장바구니에 담기') │ │
│ │ ────────────────────────────────────────────── │ │
│ │ ├── 비-인터랙티브 요소에 적합 │ │
│ │ └── 텍스트 콘텐츠로 찾기 │ │
│ │ │ │
│ │ 5순위: getByDisplayValue('kim@email.com') │ │
│ │ ────────────────────────────────────────────── │ │
│ │ └── 현재 입력된 값으로 찾기 │ │
│ │ │ │
│ │ 6순위: getByAltText('프로필 사진') │ │
│ │ ────────────────────────────────────────────── │ │
│ │ └── 이미지, area 요소에 사용 │ │
│ │ │ │
│ │ 7순위: getByTitle('닫기') │ │
│ │ ────────────────────────────────────────────── │ │
│ │ └── title 속성으로 찾기 │ │
│ │ │ │
│ │ 최후 수단: getByTestId('submit-btn') │ │
│ │ ────────────────────────────────────────────── │ │
│ │ ├── data-testid 속성 사용 │ │
│ │ ├── 사용자에게 보이지 않는 식별자 │ │
│ │ └── 위 방법이 모두 불가능할 때만 사용! │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 왜 이 순서인가? │
│ ├── 1순위(Role): 접근성이 좋은 앱 = 테스트하기 쉬운 앱 │
│ ├── 중간순위: 사용자가 인식할 수 있는 속성들 │
│ └── 최후수단(TestId): 사용자에게 보이지 않음 = 최후의 수단 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.4 Testing Library 생태계 - React를 넘어서
┌─────────────────────────────────────────────────────────────────┐
│ Testing Library 생태계 확장 │
│ │
│ React Testing Library의 철학이 다른 프레임워크로 확산: │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ @testing-library/dom │ │
│ │ (Core - 핵심 엔진) │ │
│ │ │ │ │
│ │ ┌────────┬───────┬──┴──┬────────┬──────────┐ │ │
│ │ │ │ │ │ │ │ │ │
│ │ /react /vue /angular /svelte /preact /solid │ │
│ │ │ │ │ │ │ │ │ │
│ │ React Vue Angular Svelte Preact Solid │ │
│ │ Testing Test Testing Testing Testing Testing │ │
│ │ Library Utils Testing Library Library Testing │ │
│ │ (동일 Library Library │ │
│ │ 철학) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 동일한 API, 동일한 철학: │
│ ├── screen.getByRole() → 모든 프레임워크에서 동일 │
│ ├── userEvent.click() → 모든 프레임워크에서 동일 │
│ ├── waitFor() → 모든 프레임워크에서 동일 │
│ └── 한 번 배우면 어디서든 적용 가능! │
│ │
│ 보조 라이브러리: │
│ ├── @testing-library/user-event: 실제 사용자 이벤트 시뮬 │
│ ├── @testing-library/jest-dom: DOM 매처 (toBeInTheDocument) │
│ └── @testing-library/react-hooks: 커스텀 훅 테스트 (deprecated)│
│ → React 18부터는 renderHook()이 RTL에 내장 │
│ │
└─────────────────────────────────────────────────────────────────┘
5. 프론트엔드 테스트 도구의 역사적 진화
5.1 jQuery 시대 (2008-2014): 혼돈의 시작
┌─────────────────────────────────────────────────────────────────┐
│ Phase 1: jQuery 시대의 테스트 도구 │
│ │
│ 시대 배경: │
│ ├── jQuery가 프론트엔드의 전부이던 시절 │
│ ├── SPA(Single Page Application) 개념 초기 │
│ ├── 프론트엔드 테스트 = "해본 적 없는데요?" │
│ └── 대부분 수동 테스트 (F5 + 눈으로 확인) │
│ │
│ 주요 도구: │
│ ┌────────────┬────────┬─────────────────────────────────┐ │
│ │ 도구 │ 연도 │ 특징 │ │
│ ├────────────┼────────┼─────────────────────────────────┤ │
│ │ QUnit │ 2008 │ jQuery 팀이 만든 테스트 프레임웤│ │
│ │ │ │ jQuery 자체 테스트에 사용 │ │
│ ├────────────┼────────┼─────────────────────────────────┤ │
│ │ Jasmine │ 2010 │ BDD 스타일 (describe/it) │ │
│ │ │ │ 브라우저에서 직접 실행 │ │
│ ├────────────┼────────┼─────────────────────────────────┤ │
│ │ Mocha │ 2011 │ Node.js 기반, 유연한 구조 │ │
│ │ │ │ Chai (assertion), Sinon (mock) │ │
│ ├────────────┼────────┼─────────────────────────────────┤ │
│ │ Karma │ 2012 │ 실제 브라우저에서 테스트 실행 │ │
│ │ │ │ Angular 팀 (Vojta Jina)이 제작 │ │
│ └────────────┴────────┴─────────────────────────────────┘ │
│ │
│ 이 시대의 고통: │
│ ├── 브라우저마다 다른 동작 (IE6, IE7, IE8...) │
│ ├── 테스트 환경 설정이 매우 복잡 │
│ ├── DOM 조작 테스트 = 매우 번거로움 │
│ └── "프론트엔드 테스트? 시간 낭비야" 라는 인식 │
│ │
└─────────────────────────────────────────────────────────────────┘
5.2 React/Component 시대 (2015-2018): Jest + Enzyme
┌─────────────────────────────────────────────────────────────────┐
│ Phase 2: 컴포넌트 시대의 테스트 │
│ │
│ 시대 배경: │
│ ├── React (2013), Vue (2014), Angular 2 (2016) 등장 │
│ ├── "컴포넌트" 단위로 UI를 구성하는 패러다임 │
│ ├── Node.js 기반 빌드 도구 (Webpack, Babel) 보편화 │
│ └── 프론트엔드 테스트가 본격적으로 가능해짐 │
│ │
│ 주요 도구: │
│ ┌────────────┬────────┬─────────────────────────────────┐ │
│ │ 도구 │ 연도 │ 특징 │ │
│ ├────────────┼────────┼─────────────────────────────────┤ │
│ │ Jest │ 2014 │ Facebook(Meta)이 제작 │ │
│ │ │ │ Zero-config, Snapshot testing │ │
│ │ │ │ jsdom 내장 (브라우저 시뮬레이션) │ │
│ ├────────────┼────────┼─────────────────────────────────┤ │
│ │ Enzyme │ 2015 │ Airbnb가 제작 │ │
│ │ │ │ shallow/mount/render 3가지 모드 │ │
│ │ │ │ 컴포넌트 내부 검사에 특화 │ │
│ ├────────────┼────────┼─────────────────────────────────┤ │
│ │ Storybook │ 2016 │ 컴포넌트 카탈로그/문서화 도구 │ │
│ │ │ │ 시각적 테스트의 기반 │ │
│ └────────────┴────────┴─────────────────────────────────┘ │
│ │
│ Jest의 혁신: │
│ ├── 설정 없이 바로 사용 (Zero Configuration) │
│ ├── 병렬 실행으로 빠른 속도 │
│ ├── Snapshot Testing 도입 → "UI가 변했나?" 자동 감지 │
│ ├── 모킹(Mocking) 내장 → 외부 라이브러리 불필요 │
│ └── Watch 모드 → 변경된 파일 관련 테스트만 자동 재실행 │
│ │
└─────────────────────────────────────────────────────────────────┘
5.3 Testing Library 시대 (2018-2020)
┌─────────────────────────────────────────────────────────────────┐
│ Phase 3: 사용자 중심 테스트의 시대 │
│ │
│ 핵심 변화: "어떻게 구현되었나" → "사용자에게 어떻게 보이나" │
│ │
│ ┌────────────┬────────┬─────────────────────────────────┐ │
│ │ 도구 │ 연도 │ 특징 │ │
│ ├────────────┼────────┼─────────────────────────────────┤ │
│ │ React │ 2018 │ Kent C. Dodds가 제작 │ │
│ │ Testing │ │ 사용자 관점 테스트 철학 │ │
│ │ Library │ │ Enzyme 대체 │ │
│ ├────────────┼────────┼─────────────────────────────────┤ │
│ │ Cypress │ 2018 │ E2E 테스트 혁신 │ │
│ │ │ (GA) │ Time-travel debugging │ │
│ │ │ │ 실시간 리로드, 스크린샷 자동 │ │
│ ├────────────┼────────┼─────────────────────────────────┤ │
│ │ MSW │ 2019 │ Service Worker 기반 API 모킹 │ │
│ │ │ │ 네트워크 레벨에서 요청 가로채기 │ │
│ │ │ │ 테스트 + 개발 환경 모두 사용 │ │
│ └────────────┴────────┴─────────────────────────────────┘ │
│ │
│ 패러다임 전환 요약: │
│ ├── Before: wrapper.state('count') → "내부를 들여다보자" │
│ ├── After: screen.getByText('3') → "화면에 뭐가 보이지?" │
│ └── 결과: 리팩토링에 강건한 테스트, 더 높은 신뢰도 │
│ │
└─────────────────────────────────────────────────────────────────┘
5.4 E2E 르네상스 (2020-2023): Cypress와 Playwright
┌─────────────────────────────────────────────────────────────────┐
│ Phase 4: E2E 테스트의 부활 │
│ │
│ E2E 테스트가 "느리고 불안정하다"는 인식을 깨뜨린 도구들: │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Cypress (2018 GA) │ │
│ │ ├── 브라우저 안에서 직접 실행 (in-process) │ │
│ │ ├── Time-travel debugging (각 단계 스크린샷) │ │
│ │ ├── 자동 대기 (Auto-waiting) → flaky test 감소 │ │
│ │ ├── 실시간 리로드 │ │
│ │ ├── 한계: Chromium 계열만 (2020년부터 Firefox 지원) │ │
│ │ └── 한계: 다중 탭/다중 도메인 제한 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Playwright (2020, Microsoft) │ │
│ │ ├── Chromium + Firefox + WebKit 모두 지원 │ │
│ │ ├── 자동 대기 (Auto-waiting) 내장 │ │
│ │ ├── 다중 탭, 다중 브라우저 컨텍스트 지원 │ │
│ │ ├── 네트워크 가로채기 (Route) 내장 │ │
│ │ ├── Codegen: 브라우저 조작을 녹화 → 테스트 코드 생성 │ │
│ │ ├── Trace Viewer: 테스트 실행 과정 타임라인 시각화 │ │
│ │ └── 2024년 6월: npm 다운로드에서 Cypress 추월! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Cypress vs Playwright 비교: │
│ ┌──────────────────┬──────────────────┬──────────────────┐ │
│ │ 항목 │ Cypress │ Playwright │ │
│ ├──────────────────┼──────────────────┼──────────────────┤ │
│ │ 브라우저 지원 │ Chromium+Firefox │ Chrom+FF+WebKit │ │
│ │ 실행 방식 │ In-browser │ Out-of-process │ │
│ │ 언어 │ JavaScript │ JS/TS/Python/C# │ │
│ │ 다중 탭 │ 제한적 │ 완전 지원 │ │
│ │ 병렬 실행 │ 유료(Dashboard) │ 무료 내장 │ │
│ │ API 테스트 │ cy.request() │ request context │ │
│ │ 컴포넌트 테스트 │ 지원(실험적) │ 지원(실험적) │ │
│ │ 커뮤니티 크기 │ 크고 성숙 │ 빠르게 성장 중 │ │
│ │ 추세 (2025) │ 유지 │ 상승 중 │ │
│ └──────────────────┴──────────────────┴──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
5.5 현대 (2023-2026): Vitest, Storybook Play Functions
┌─────────────────────────────────────────────────────────────────┐
│ Phase 5: 현재와 미래 │
│ │
│ ┌────────────┬────────┬─────────────────────────────────┐ │
│ │ 도구 │ 연도 │ 특징 │ │
│ ├────────────┼────────┼─────────────────────────────────┤ │
│ │ Vitest │ 2022 │ Vite 기반 테스트 러너 │ │
│ │ │ │ Jest 호환 API │ │
│ │ │ │ ESM 네이티브, HMR 활용 │ │
│ │ │ │ 2025 State of JS: 98% retention │ │
│ ├────────────┼────────┼─────────────────────────────────┤ │
│ │ Storybook │ 2023 │ Play Functions으로 인터랙션 테스트│ │
│ │ 7.x+ │ │ 컴포넌트 단위 시각적 + 동작 검증 │ │
│ │ │ │ Chromatic 연동 Visual Regression │ │
│ ├────────────┼────────┼─────────────────────────────────┤ │
│ │ Testing │ 2023 │ Browser Mode 추가 │ │
│ │ Library │ │ 실제 브라우저에서 컴포넌트 테스트│ │
│ │ + Vitest │ │ jsdom 한계 극복 │ │
│ └────────────┴────────┴─────────────────────────────────┘ │
│ │
│ 전체 진화 타임라인: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 2008 2011 2014 2016 2018 2019 2020 2022 2025 │ │
│ │ │ │ │ │ │ │ │ │ │ │ │
│ │ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ │ │
│ │ QUnit Mocha Jest SB RTL MSW PW Vitest 현재 │ │
│ │ ──── ──── ──── ──── ──── ───── ─── ────── ──── │ │
│ │ jQuery React Testing E2E Vite │ │
│ │ 시대 시대 Library 르네 시대 │ │
│ │ 혁명 상스 │ │
│ │ │ │
│ │ SB=Storybook, RTL=React Testing Library, │ │
│ │ PW=Playwright, MSW=Mock Service Worker │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
6. 현대 테스트 스택 권장 (2025-2026)
6.1 Unit/Component Test: Vitest
┌─────────────────────────────────────────────────────────────────┐
│ Vitest - 차세대 테스트 러너 │
│ │
│ Vitest란? │
│ ├── Vite 빌드 도구 위에 구축된 테스트 프레임워크 │
│ ├── Evan You(Vue 제작자)의 Vite 생태계의 일부 │
│ ├── "Blazing fast unit test framework powered by Vite" │
│ └── 2025 State of JS: 98% retention rate (최고 만족도) │
│ │
│ Jest 대비 장점: │
│ ┌──────────────────┬──────────────────┬──────────────────┐ │
│ │ 항목 │ Jest │ Vitest │ │
│ ├──────────────────┼──────────────────┼──────────────────┤ │
│ │ Cold Start │ ~3초 │ ~1초 (66% 빠름) │ │
│ │ HMR 지원 │ 없음 │ 있음 (즉시 재실행)│ │
│ │ ESM 지원 │ 실험적 │ 네이티브 │ │
│ │ TypeScript │ ts-jest 필요 │ 내장 │ │
│ │ Vite 설정 공유 │ 별도 설정 │ vite.config 재사용│ │
│ │ In-source Test │ 불가 │ 가능 │ │
│ │ API 호환 │ - │ Jest 호환 │ │
│ │ UI 대시보드 │ 없음 │ 내장 (--ui) │ │
│ │ Browser Mode │ 없음 │ 있음 (실험적) │ │
│ └──────────────────┴──────────────────┴──────────────────┘ │
│ │
│ 코드 예시: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // vitest.config.ts │ │
│ │ import { defineConfig } from 'vitest/config' │ │
│ │ │ │
│ │ export default defineConfig({ │ │
│ │ test: { │ │
│ │ environment: 'jsdom', │ │
│ │ setupFiles: './src/test/setup.ts', │ │
│ │ css: true, │ │
│ │ coverage: { │ │
│ │ provider: 'v8', │ │
│ │ reporter: ['text', 'html', 'lcov'], │ │
│ │ }, │ │
│ │ }, │ │
│ │ }) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 언제 Jest를 계속 써야 하나? │
│ ├── CRA(Create React App) 기반 프로젝트 (레거시) │
│ ├── Jest 생태계에 깊이 의존하는 기존 프로젝트 │
│ └── 마이그레이션 비용이 이득보다 클 때 │
│ │
│ Vitest로 마이그레이션: │
│ ├── Jest API와 거의 동일 → 대부분 import만 변경 │
│ ├── jest.fn() → vi.fn() │
│ ├── jest.mock() → vi.mock() │
│ └── 나머지 describe/it/expect는 동일! │
│ │
└─────────────────────────────────────────────────────────────────┘
6.2 Integration Test: Testing Library + MSW v2
┌─────────────────────────────────────────────────────────────────┐
│ Integration Test 최적 조합 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Testing Library + MSW v2 │ │
│ │ (사용자 관점 (네트워크 레벨 │ │
│ │ 컴포넌트 테스트) API 모킹) │ │
│ │ │ │
│ │ ↓ ↓ │ │
│ │ "사용자처럼 클릭하고" "API 응답을 제어하며" │ │
│ │ │ │
│ │ ↓ │ │
│ │ "실제 동작과 거의 동일한 환경에서 테스트" │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 코드 예시 - 로그인 컴포넌트 통합 테스트: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // LoginForm.test.tsx │ │
│ │ import { render, screen } from '@testing-library/react'; │ │
│ │ import userEvent from '@testing-library/user-event'; │ │
│ │ import { http, HttpResponse } from 'msw'; │ │
│ │ import { server } from '../mocks/server'; │ │
│ │ import { LoginForm } from './LoginForm'; │ │
│ │ │ │
│ │ test('로그인 성공 시 환영 메시지 표시', async () => { │ │
│ │ // API 모킹 (MSW) │ │
│ │ server.use( │ │
│ │ http.post('/api/login', () => { │ │
│ │ return HttpResponse.json({ │ │
│ │ user: { name: '김철수' } │ │
│ │ }); │ │
│ │ }) │ │
│ │ ); │ │
│ │ │ │
│ │ // 렌더링 │ │
│ │ render(<LoginForm />); │ │
│ │ │ │
│ │ // 사용자처럼 상호작용 │ │
│ │ await userEvent.type( │ │
│ │ screen.getByLabelText('이메일'), │ │
│ │ 'kim@test.com' │ │
│ │ ); │ │
│ │ await userEvent.type( │ │
│ │ screen.getByLabelText('비밀번호'), │ │
│ │ 'password123' │ │
│ │ ); │ │
│ │ await userEvent.click( │ │
│ │ screen.getByRole('button', { name: '로그인' }) │ │
│ │ ); │ │
│ │ │ │
│ │ // 사용자가 보는 결과 검증 │ │
│ │ expect( │ │
│ │ await screen.findByText('환영합니다, 김철수님!') │ │
│ │ ).toBeInTheDocument(); │ │
│ │ }); │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 이 테스트가 검증하는 것: │
│ ├── 폼 렌더링 ✓ │
│ ├── 사용자 입력 ✓ │
│ ├── API 호출 ✓ │
│ ├── 응답 처리 ✓ │
│ ├── UI 업데이트 ✓ │
│ └── → 하나의 테스트로 전체 플로우를 검증! (Integration) │
│ │
└─────────────────────────────────────────────────────────────────┘
6.3 E2E Test: Playwright
┌─────────────────────────────────────────────────────────────────┐
│ Playwright - E2E 테스트의 현재와 미래 │
│ │
│ 왜 Playwright인가? (2025년 기준) │
│ ├── 2024년 6월: npm 주간 다운로드에서 Cypress 추월 │
│ ├── 크로스 브라우저: Chromium + Firefox + WebKit │
│ ├── 크로스 플랫폼: Windows + macOS + Linux │
│ ├── 병렬 실행 무료, 자동 대기 내장 │
│ └── Microsoft의 강력한 지원 │
│ │
│ 코드 예시: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // e2e/checkout.spec.ts │ │
│ │ import { test, expect } from '@playwright/test'; │ │
│ │ │ │
│ │ test('상품 주문 플로우', async ({ page }) => { │ │
│ │ // 상품 페이지로 이동 │ │
│ │ await page.goto('/products/1'); │ │
│ │ │ │
│ │ // 장바구니에 담기 │ │
│ │ await page.getByRole('button', │ │
│ │ { name: '장바구니에 담기' } │ │
│ │ ).click(); │ │
│ │ │ │
│ │ // 장바구니로 이동 │ │
│ │ await page.getByRole('link', │ │
│ │ { name: '장바구니' } │ │
│ │ ).click(); │ │
│ │ │ │
│ │ // 상품이 장바구니에 있는지 확인 │ │
│ │ await expect( │ │
│ │ page.getByText('주문 상품 1개') │ │
│ │ ).toBeVisible(); │ │
│ │ │ │
│ │ // 결제 진행 │ │
│ │ await page.getByRole('button', │ │
│ │ { name: '결제하기' } │ │
│ │ ).click(); │ │
│ │ │ │
│ │ // 주문 완료 확인 │ │
│ │ await expect( │ │
│ │ page.getByText('주문이 완료되었습니다') │ │
│ │ ).toBeVisible(); │ │
│ │ }); │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Playwright 핵심 기능: │
│ ├── Codegen: npx playwright codegen → 녹화로 코드 생성 │
│ ├── Trace Viewer: 실패 시 실행 과정 타임라인 시각화 │
│ ├── Visual Comparison: 스크린샷 비교 내장 │
│ ├── Network Mocking: page.route()로 API 응답 제어 │
│ └── Test Generator: VS Code 확장으로 테스트 생성 │
│ │
└─────────────────────────────────────────────────────────────────┘
6.4 Visual Regression: Storybook + Chromatic/Percy
┌─────────────────────────────────────────────────────────────────┐
│ Visual Regression Testing (시각적 회귀 테스트) │
│ │
│ "코드는 맞는데, 보이는 게 달라졌다!"를 자동으로 잡는 기술 │
│ │
│ 문제 상황: │
│ ├── CSS 한 줄 수정 → 다른 페이지 레이아웃 깨짐 │
│ ├── 컴포넌트 라이브러리 업데이트 → 버튼 색상 변경 │
│ ├── 폰트 로딩 실패 → 대체 폰트로 레이아웃 어긋남 │
│ └── 이런 문제는 기능 테스트로 잡을 수 없음! │
│ │
│ 해결 도구: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Storybook (컴포넌트 카탈로그) │ │
│ │ │ │ │
│ │ ├── 각 컴포넌트의 모든 상태를 Story로 정의 │ │
│ │ │ (기본, 호버, 에러, 로딩, 빈 상태 등) │ │
│ │ │ │ │
│ │ ├── Play Functions: 인터랙션 테스트 │ │
│ │ │ (클릭, 입력 등을 Story 안에서 실행) │ │
│ │ │ │ │
│ │ └── Chromatic 또는 Percy 연동 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 스크린샷 비교 │ │
│ │ ┌────────────┬────────────┐ │ │
│ │ │ 이전 버전 │ 현재 버전 │ │ │
│ │ │ (기준) │ (변경) │ │ │
│ │ └─────┬──────┴─────┬──────┘ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌────────────────────┐ │ │
│ │ │ 픽셀 단위 비교 │ │ │
│ │ │ 차이 발견 시 알림 │ │ │
│ │ └────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Chromatic vs Percy: │
│ ┌──────────────┬──────────────────┬──────────────────┐ │
│ │ │ Chromatic │ Percy │ │
│ ├──────────────┼──────────────────┼──────────────────┤ │
│ │ 만든 곳 │ Storybook 팀 │ BrowserStack │ │
│ │ Storybook │ 네이티브 연동 │ 플러그인 연동 │ │
│ │ 무료 티어 │ 5,000 스냅/월 │ 5,000 스냅/월 │ │
│ │ CI 연동 │ GitHub Actions │ 다양한 CI 지원 │ │
│ │ 리뷰 UI │ 매우 직관적 │ 직관적 │ │
│ └──────────────┴──────────────────┴──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
6.5 현대 테스트 스택 전체 구조
┌─────────────────────────────────────────────────────────────────┐
│ 2025-2026 권장 프론트엔드 테스트 스택 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──── Static Analysis ────────────────────────────┐ │ │
│ │ │ TypeScript (strict mode) │ │ │
│ │ │ ESLint + eslint-plugin-testing-library │ │ │
│ │ │ Prettier │ │ │
│ │ │ → 코드 실행 전 오류 잡기 (무료!) │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌──── Unit Test ──────────────────────────────────┐ │ │
│ │ │ Vitest │ │ │
│ │ │ → 순수 함수, 유틸리티, 복잡한 로직 │ │ │
│ │ │ → 빠르게 많이 실행 (~1ms/test) │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌──── Integration Test ★ ─────────────────────────┐ │ │
│ │ │ Vitest + React Testing Library + MSW v2 │ │ │
│ │ │ → 컴포넌트 동작을 사용자 관점으로 검증 │ │ │
│ │ │ → API 통신 포함한 전체 동작 확인 │ │ │
│ │ │ → 가장 높은 ROI! 여기에 집중 투자! │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌──── E2E Test ───────────────────────────────────┐ │ │
│ │ │ Playwright │ │ │
│ │ │ → 핵심 사용자 시나리오만 (로그인, 결제 등) │ │ │
│ │ │ → 크로스 브라우저 검증 │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌──── Visual Test ────────────────────────────────┐ │ │
│ │ │ Storybook + Chromatic │ │ │
│ │ │ → 컴포넌트 스타일 변경 자동 감지 │ │ │
│ │ │ → 디자인 시스템 일관성 보장 │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌──── Performance Test ───────────────────────────┐ │ │
│ │ │ Lighthouse CI │ │ │
│ │ │ → 성능 점수 회귀 방지 │ │ │
│ │ │ → Core Web Vitals 모니터링 │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 투자 비율 (Testing Trophy 기반): │
│ ├── Static Analysis: 설정 후 자동 (초기 투자만) │
│ ├── Unit Test: ~20% (복잡한 로직에만) │
│ ├── Integration Test: ~50% ★ (핵심!) │
│ ├── E2E Test: ~15% (핵심 시나리오만) │
│ └── Visual/Performance: ~15% (자동화 파이프라인) │
│ │
└─────────────────────────────────────────────────────────────────┘
7. 테스트 안티패턴 - 이렇게 하면 안 된다
7.1 Implementation Detail 테스트 (내부 구현 의존)
┌─────────────────────────────────────────────────────────────────┐
│ 안티패턴 1: 내부 구현 테스트 │
│ │
│ "코드가 어떻게 동작하는가"를 테스트하면 안 된다! │
│ "코드가 무엇을 하는가"를 테스트해야 한다! │
│ │
│ 나쁜 예: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // 내부 구현에 의존 → 리팩토링하면 깨짐 │ │
│ │ test('카운터 증가', () => { │ │
│ │ const { result } = renderHook(() => useCounter()); │ │
│ │ act(() => result.current.increment()); │ │
│ │ │ │
│ │ // 내부 state를 직접 검사 │ │
│ │ expect(result.current.count).toBe(1); │ │
│ │ │ │
│ │ // 함수 호출 여부를 검사 │ │
│ │ expect(mockSetState).toHaveBeenCalledWith(1); │ │
│ │ }); │ │
│ │ │ │
│ │ 문제: useCounter를 useReducer로 리팩토링하면 깨짐! │ │
│ │ 사용자 경험은 동일한데 테스트가 실패함 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 좋은 예: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // 사용자 관점 → 리팩토링해도 안 깨짐 │ │
│ │ test('증가 버튼을 누르면 숫자가 1 증가한다', async () => {│ │
│ │ render(<Counter />); │ │
│ │ │ │
│ │ await userEvent.click( │ │
│ │ screen.getByRole('button', { name: '증가' }) │ │
│ │ ); │ │
│ │ │ │
│ │ expect(screen.getByText('1')).toBeInTheDocument(); │ │
│ │ }); │ │
│ │ │ │
│ │ → useState든 useReducer든 Zustand든 상관없이 통과! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Kent C. Dodds의 정리: │
│ ├── false negative: 코드는 맞는데 테스트가 실패 → 최악! │
│ ├── false positive: 코드는 틀린데 테스트가 통과 → 나쁨 │
│ └── 내부 구현 테스트 = false negative의 주범 │
│ │
└─────────────────────────────────────────────────────────────────┘
7.2 Snapshot 남용
┌─────────────────────────────────────────────────────────────────┐
│ 안티패턴 2: Snapshot Testing 남용 │
│ │
│ Snapshot Test란? │
│ ├── 컴포넌트 렌더링 결과를 파일로 저장 │
│ ├── 다음 실행 시 저장된 결과와 비교 │
│ └── 달라지면 테스트 실패 │
│ │
│ 왜 남용하게 되나? │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // 아, 테스트 작성 귀찮다. 스냅샷이나 찍자. │ │
│ │ test('renders correctly', () => { │ │
│ │ const { container } = render(<UserProfile />); │ │
│ │ expect(container).toMatchSnapshot(); │ │
│ │ // 끝! 쉽다! │ │
│ │ }); │ │
│ │ │ │
│ │ 생성된 스냅샷: 500줄짜리 HTML 덩어리 │ │
│ │ → 코드 리뷰 시: "뭐가 바뀐 건지 모르겠는데 Update 하자" │ │
│ │ → 결국 아무도 확인하지 않는 스냅샷 = 가치 없는 테스트 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 문제점: │
│ ├── 1. "Update Snapshot" 습관 → 진짜 변경도 무시 │
│ ├── 2. 무엇을 테스트하는지 불명확 (모든 것을 테스트 = 아무것도)│
│ ├── 3. 코드 리뷰에서 500줄 diff → 리뷰어 포기 │
│ ├── 4. 리팩토링할 때마다 대량의 스냅샷 업데이트 필요 │
│ └── 5. false positive 양산 (버그인데 스냅샷 업데이트로 통과) │
│ │
│ 올바른 사용법: │
│ ├── 작은 단위에만 사용 (디자인 토큰, 설정 객체 등) │
│ ├── toMatchInlineSnapshot() 사용 → 리뷰가 쉬움 │
│ └── 컴포넌트 전체 HTML에는 사용하지 말 것 │
│ │
│ 대안: │
│ ├── 구체적인 assertion: expect(screen.getByText('제목')) │
│ ├── Visual Regression: Chromatic/Percy (시각적 비교) │
│ └── Play Functions: Storybook에서 인터랙션 테스트 │
│ │
└─────────────────────────────────────────────────────────────────┘
7.3 Over-mocking (과도한 모킹)
┌─────────────────────────────────────────────────────────────────┐
│ 안티패턴 3: 모든 것을 Mock하기 │
│ │
│ "모킹(Mocking)이란?" │
│ = 실제 모듈/함수를 가짜(Mock)로 대체하는 것 │
│ = 비유: 영화 촬영에서 스턴트 배우를 쓰는 것 │
│ │
│ 과도한 모킹의 문제: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // 모든 것을 mock한 테스트 │ │
│ │ vi.mock('./api'); │ │
│ │ vi.mock('./utils'); │ │
│ │ vi.mock('./hooks/useAuth'); │ │
│ │ vi.mock('./store'); │ │
│ │ vi.mock('react-router-dom'); │ │
│ │ │ │
│ │ test('renders', () => { │ │
│ │ render(<App />); │ │
│ │ // ...뭘 테스트하는 거지? │ │
│ │ // 모든 걸 가짜로 대체했으니 │ │
│ │ // 실제 동작과 무관한 테스트가 됨 │ │
│ │ }); │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 비유: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 자동차 테스트를 한다면서... │ │
│ │ ├── 엔진 → 가짜 엔진으로 교체 │ │
│ │ ├── 바퀴 → 가짜 바퀴로 교체 │ │
│ │ ├── 핸들 → 가짜 핸들로 교체 │ │
│ │ ├── 브레이크 → 가짜 브레이크로 교체 │ │
│ │ └── "자동차 테스트 통과!" → 진짜 자동차는 안 달려... │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 올바른 모킹 전략: │
│ ├── 네트워크 요청 → MSW로 모킹 (네트워크 레벨) │
│ │ → fetch/axios를 mock하지 말고 MSW를 사용하라! │
│ ├── 시간/날짜 → vi.useFakeTimers() │
│ ├── 외부 서비스(결제, 이메일) → 경계에서만 모킹 │
│ └── 나머지는 실제 코드를 사용하라! │
│ │
│ 원칙: "Mock하는 것이 적을수록, 테스트의 신뢰도가 높아진다" │
│ │
└─────────────────────────────────────────────────────────────────┘
7.4 100% 커버리지 집착
┌─────────────────────────────────────────────────────────────────┐
│ 안티패턴 4: 100% 코드 커버리지 집착 │
│ │
│ 코드 커버리지(Code Coverage)란? │
│ = 테스트가 실행한 코드의 비율 │
│ = Line Coverage, Branch Coverage, Function Coverage 등 │
│ │
│ 왜 100%가 함정인가? │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 커버리지 100% = "모든 줄을 실행했다" │ │
│ │ ≠ "모든 경우를 검증했다" │ │
│ │ ≠ "버그가 없다" │ │
│ │ │ │
│ │ 예시: │ │
│ │ function divide(a, b) { return a / b; } │ │
│ │ │ │
│ │ test: divide(10, 2) → 5 ✓ │ │
│ │ → 커버리지 100%! 하지만 divide(10, 0)은? │ │
│ │ → Infinity를 반환하는 버그를 못 잡음! │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 전문가 의견: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Kent Beck (TDD 창시자, XP 창시자): │ │
│ │ "I get paid for code that works, not for tests, │ │
│ │ so my philosophy is to test as little as possible │ │
│ │ to reach a given level of confidence." │ │
│ │ "나는 테스트가 아니라 동작하는 코드로 돈을 받는다. │ │
│ │ 내 철학은 원하는 수준의 자신감을 얻을 만큼만 │ │
│ │ 최소한으로 테스트하는 것이다." │ │
│ │ │ │
│ │ Dan Abramov (React Core Team): │ │
│ │ "Bad tests are worse than no tests." │ │
│ │ "나쁜 테스트는 테스트가 없는 것보다 나쁘다." │ │
│ │ → 나쁜 테스트는 거짓된 안전감을 주고, │ │
│ │ 유지보수 비용만 증가시킨다. │ │
│ │ │ │
│ │ Martin Fowler: │ │
│ │ "Test coverage is a useful tool for finding untested │ │
│ │ parts of a codebase. It is of little use as a │ │
│ │ numeric statement of how good your tests are." │ │
│ │ "커버리지는 테스트되지 않은 부분을 찾는 데 유용하지만, │ │
│ │ 테스트 품질의 수치적 지표로는 거의 쓸모없다." │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 권장 전략: │
│ ├── 목표 커버리지: 70-80% (맹목적 100% 금지) │
│ ├── 커버리지 = "어디를 테스트 안 했지?" 발견 도구 │
│ ├── 커버리지 ≠ "테스트 품질" 지표 │
│ ├── 핵심 비즈니스 로직: 높은 커버리지 + Mutation Testing │
│ ├── UI 레이아웃 코드: 낮은 커버리지 + Visual Regression │
│ └── 글루 코드(라우팅, 설정): E2E로 커버 │
│ │
└─────────────────────────────────────────────────────────────────┘
7.5 기타 안티패턴 모음
┌─────────────────────────────────────────────────────────────────┐
│ 추가 안티패턴들 │
│ │
│ 5. Flaky Test (불안정한 테스트) │
│ ├── 같은 코드인데 실행할 때마다 결과가 다름 │
│ ├── 원인: 타이밍 의존, 테스트 간 상태 공유, 네트워크 의존 │
│ ├── 해결: waitFor() 사용, 테스트 격리, MSW로 API 모킹 │
│ └── Google 연구: 전체 테스트 실패의 16%가 Flaky Test! │
│ │
│ 6. 테스트 이름이 불명확 │
│ ├── 나쁜: test('test1'), test('renders correctly') │
│ ├── 좋은: test('장바구니가 비어있을 때 빈 상태 메시지를 보여준다')│
│ └── 원칙: "테스트 이름만 읽어도 뭘 검증하는지 알 수 있어야 함" │
│ │
│ 7. 하나의 테스트에 너무 많은 검증 │
│ ├── 나쁜: 로그인 + 상품 검색 + 장바구니 + 결제 → 하나의 test │
│ ├── 좋은: 각 시나리오를 독립적인 test로 분리 │
│ └── 이유: 실패 시 어디서 실패했는지 즉시 파악 가능 │
│ │
│ 8. 테스트 없이 리팩토링 │
│ ├── 테스트가 있으면 → "안전한 리팩토링" 가능 │
│ ├── 테스트가 없으면 → "위험한 도박" │
│ └── Michael Feathers: "Legacy code is code without tests" │
│ │
└─────────────────────────────────────────────────────────────────┘
8. Google의 테스트 분류 체계
8.1 Small / Medium / Large 테스트
┌─────────────────────────────────────────────────────────────────┐
│ Google의 테스트 크기 분류 │
│ │
│ Google은 Unit/Integration/E2E 대신 │
│ Small/Medium/Large라는 자체 분류 체계를 사용한다. │
│ (출처: "Software Engineering at Google", O'Reilly 2020) │
│ │
│ ┌────────────┬──────────────┬──────────────┬──────────────┐ │
│ │ 속성 │ Small │ Medium │ Large │ │
│ ├────────────┼──────────────┼──────────────┼──────────────┤ │
│ │ 프로세스 │ 단일 프로세스│ 단일 머신 │ 제한 없음 │ │
│ │ │ │ 다중 프로세스│ │ │
│ ├────────────┼──────────────┼──────────────┼──────────────┤ │
│ │ 네트워크 │ localhost만 │ localhost만 │ 외부 허용 │ │
│ ├────────────┼──────────────┼──────────────┼──────────────┤ │
│ │ DB 접근 │ 불가 │ 가능 │ 가능 │ │
│ ├────────────┼──────────────┼──────────────┼──────────────┤ │
│ │ 파일시스템 │ 불가 │ 가능 │ 가능 │ │
│ ├────────────┼──────────────┼──────────────┼──────────────┤ │
│ │ Sleep/Wait │ 불가 │ 불가 │ 가능 │ │
│ ├────────────┼──────────────┼──────────────┼──────────────┤ │
│ │ 실행 시간 │ ~1초 이내 │ ~5초 이내 │ ~15분 이내 │ │
│ ├────────────┼──────────────┼──────────────┼──────────────┤ │
│ │ 비율 │ ~80% │ ~15% │ ~5% │ │
│ └────────────┴──────────────┴──────────────┴──────────────┘ │
│ │
│ 프론트엔드에 적용하면: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Small (80%): │ │
│ │ ├── Vitest로 실행하는 순수 함수 테스트 │ │
│ │ ├── 컴포넌트 렌더링 테스트 (jsdom) │ │
│ │ ├── 커스텀 훅 테스트 │ │
│ │ └── 외부 의존 없음, 1초 이내 완료 │ │
│ │ │ │
│ │ Medium (15%): │ │
│ │ ├── MSW를 활용한 API 통합 테스트 │ │
│ │ ├── 라우팅이 포함된 컴포넌트 테스트 │ │
│ │ ├── localStorage/sessionStorage 사용 테스트 │ │
│ │ └── 단일 머신 내에서 완료, 5초 이내 │ │
│ │ │ │
│ │ Large (5%): │ │
│ │ ├── Playwright E2E 테스트 │ │
│ │ ├── 실제 API 서버 연동 테스트 │ │
│ │ ├── 크로스 브라우저 테스트 │ │
│ │ └── 외부 서비스 접근, 분 단위 소요 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Google의 핵심 원칙: │
│ ├── "크기"로 분류하면 도구(Unit/Integration)보다 명확 │
│ ├── 같은 "Unit Test"도 DB 접근하면 → Medium으로 분류 │
│ ├── CI에서 Small은 모든 커밋, Large는 매일 1회만 실행 │
│ └── 테스트 크기가 커질수록 더 엄격하게 관리 │
│ │
└─────────────────────────────────────────────────────────────────┘
8.2 Google의 “Testing on the Toilet” 문화
┌─────────────────────────────────────────────────────────────────┐
│ Testing on the Toilet (TotT) │
│ │
│ Google 내부 문화: │
│ ├── 화장실 칸막이에 테스트 관련 팁을 붙여놓음 │
│ ├── 매주 새로운 내용으로 교체 │
│ ├── "화장실에서도 테스트를 생각하라!" │
│ └── 이 중 일부가 외부에 공개됨 (Google Testing Blog) │
│ │
│ TotT에서 나온 유명한 원칙들: │
│ ├── "Don't Put Logic in Tests" │
│ │ → 테스트에 if/for/복잡한 로직을 넣지 마라 │
│ ├── "Test Behaviors, Not Methods" │
│ │ → 메서드가 아니라 동작(behavior)을 테스트하라 │
│ ├── "Keep Tests Focused" │
│ │ → 하나의 테스트 = 하나의 동작 검증 │
│ └── "Make Tests Complete and Concise" │
│ → 테스트만 읽어도 전체 맥락을 알 수 있게 │
│ │
│ 프론트엔드 개발자에게 주는 교훈: │
│ ├── 테스트 = 살아있는 문서 │
│ ├── 새로운 팀원이 테스트만 읽어도 기능을 이해할 수 있어야 함 │
│ └── 테스트 이름 = 요구사항 명세서 │
│ │
└─────────────────────────────────────────────────────────────────┘
9. Spotify의 Testing Honeycomb
9.1 허니콤 모델이 탄생한 배경
┌─────────────────────────────────────────────────────────────────┐
│ Spotify의 Testing Honeycomb (테스트 벌집) │
│ │
│ 배경: │
│ ├── Spotify는 800+ 마이크로서비스로 운영 │
│ ├── 마이크로서비스에서 Unit Test의 가치가 낮다고 판단 │
│ ├── 서비스 간 통신(Integration)이 핵심 관심사 │
│ └── 2018년 Spotify Engineering Blog에서 발표 │
│ │
│ Honeycomb 구조: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ╱──────╲ │ │
│ │ ╱ E2E ╲ │ │
│ │ ╱──────────╲ │ │
│ │ ╱ ╲ │ │
│ │ ╱ ╲ │ │
│ │ ╱ Integration ╲ │ │
│ │ ╱ Tests (핵심!) ╲ │ │
│ │ ╱ ╲ │ │
│ │ ╲ 가장 넓음! ╱ │ │
│ │ ╲ ╱ │ │
│ │ ╲ ╱ │ │
│ │ ╲ Implementation ╱ │ │
│ │ ╲ Details ╱ │ │
│ │ ╲ (Unit 대신) ╱ │ │
│ │ ╲──────────╱ │ │
│ │ ╲──────╱ │ │
│ │ │ │
│ │ 피라미드와의 차이: Unit 대신 "Implementation Details" │ │
│ │ = Unit Test를 최소화하고 Integration에 집중 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 핵심 아이디어: │
│ ├── Unit Test는 "구현 세부사항"을 테스트하는 경우가 많음 │
│ ├── 마이크로서비스에서 중요한 것 = "서비스 간 계약(Contract)" │
│ ├── Integration Test가 계약을 검증하는 최적의 방법 │
│ └── E2E는 핵심 사용자 여정(User Journey)만 │
│ │
│ 프론트엔드에 주는 시사점: │
│ ├── 컴포넌트 내부 로직(Unit)보다 │
│ │ 컴포넌트 간 상호작용(Integration)이 더 중요 │
│ ├── Testing Trophy와 동일한 결론: │
│ │ "Integration Test에 가장 많이 투자하라" │
│ └── API 계약 테스트 → MSW로 API 응답 규격 검증 │
│ │
└─────────────────────────────────────────────────────────────────┘
9.2 세 가지 모델 비교 요약
┌─────────────────────────────────────────────────────────────────┐
│ 테스트 모델 3가지 비교 │
│ │
│ ┌──────────────┬────────────────┬─────────────┬────────────┐ │
│ │ │ Test Pyramid │ Testing │ Honeycomb │ │
│ │ │ (Mike Cohn) │ Trophy │ (Spotify) │ │
│ │ │ │ (Kent Dodds)│ │ │
│ ├──────────────┼────────────────┼─────────────┼────────────┤ │
│ │ 연도 │ 2009 │ 2018 │ 2018 │ │
│ ├──────────────┼────────────────┼─────────────┼────────────┤ │
│ │ 대상 │ 모노리스 │ 프론트엔드 │ 마이크로 │ │
│ │ │ 백엔드 │ (React 등) │ 서비스 │ │
│ ├──────────────┼────────────────┼─────────────┼────────────┤ │
│ │ 핵심 계층 │ Unit │ Integration │ Integration│ │
│ ├──────────────┼────────────────┼─────────────┼────────────┤ │
│ │ Static │ 없음 │ 기반에 포함 │ 없음 │ │
│ ├──────────────┼────────────────┼─────────────┼────────────┤ │
│ │ 핵심 가치 │ 속도 + 비용 │ 신뢰도+ROI │ 계약 검증 │ │
│ ├──────────────┼────────────────┼─────────────┼────────────┤ │
│ │ 모양 │ 피라미드 △ │ 트로피 🏆 │ 벌집 ⬡ │ │
│ └──────────────┴────────────────┴─────────────┴────────────┘ │
│ │
│ 공통된 결론: │
│ ├── E2E는 최소한으로 유지 │
│ ├── Integration이 가장 높은 가성비 │
│ └── 현대 아키텍처에서 Unit만으로는 부족 │
│ │
└─────────────────────────────────────────────────────────────────┘
10. MSW (Mock Service Worker) - 네트워크 레벨 API 모킹
10.1 MSW란?
┌─────────────────────────────────────────────────────────────────┐
│ MSW (Mock Service Worker) 이해하기 │
│ │
│ MSW란? │
│ ├── Service Worker를 활용한 네트워크 레벨 API 모킹 도구 │
│ ├── Artem Zakharchenko가 2019년에 제작 │
│ ├── fetch()나 axios를 mock하지 않음! │
│ └── 네트워크 레벨에서 요청을 가로채서 가짜 응답을 반환 │
│ │
│ 기존 모킹 vs MSW: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 기존 방식 (jest.mock, vi.mock): │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Component│──→│ fetch() │──→│ Server │ │ │
│ │ │ │ │ (가짜!) │ │ (도달X) │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ → fetch 자체를 가짜로 교체 │ │
│ │ → 실제 네트워크 동작과 다를 수 있음 │ │
│ │ │ │
│ │ MSW 방식: │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Component│──→│ fetch() │──→│ MSW │ │ │
│ │ │ │ │ (진짜!) │ │ (가로챔) │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ → fetch는 진짜 fetch 그대로 │ │
│ │ → 네트워크 레벨에서 요청을 가로채서 응답 │ │
│ │ → 실제 동작과 거의 동일! │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 비유: │
│ ├── jest.mock = 가짜 전화기 (다이얼도 안 돌아감) │
│ └── MSW = 진짜 전화기 + 가짜 교환원 (전화는 진짜로 걸리지만 │
│ 교환원이 미리 준비된 대본으로 응답) │
│ │
└─────────────────────────────────────────────────────────────────┘
10.2 MSW v2 사용법
┌─────────────────────────────────────────────────────────────────┐
│ MSW v2 핵심 사용법 │
│ │
│ 1. Handler 정의 (가로챌 API 정의): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // src/mocks/handlers.ts │ │
│ │ import { http, HttpResponse } from 'msw'; │ │
│ │ │ │
│ │ export const handlers = [ │ │
│ │ // GET /api/users │ │
│ │ http.get('/api/users', () => { │ │
│ │ return HttpResponse.json([ │ │
│ │ { id: 1, name: '김철수', email: 'kim@test.com' }, │ │
│ │ { id: 2, name: '이영희', email: 'lee@test.com' }, │ │
│ │ ]); │ │
│ │ }), │ │
│ │ │ │
│ │ // POST /api/users │ │
│ │ http.post('/api/users', async ({ request }) => { │ │
│ │ const body = await request.json(); │ │
│ │ return HttpResponse.json( │ │
│ │ { id: 3, ...body }, │ │
│ │ { status: 201 } │ │
│ │ ); │ │
│ │ }), │ │
│ │ │ │
│ │ // 에러 시나리오 │ │
│ │ http.get('/api/users/:id', ({ params }) => { │ │
│ │ if (params.id === '999') { │ │
│ │ return HttpResponse.json( │ │
│ │ { message: '사용자를 찾을 수 없습니다' }, │ │
│ │ { status: 404 } │ │
│ │ ); │ │
│ │ } │ │
│ │ return HttpResponse.json( │ │
│ │ { id: params.id, name: '김철수' } │ │
│ │ ); │ │
│ │ }), │ │
│ │ ]; │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 2. 테스트 서버 설정: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // src/mocks/server.ts │ │
│ │ import { setupServer } from 'msw/node'; │ │
│ │ import { handlers } from './handlers'; │ │
│ │ │ │
│ │ export const server = setupServer(...handlers); │ │
│ │ │ │
│ │ // src/test/setup.ts │ │
│ │ import { server } from '../mocks/server'; │ │
│ │ │ │
│ │ beforeAll(() => server.listen()); │ │
│ │ afterEach(() => server.resetHandlers()); │ │
│ │ afterAll(() => server.close()); │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 3. 개발 환경에서도 동일한 handler 재사용: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // src/mocks/browser.ts (개발 환경용) │ │
│ │ import { setupWorker } from 'msw/browser'; │ │
│ │ import { handlers } from './handlers'; │ │
│ │ │ │
│ │ export const worker = setupWorker(...handlers); │ │
│ │ │ │
│ │ // main.tsx에서 │ │
│ │ if (process.env.NODE_ENV === 'development') { │ │
│ │ const { worker } = await import('./mocks/browser'); │ │
│ │ await worker.start(); │ │
│ │ } │ │
│ │ │ │
│ │ → 백엔드 없이 프론트엔드 개발 가능! │ │
│ │ → 테스트와 개발 환경이 동일한 mock 데이터 사용 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ MSW v2 vs v1 변경점: │
│ ├── rest.get() → http.get() (네이밍 변경) │
│ ├── res(ctx.json()) → HttpResponse.json() (간결해짐) │
│ ├── GraphQL handler도 동일한 패턴 │
│ └── ESM 네이티브 지원 │
│ │
└─────────────────────────────────────────────────────────────────┘
10.3 MSW로 GraphQL 모킹
┌─────────────────────────────────────────────────────────────────┐
│ MSW GraphQL 지원 │
│ │
│ REST뿐 아니라 GraphQL도 동일한 방식으로 모킹 가능: │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ import { graphql, HttpResponse } from 'msw'; │ │
│ │ │ │
│ │ export const graphqlHandlers = [ │ │
│ │ // Query │ │
│ │ graphql.query('GetUsers', () => { │ │
│ │ return HttpResponse.json({ │ │
│ │ data: { │ │
│ │ users: [ │ │
│ │ { id: '1', name: '김철수' }, │ │
│ │ { id: '2', name: '이영희' }, │ │
│ │ ], │ │
│ │ }, │ │
│ │ }); │ │
│ │ }), │ │
│ │ │ │
│ │ // Mutation │ │
│ │ graphql.mutation('CreateUser', ({ variables }) => { │ │
│ │ return HttpResponse.json({ │ │
│ │ data: { │ │
│ │ createUser: { │ │
│ │ id: '3', │ │
│ │ name: variables.name, │ │
│ │ }, │ │
│ │ }, │ │
│ │ }); │ │
│ │ }), │ │
│ │ ]; │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ MSW의 가치 요약: │
│ ├── 1. 테스트와 개발 환경에서 동일한 mock 재사용 │
│ ├── 2. 네트워크 레벨 모킹 → 실제 동작에 가까움 │
│ ├── 3. REST + GraphQL 모두 지원 │
│ ├── 4. fetch/axios를 직접 mock하는 것보다 안전 │
│ └── 5. 브라우저(Service Worker) + Node.js 양쪽 지원 │
│ │
└─────────────────────────────────────────────────────────────────┘
11. 뮤테이션 테스트와 테스트 품질 측정
11.1 코드 커버리지의 한계
┌─────────────────────────────────────────────────────────────────┐
│ 코드 커버리지가 말해주지 않는 것 │
│ │
│ 커버리지 100%인데 버그가 있는 경우: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // 실제 코드 │ │
│ │ function isAdult(age: number): boolean { │ │
│ │ return age >= 18; │ │
│ │ } │ │
│ │ │ │
│ │ // 테스트 (커버리지 100%) │ │
│ │ test('성인 판별', () => { │ │
│ │ expect(isAdult(20)).toBe(true); │ │
│ │ }); │ │
│ │ │ │
│ │ → 커버리지 100%! 하지만... │ │
│ │ → isAdult(17) → false인지 검증 안 함 │ │
│ │ → isAdult(18) → 경계값 검증 안 함 │ │
│ │ → 누군가 >= 를 > 로 바꿔도 테스트 통과! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 커버리지의 진짜 가치: │
│ ├── "어디를 테스트하지 않았는가" → 유용 │
│ ├── "테스트가 얼마나 좋은가" → 알 수 없음 │
│ └── 높은 커버리지 ≠ 높은 테스트 품질 │
│ │
│ → 그래서 "뮤테이션 테스트"가 필요하다 │
│ │
└─────────────────────────────────────────────────────────────────┘
11.2 뮤테이션 테스트 원리
┌─────────────────────────────────────────────────────────────────┐
│ 뮤테이션 테스트 (Mutation Testing) │
│ │
│ 아이디어: "코드를 일부러 망가뜨려서 테스트가 잡아내는지 확인" │
│ │
│ 동작 원리: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 원본 코드: return age >= 18; │ │
│ │ │ │ │
│ │ ▼ 뮤턴트(Mutant) 생성 │ │
│ │ ┌────────────────────────────────────────┐ │ │
│ │ │ 뮤턴트 1: return age > 18; (>= → >)│ │ │
│ │ │ 뮤턴트 2: return age <= 18; (>= → <=)│ │ │
│ │ │ 뮤턴트 3: return age >= 19; (18 → 19)│ │ │
│ │ │ 뮤턴트 4: return true; (조건 제거)│ │ │
│ │ └────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ 각 뮤턴트에 대해 테스트 실행 │ │
│ │ │ │
│ │ 뮤턴트 1 (age > 18): │ │
│ │ ├── isAdult(20) → true (원본과 동일) → 테스트 통과 ✓ │ │
│ │ └── 테스트가 뮤턴트를 못 잡음! → 뮤턴트 "생존" 😱 │ │
│ │ │ │
│ │ 뮤턴트 4 (return true): │ │
│ │ ├── isAdult(20) → true (원본과 동일) → 테스트 통과 ✓ │ │
│ │ └── 테스트가 뮤턴트를 못 잡음! → 뮤턴트 "생존" 😱 │ │
│ │ │ │
│ │ 결론: 4개 뮤턴트 중 2개 이상 생존 │ │
│ │ → 테스트가 약하다! 더 강화해야 함! │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 핵심 지표: Mutation Score │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Mutation Score = 죽은 뮤턴트 / 전체 뮤턴트 × 100% │ │
│ │ │ │
│ │ 예: 100개 뮤턴트 중 80개 죽음 → Mutation Score = 80% │ │
│ │ → 20개의 뮤턴트가 살아남음 = 20곳의 테스트 약점! │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11.3 Stryker Mutator - JavaScript/TypeScript용 뮤테이션 테스트 도구
┌─────────────────────────────────────────────────────────────────┐
│ Stryker Mutator │
│ │
│ JavaScript/TypeScript 생태계에서 가장 성숙한 뮤테이션 도구: │
│ │
│ 설정: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // stryker.config.mjs │ │
│ │ export default { │ │
│ │ mutate: ['src/**/*.ts', '!src/**/*.test.ts'], │ │
│ │ testRunner: 'vitest', │ │
│ │ reporters: ['html', 'clear-text', 'progress'], │ │
│ │ coverageAnalysis: 'perTest', │ │
│ │ }; │ │
│ │ │ │
│ │ // 실행 │ │
│ │ npx stryker run │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Stryker 뮤테이션 종류: │
│ ├── 산술: + → -, * → /, % → * 등 │
│ ├── 비교: > → >=, === → !==, < → <= 등 │
│ ├── 논리: && → ||, ! → (제거) 등 │
│ ├── 문자열: "hello" → "" 등 │
│ ├── 배열: push → (제거), length → 0 등 │
│ └── 제어흐름: if(cond) → if(true), if(false) 등 │
│ │
│ 주의사항: │
│ ├── 실행 시간이 매우 김 (뮤턴트 수 × 테스트 실행 시간) │
│ ├── CI에서는 incremental mode 사용 권장 │
│ ├── 핵심 비즈니스 로직에만 적용하면 효과적 │
│ └── 모든 코드에 적용하면 시간 대비 효용이 낮음 │
│ │
└─────────────────────────────────────────────────────────────────┘
12. AI 시대의 테스트 (2024-2026)
12.1 AI 테스트 생성의 현재
┌─────────────────────────────────────────────────────────────────┐
│ AI가 바꾸고 있는 테스트 작성 방식 │
│ │
│ AI 도구별 테스트 생성 능력: │
│ ┌────────────────┬──────────────────────────────────────┐ │
│ │ 도구 │ 테스트 관련 능력 │ │
│ ├────────────────┼──────────────────────────────────────┤ │
│ │ GitHub Copilot │ 인라인 테스트 코드 자동 완성 │ │
│ │ │ 기존 패턴 기반 테스트 생성 │ │
│ ├────────────────┼──────────────────────────────────────┤ │
│ │ Claude │ 전체 테스트 파일 생성 │ │
│ │ (Anthropic) │ 복잡한 시나리오 테스트 설계 │ │
│ │ │ 테스트 전략 자문 │ │
│ ├────────────────┼──────────────────────────────────────┤ │
│ │ Cursor │ 코드 변경 시 관련 테스트 자동 제안 │ │
│ │ │ 테스트 실패 원인 분석 │ │
│ ├────────────────┼──────────────────────────────────────┤ │
│ │ Codium AI │ 테스트 전문 AI │ │
│ │ (Qodo) │ 경계값, 엣지 케이스 자동 생성 │ │
│ └────────────────┴──────────────────────────────────────┘ │
│ │
│ AI가 잘하는 것: │
│ ├── 보일러플레이트 코드 생성 (setup, teardown) │
│ ├── 행복 경로(Happy Path) 테스트 빠르게 작성 │
│ ├── 기존 테스트 패턴을 따라 유사한 테스트 생성 │
│ ├── 경계값(Boundary Value) 테스트 케이스 제안 │
│ └── 테스트 코드 리팩토링 (중복 제거, 구조 개선) │
│ │
│ AI가 못하는 것 (아직): │
│ ├── 전체 테스트 전략 수립 (무엇을 테스트할지 결정) │
│ ├── 비즈니스 컨텍스트 이해 (도메인 지식 기반 시나리오) │
│ ├── 유의미한 Integration Test 설계 │
│ ├── 테스트 우선순위 결정 (무엇이 더 중요한가) │
│ └── 실제 사용자 행동 패턴 모방 │
│ │
└─────────────────────────────────────────────────────────────────┘
12.2 AI 할루시네이션 주의
┌─────────────────────────────────────────────────────────────────┐
│ AI 생성 테스트의 주의사항 │
│ │
│ 할루시네이션(Hallucination)이란? │
│ = AI가 그럴듯하지만 틀린 코드를 생성하는 현상 │
│ │
│ 테스트에서 특히 위험한 이유: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 존재하지 않는 API 사용 │ │
│ │ AI: screen.getByClassName('btn') │ │
│ │ 실제: Testing Library에 getByClassName은 없음! │ │
│ │ │ │
│ │ 2. 잘못된 assertion │ │
│ │ AI: expect(result).toHaveBeenCalledWith(...) │ │
│ │ 실제: result는 함수가 아닌데 spy matcher 사용 │ │
│ │ │ │
│ │ 3. 테스트가 통과하지만 아무것도 검증 안 함 │ │
│ │ AI: test('works', () => { │ │
│ │ render(<App />); │ │
│ │ // assertion 없이 끝! │ │
│ │ }); │ │
│ │ → 통과하지만 가치 없는 테스트 │ │
│ │ │ │
│ │ 4. 구식 API 사용 │ │
│ │ AI: import { rest } from 'msw'; // MSW v1 문법! │ │
│ │ 실제: MSW v2에서는 http, HttpResponse 사용 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ AI 테스트 코드 검증 체크리스트: │
│ ├── [ ] 사용된 API가 실제로 존재하는가? │
│ ├── [ ] import 경로가 올바른가? │
│ ├── [ ] assertion이 실제로 무언가를 검증하는가? │
│ ├── [ ] 라이브러리 버전이 현재 프로젝트와 맞는가? │
│ ├── [ ] 테스트를 일부러 실패시켜봤을 때 정말 실패하는가? │
│ └── [ ] 비즈니스 요구사항을 정확히 반영하는가? │
│ │
└─────────────────────────────────────────────────────────────────┘
12.3 개발자의 역할: 전략 설계 + AI 결과 검증
┌─────────────────────────────────────────────────────────────────┐
│ AI 시대, 개발자의 테스트 역할 변화 │
│ │
│ Before AI (2020 이전): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 개발자가 하는 일: │ │
│ │ ├── 테스트 전략 수립 (전략) │ │
│ │ ├── 테스트 시나리오 설계 (설계) │ │
│ │ ├── 테스트 코드 작성 (코딩) ← 80% │ │
│ │ ├── 테스트 실행 및 디버깅 (실행) │ │
│ │ └── 테스트 결과 분석 (분석) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ After AI (2025+): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 개발자가 하는 일: │ │
│ │ ├── 테스트 전략 수립 (전략) ← 더 중요해짐! │ │
│ │ ├── 테스트 시나리오 설계 (설계) ← 핵심 역할! │ │
│ │ ├── AI에게 테스트 코드 생성 지시 (프롬프팅) │ │
│ │ ├── AI 생성 코드 검증 및 수정 (검증) ← 새 역할 │ │
│ │ └── 테스트 품질 및 전략 개선 (개선) │ │
│ │ │ │
│ │ 변화: "코드를 작성하는 사람" │ │
│ │ → "전략을 설계하고 결과를 검증하는 사람" │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ AI와 협업하는 테스트 워크플로우: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. 개발자: "이 컴포넌트의 테스트 전략을 세우자" │ │
│ │ ├── 핵심 사용자 시나리오 3개 정의 │ │
│ │ ├── 엣지 케이스 2개 식별 │ │
│ │ └── 에러 시나리오 1개 추가 │ │
│ │ │ │
│ │ 2. AI: 시나리오별 테스트 코드 생성 │ │
│ │ └── 보일러플레이트 + 기본 assertion 자동 작성 │ │
│ │ │ │
│ │ 3. 개발자: AI 코드 검증 + 수정 │ │
│ │ ├── API 정확성 확인 │ │
│ │ ├── 비즈니스 로직 반영 여부 확인 │ │
│ │ ├── assertion 강화 (부족한 검증 추가) │ │
│ │ └── 테스트 실패 시키기 (mutation test 수동) │ │
│ │ │ │
│ │ 4. CI: 자동 실행 + 커버리지 리포트 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
13. 실무 전략과 베스트 프랙티스
13.1 전문가 명언 모음
┌─────────────────────────────────────────────────────────────────┐
│ 테스트에 관한 전문가 명언 모음 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Kent C. Dodds: │ │
│ │ "The more your tests resemble the way your software │ │
│ │ is used, the more confidence they can give you." │ │
│ │ → 사용자처럼 테스트하라 │ │
│ │ │ │
│ │ Guillermo Rauch (Vercel CEO): │ │
│ │ "Write tests. Not too many. Mostly integration." │ │
│ │ → 테스트를 작성하되, 너무 많지 않게, 대부분 통합으로 │ │
│ │ │ │
│ │ Kent Beck (TDD 창시자): │ │
│ │ "Test as little as possible to reach a given level │ │
│ │ of confidence." │ │
│ │ → 자신감을 얻을 만큼만 최소한으로 테스트하라 │ │
│ │ │ │
│ │ Dan Abramov (React Core Team): │ │
│ │ "Bad tests are worse than no tests." │ │
│ │ → 나쁜 테스트는 없는 것보다 나쁘다 │ │
│ │ │ │
│ │ Martin Fowler: │ │
│ │ "Whenever you are tempted to type something into a │ │
│ │ print statement or a debugger expression, write it │ │
│ │ as a test instead." │ │
│ │ → console.log 대신 테스트를 작성하라 │ │
│ │ │ │
│ │ Michael Feathers: │ │
│ │ "Legacy code is simply code without tests." │ │
│ │ → 레거시 코드 = 테스트가 없는 코드 │ │
│ │ │ │
│ │ Robert C. Martin (Uncle Bob): │ │
│ │ "The only way to go fast, is to go well." │ │
│ │ → 빠르게 가는 유일한 방법은 제대로 가는 것이다 │ │
│ │ │ │
│ │ Mike Cohn: │ │
│ │ "The goal is to build quality in rather than │ │
│ │ inspect quality in." │ │
│ │ → 품질을 검사해서 넣는 게 아니라, 만들면서 넣어야 한다 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
13.2 실무 사례: Airbnb, Netflix, Shopify
┌─────────────────────────────────────────────────────────────────┐
│ 대형 회사들의 프론트엔드 테스트 전략 │
│ │
│ Airbnb: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 전략: │ │
│ │ ├── Enzyme → React Testing Library 마이그레이션 완료 │ │
│ │ ├── Jest + React Testing Library 기반 통합 테스트 중심 │ │
│ │ ├── 디자인 시스템(DLS)에 Visual Regression 적용 │ │
│ │ ├── Chromatic으로 Storybook 기반 시각적 테스트 │ │
│ │ └── 핵심 예약 플로우에 E2E (Playwright) 적용 │ │
│ │ │ │
│ │ 교훈: "테스트 도구보다 테스트 철학이 중요하다" │ │
│ │ Enzyme 시절의 내부 구현 의존 테스트 → │ │
│ │ RTL 전환으로 테스트 안정성 크게 향상 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Netflix: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 전략: │ │
│ │ ├── A/B 테스트가 매우 활발 → 테스트 자동화 필수 │ │
│ │ ├── React + Testing Library + Jest 사용 │ │
│ │ ├── Falcor(자체 데이터 레이어) 통합 테스트에 집중 │ │
│ │ ├── 성능 테스트: 자체 RUM (Real User Monitoring) │ │
│ │ └── E2E: Selenium → Playwright로 전환 중 │ │
│ │ │ │
│ │ 교훈: "성능도 테스트의 일부다" │ │
│ │ 기능뿐 아니라 로딩 속도, 인터랙션 반응 시간도 │ │
│ │ 자동화된 성능 테스트로 모니터링 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Shopify: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 전략: │ │
│ │ ├── Polaris (디자인 시스템): Storybook + Chromatic │ │
│ │ ├── React Testing Library 기반 컴포넌트 테스트 │ │
│ │ ├── MSW로 GraphQL API 모킹 (Storefront API) │ │
│ │ ├── Playwright로 핵심 체크아웃 플로우 E2E │ │
│ │ └── Hydrogen (React 프레임워크)에 테스트 내장 │ │
│ │ │ │
│ │ 교훈: "디자인 시스템에는 Visual Regression이 필수" │ │
│ │ 수백 개 컴포넌트의 스타일 변경을 수동으로 확인하는 건 │ │
│ │ 불가능 → Chromatic으로 자동 스크린샷 비교 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 공통 패턴: │
│ ├── Integration Test 중심 (Testing Trophy 따름) │
│ ├── Visual Regression으로 디자인 시스템 보호 │
│ ├── E2E는 핵심 비즈니스 플로우에만 집중 │
│ ├── Playwright 채택 증가 추세 │
│ └── MSW로 API 모킹 표준화 │
│ │
└─────────────────────────────────────────────────────────────────┘
13.3 프론트엔드 테스트 체크리스트
┌─────────────────────────────────────────────────────────────────┐
│ 프론트엔드 테스트 체크리스트 │
│ │
│ 프로젝트 시작 시: │
│ ├── [ ] TypeScript strict mode 활성화 │
│ ├── [ ] ESLint + eslint-plugin-testing-library 설정 │
│ ├── [ ] Vitest (또는 Jest) 설정 + jsdom 환경 │
│ ├── [ ] React Testing Library 설치 │
│ ├── [ ] MSW v2 handler 구조 설계 │
│ ├── [ ] Playwright 설정 (E2E용) │
│ └── [ ] CI 파이프라인에 테스트 단계 추가 │
│ │
│ 컴포넌트 개발 시: │
│ ├── [ ] 사용자 관점에서 테스트 시나리오 정의 │
│ ├── [ ] getByRole 우선으로 요소 쿼리 │
│ ├── [ ] userEvent로 사용자 상호작용 시뮬레이션 │
│ ├── [ ] 행복 경로(Happy Path) + 에러 시나리오 테스트 │
│ ├── [ ] 로딩 상태, 빈 상태, 에러 상태 테스트 │
│ ├── [ ] API 호출은 MSW로 모킹 │
│ └── [ ] 접근성(a11y) 검증 포함 │
│ │
│ 코드 리뷰 시: │
│ ├── [ ] 테스트가 내부 구현에 의존하지 않는가? │
│ ├── [ ] 테스트 이름이 동작을 명확히 설명하는가? │
│ ├── [ ] 불필요한 mock이 없는가? │
│ ├── [ ] snapshot 남용이 없는가? │
│ └── [ ] 테스트를 실패시켜봤을 때 정말 실패하는가? │
│ │
│ 배포 전: │
│ ├── [ ] 모든 Unit/Integration 테스트 통과 │
│ ├── [ ] 핵심 E2E 시나리오 통과 │
│ ├── [ ] Visual Regression 차이 없음 (또는 승인됨) │
│ ├── [ ] 커버리지 목표 달성 (70-80%) │
│ └── [ ] Lighthouse 성능 점수 유지 │
│ │
│ 정기 점검: │
│ ├── [ ] Flaky test 목록 관리 및 수정 │
│ ├── [ ] 커버리지 트렌드 모니터링 │
│ ├── [ ] 테스트 실행 시간 모니터링 │
│ ├── [ ] 핵심 로직에 Mutation Testing 적용 │
│ └── [ ] 테스트 부채(Test Debt) 관리 │
│ │
└─────────────────────────────────────────────────────────────────┘
13.4 프로젝트 규모별 권장 전략
┌─────────────────────────────────────────────────────────────────┐
│ 프로젝트 규모별 테스트 전략 │
│ │
│ 소규모 (1-3명, MVP, 사이드 프로젝트): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ├── Static: TypeScript + ESLint (필수) │ │
│ │ ├── Integration: 핵심 컴포넌트 3-5개만 │ │
│ │ ├── E2E: 핵심 시나리오 1-2개 (로그인, 주요 기능) │ │
│ │ └── Visual/Mutation: 생략 가능 │ │
│ │ │ │
│ │ 투자 시간: 전체 개발의 10-15% │ │
│ │ 목표: "핵심 기능이 깨지지 않는다"는 최소한의 확신 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 중규모 (4-10명, 서비스 운영 중): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ├── Static: TypeScript strict + ESLint │ │
│ │ ├── Unit: 유틸리티 + 커스텀 훅 │ │
│ │ ├── Integration: 모든 주요 컴포넌트 + MSW │ │
│ │ ├── E2E: 핵심 사용자 플로우 5-10개 (Playwright) │ │
│ │ ├── Visual: 디자인 시스템 있으면 Chromatic │ │
│ │ └── CI: 모든 PR에서 테스트 자동 실행 │ │
│ │ │ │
│ │ 투자 시간: 전체 개발의 20-25% │ │
│ │ 목표: "자신감 있게 배포할 수 있다" │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 대규모 (10명+, 엔터프라이즈): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ├── Static: TypeScript strict + ESLint + Prettier │ │
│ │ ├── Unit: 모든 비즈니스 로직 + Mutation Testing │ │
│ │ ├── Integration: 전체 컴포넌트 + MSW + 접근성 │ │
│ │ ├── E2E: 모든 사용자 시나리오 (Playwright, 병렬) │ │
│ │ ├── Visual: Storybook + Chromatic (필수) │ │
│ │ ├── Performance: Lighthouse CI + 커스텀 메트릭 │ │
│ │ ├── 보안: 의존성 취약점 자동 스캔 │ │
│ │ └── Contract: API 스키마 검증 (OpenAPI, GraphQL) │ │
│ │ │ │
│ │ 투자 시간: 전체 개발의 25-35% │ │
│ │ 목표: "장애 없이 안정적으로 서비스를 운영한다" │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
13.5 최종 요약
┌─────────────────────────────────────────────────────────────────┐
│ 프론트엔드 테스트 전략 - 최종 요약 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. Testing Trophy를 따르라 │ │
│ │ → Integration Test에 가장 많이 투자 │ │
│ │ │ │
│ │ 2. 사용자처럼 테스트하라 │ │
│ │ → React Testing Library + getByRole 우선 │ │
│ │ │ │
│ │ 3. 네트워크는 MSW로 모킹하라 │ │
│ │ → fetch/axios를 mock하지 말고 네트워크 레벨에서 │ │
│ │ │ │
│ │ 4. E2E는 핵심만 │ │
│ │ → Playwright로 5-10개 핵심 시나리오 │ │
│ │ │ │
│ │ 5. 내부 구현을 테스트하지 마라 │ │
│ │ → state, props, 함수 호출 직접 검사 금지 │ │
│ │ │ │
│ │ 6. TypeScript는 공짜 테스트 │ │
│ │ → strict mode로 타입 에러 사전 차단 │ │
│ │ │ │
│ │ 7. 커버리지에 집착하지 마라 │ │
│ │ → 70-80% 목표, Mutation Testing으로 품질 검증 │ │
│ │ │ │
│ │ 8. AI를 활용하되 검증하라 │ │
│ │ → 전략은 사람이, 코딩은 AI가, 검증은 다시 사람이 │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────┐ │ │
│ │ │ "Write tests. Not too many. Mostly integration."│ │ │
│ │ │ — Guillermo Rauch │ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
관련 키워드
Test Pyramid, Testing Trophy, Kent C. Dodds, Guillermo Rauch, Mike Cohn, Vitest, Jest, Playwright, Cypress, React Testing Library, Enzyme, MSW, Mock Service Worker, Storybook, Chromatic, Percy, E2E Test, Unit Test, Integration Test, Visual Regression, Mutation Testing, Stryker Mutator, Code Coverage, Snapshot Testing, Testing Library, userEvent, getByRole, Query Priority, Flaky Test, Static Analysis, TypeScript, ESLint, jsdom, Service Worker, Testing Honeycomb, Spotify, Google Testing, Small Medium Large, Kent Beck, Dan Abramov, Martin Fowler, Michael Feathers, TDD, BDD, Lighthouse CI, Core Web Vitals, a11y, Accessibility Testing, AI Test Generation, GitHub Copilot, Codium AI, Test Debt, Regression Test