프론트엔드 테스트 디렉토리 구조 - 최적의 테스트 파일 배치 전략
TL;DR
- 프론트엔드 테스트 디렉토리 구조 - 최적의 테스트 파일 배치 전략의 핵심 개념을 빠르게 파악할 수 있다.
- 배경과 이유를 통해 왜 필요한지 맥락을 이해할 수 있다.
- 특징과 상세 내용을 통해 실무 적용 포인트를 확인할 수 있다.
1. 개념
프론트엔드 테스트 디렉토리 구조 - 최적의 테스트 파일 배치 전략의 핵심 정의와 문제 공간을 간단히 정리한다.
2. 배경
이 주제가 등장한 기술적·조직적 배경과 기존 접근의 한계를 설명한다.
3. 이유
왜 지금 이 방식을 채택해야 하는지, 기대 효과와 트레이드오프를 함께 정리한다.
4. 특징
핵심 동작 방식, 장단점, 적용 시 주의점을 빠르게 훑을 수 있도록 요약한다.
5. 상세 내용
프론트엔드 테스트 디렉토리 구조 - 최적의 테스트 파일 배치 전략
작성일: 2026-02-27 카테고리: Frontend / Testing / Project Structure 포함 내용: Colocation, tests, .test.ts, .spec.ts, Jest, Vitest, Playwright, Cypress, E2E, Unit Test, 모노레포, Kent C. Dodds
1. 왜 테스트 파일 배치가 중요한가?
1.1 테스트 파일 위치가 개발 생산성에 미치는 영향
┌─────────────────────────────────────────────────────────────────┐
│ 테스트 파일 배치는 왜 중요한가? │
│ │
│ 비유: 도서관의 책 정리 시스템 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 잘 정리된 도서관: │ │
│ │ ├── 요리책 → 요리 코너에 있다 │ │
│ │ ├── 소설 → 문학 코너에 있다 │ │
│ │ ├── 찾고 싶은 책을 바로 찾을 수 있다 │ │
│ │ └── 새 책이 들어와도 어디 놓을지 명확하다 │ │
│ │ │ │
│ │ 정리 안 된 도서관: │ │
│ │ ├── 요리책이 과학 코너에 섞여 있다 │ │
│ │ ├── 같은 시리즈인데 1권과 2권이 다른 층에 있다 │ │
│ │ ├── 책을 찾느라 30분을 허비한다 │ │
│ │ └── 결국 "그냥 안 읽지 뭐" → 독서 포기 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 테스트 파일도 마찬가지: │
│ ├── 잘 배치하면 → 테스트를 쉽게 찾고, 쉽게 실행 │
│ ├── 못 배치하면 → 테스트를 못 찾고, 안 실행하게 됨 │
│ └── "테스트를 찾을 수 없으면 테스트를 실행하지 않는다" │
│ │
│ Google 내부 연구 (2019): │
│ ├── 테스트 파일이 소스 옆에 있을 때 → 테스트 커버리지 23% 높음 │
│ ├── 테스트 파일이 멀리 있을 때 → 새 기능에 테스트 누락 빈번 │
│ └── "눈에 보여야 마음에 있다" (Out of sight, out of mind) │
│ │
└─────────────────────────────────────────────────────────────────┘
1.2 일관성의 중요성
┌─────────────────────────────────────────────────────────────────┐
│ 일관성이 없으면 생기는 문제 │
│ │
│ 프로젝트 A의 현실: │
│ │
│ src/ │
│ ├── components/ │
│ │ ├── Button/ │
│ │ │ ├── Button.tsx │
│ │ │ └── Button.test.tsx ← 여기에 테스트가 있네? │
│ │ ├── Modal/ │
│ │ │ └── Modal.tsx ← 테스트가 없네? │
│ │ └── Header/ │
│ │ └── Header.tsx ← 이것도 없네? │
│ tests/ │
│ ├── Modal.spec.ts ← 여기 있었구나! │
│ └── components/ │
│ └── header-test.js ← 이름도 다르고 확장자도 다름 │
│ │
│ 팀원의 반응: │
│ ├── "Modal 테스트는 어디 있어요?" → 10분간 검색 │
│ ├── "Header 테스트 파일 이름이 왜 달라요?" → 혼란 │
│ ├── "저는 .test로 만들었는데 옆에는 .spec이네요" → 불일치 │
│ └── "그냥 새로 하나 만들까..." → 중복 테스트 발생 │
│ │
│ 일관성의 원칙: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. 테스트 파일 위치: 한 가지 방식만 사용 │ │
│ │ 2. 파일 이름 규칙: .test.ts 또는 .spec.ts 하나만 │ │
│ │ 3. 디렉토리 구조: 소스 구조를 그대로 반영 │ │
│ │ 4. 문서화: README나 CONTRIBUTING에 규칙 명시 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
2. 두 가지 주요 접근법
2.1 Colocation (테스트 파일을 소스 옆에 배치)
┌─────────────────────────────────────────────────────────────────┐
│ Colocation 전략 (소스 옆에 테스트 배치) │
│ │
│ 핵심 인물: Kent C. Dodds │
│ 원칙: "관련된 것은 가까이 두어라" (Colocation Principle) │
│ │
│ Kent C. Dodds의 주장 (2019년 블로그): │
│ "파일을 가장 많이 사용하는 곳과 가까이 두면 │
│ 유지보수가 쉬워지고, 더 건강한 코드베이스가 된다." │
│ │
│ 디렉토리 구조 예시: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ src/ │ │
│ │ ├── components/ │ │
│ │ │ ├── Button/ │ │
│ │ │ │ ├── Button.tsx ← 소스 코드 │ │
│ │ │ │ ├── Button.test.tsx ← 테스트 (바로 옆!) │ │
│ │ │ │ ├── Button.stories.tsx ← Storybook │ │
│ │ │ │ └── Button.module.css ← 스타일 │ │
│ │ │ ├── Modal/ │ │
│ │ │ │ ├── Modal.tsx │ │
│ │ │ │ ├── Modal.test.tsx │ │
│ │ │ │ └── Modal.module.css │ │
│ │ │ └── Header/ │ │
│ │ │ ├── Header.tsx │ │
│ │ │ └── Header.test.tsx │ │
│ │ ├── hooks/ │ │
│ │ │ ├── useAuth.ts │ │
│ │ │ └── useAuth.test.ts │ │
│ │ └── utils/ │ │
│ │ ├── formatDate.ts │ │
│ │ └── formatDate.test.ts │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 장점: │
│ ├── 1. 관련 파일이 한눈에 보인다 │
│ │ → Button.tsx 옆에 Button.test.tsx가 바로 있으니 │
│ │ "아, 이 컴포넌트에 테스트가 있구나" 즉시 확인 │
│ ├── 2. 파일 이동 시 테스트도 같이 이동 │
│ │ → Button/ 폴더를 통째로 옮기면 테스트도 자동으로 따라감 │
│ │ → 별도 디렉토리면 동기화를 깜빡할 수 있음 │
│ ├── 3. 테스트 누락을 즉시 발견 │
│ │ → 폴더를 열었는데 .test.tsx가 없다? → "아, 작성해야겠다" │
│ ├── 4. import 경로가 짧다 │
│ │ → import { Button } from './Button' │
│ │ → (별도 디렉토리면 '../../../src/components/Button') │
│ └── 5. 삭제할 때 깔끔하다 │
│ → 컴포넌트 폴더 삭제 = 테스트도 같이 삭제 │
│ → 고아 테스트(orphaned test) 방지 │
│ │
│ 단점: │
│ ├── 1. 폴더 안에 파일이 많아질 수 있다 │
│ │ → Button.tsx, Button.test.tsx, Button.stories.tsx, │
│ │ Button.module.css, index.ts → 5개 이상 │
│ ├── 2. 소스와 테스트가 섞여서 빌드 설정이 필요 │
│ │ → tsconfig에서 테스트 파일 제외 설정 필요 │
│ └── 3. 파일 탐색기에서 시각적으로 복잡해질 수 있음 │
│ │
└─────────────────────────────────────────────────────────────────┘
2.2 Separation (별도 test 디렉토리)
┌─────────────────────────────────────────────────────────────────┐
│ Separation 전략 (별도 __tests__ 디렉토리) │
│ │
│ __tests__ 디렉토리는 Jest의 기본 설정에서 유래 │
│ Jest는 기본적으로 __tests__/ 안의 파일을 테스트로 인식 │
│ │
│ 디렉토리 구조 예시: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ src/ │ │
│ │ ├── components/ │ │
│ │ │ ├── __tests__/ │ │
│ │ │ │ ├── Button.test.tsx │ │
│ │ │ │ ├── Modal.test.tsx │ │
│ │ │ │ └── Header.test.tsx │ │
│ │ │ ├── Button/ │ │
│ │ │ │ ├── Button.tsx │ │
│ │ │ │ └── Button.module.css │ │
│ │ │ ├── Modal/ │ │
│ │ │ │ └── Modal.tsx │ │
│ │ │ └── Header/ │ │
│ │ │ └── Header.tsx │ │
│ │ ├── hooks/ │ │
│ │ │ ├── __tests__/ │ │
│ │ │ │ └── useAuth.test.ts │ │
│ │ │ └── useAuth.ts │ │
│ │ └── utils/ │ │
│ │ ├── __tests__/ │ │
│ │ │ └── formatDate.test.ts │ │
│ │ └── formatDate.ts │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 장점: │
│ ├── 1. 소스와 테스트가 명확히 분리 │
│ │ → 소스 코드만 볼 때 테스트 파일이 시야를 방해하지 않음 │
│ ├── 2. 배포 시 테스트 파일을 쉽게 제외 │
│ │ → __tests__/ 디렉토리만 .dockerignore에 추가하면 끝 │
│ ├── 3. Jest 기본 설정과 호환 │
│ │ → 별도 설정 없이 Jest가 자동으로 인식 │
│ └── 4. 테스트 파일만 모아서 볼 수 있다 │
│ → 코드 리뷰 시 테스트만 별도로 확인 가능 │
│ │
│ 단점: │
│ ├── 1. 소스와 테스트가 멀어진다 │
│ │ → Button.tsx를 수정하고 테스트를 깜빡할 확률 증가 │
│ ├── 2. 소스 디렉토리 구조와 동기화가 필요 │
│ │ → components/ 이름 변경 시 __tests__/도 바꿔야 함 │
│ ├── 3. import 경로가 길어진다 │
│ │ → import { Button } from '../Button/Button' │
│ └── 4. 파일 이동 시 테스트가 뒤에 남을 수 있다 │
│ → 고아 테스트(orphaned test) 발생 위험 │
│ │
└─────────────────────────────────────────────────────────────────┘
2.3 두 접근법의 비교 요약
┌─────────────────────────────────────────────────────────────────┐
│ Colocation vs Separation 비교표 │
│ │
│ ┌─────────────┬──────────────────┬──────────────────┐ │
│ │ 기준 │ Colocation │ Separation │ │
│ ├─────────────┼──────────────────┼──────────────────┤ │
│ │ 테스트 발견 │ 매우 쉬움 │ 약간 어려움 │ │
│ │ 파일 이동 │ 자동으로 따라감 │ 수동 동기화 필요 │ │
│ │ import 경로 │ 짧다 ('./Button') │ 길다 ('../src/..') │ │
│ │ 폴더 정리 │ 파일 많아질 수 │ 깔끔하게 분리 │ │
│ │ 누락 방지 │ 즉시 발견 가능 │ 놓치기 쉬움 │ │
│ │ 빌드 제외 │ 설정 필요 │ 쉬움 │ │
│ │ 팀 선호도 │ 현대 프로젝트 │ 전통적/레거시 │ │
│ │ 대표 지지자 │ Kent C. Dodds │ Jest 공식 문서 │ │
│ └─────────────┴──────────────────┴──────────────────┘ │
│ │
│ 현재 업계 트렌드 (2025~2026): │
│ ├── Colocation이 대세로 자리잡는 추세 │
│ ├── React, Vue, Angular 공식 문서 모두 Colocation 예시 사용 │
│ ├── Vitest, Jest 모두 Colocation 지원 │
│ └── Kent C. Dodds의 영향으로 프론트엔드 커뮤니티에서 표준화 │
│ │
│ 결론: "특별한 이유가 없다면 Colocation을 선택하라" │
│ │
└─────────────────────────────────────────────────────────────────┘
3. 파일 명명 규칙
3.1 .test.ts vs .spec.ts - 기원과 차이
┌─────────────────────────────────────────────────────────────────┐
│ .test.ts vs .spec.ts - 어디서 왔는가? │
│ │
│ .spec.ts의 기원: │
│ ├── "spec" = "specification" (명세서) │
│ ├── BDD (Behavior-Driven Development)에서 유래 │
│ ├── "소프트웨어의 동작 명세를 작성한다"는 의미 │
│ ├── Jasmine (2010) → Mocha → Cypress에서 전통적으로 사용 │
│ └── RSpec (Ruby)에서 프론트엔드로 전파된 관례 │
│ │
│ .test.ts의 기원: │
│ ├── "test" = "테스트" (직관적) │
│ ├── TDD (Test-Driven Development)에서 자연스럽게 사용 │
│ ├── Jest (2014, Facebook)가 기본 패턴으로 채택 │
│ ├── Vitest (2022)도 Jest와 동일한 패턴 사용 │
│ └── 더 직관적이고 이해하기 쉬움 │
│ │
│ 프레임워크별 기본 설정: │
│ ┌──────────────────┬────────────────────────┐ │
│ │ 프레임워크 │ 기본 패턴 │ │
│ ├──────────────────┼────────────────────────┤ │
│ │ Jest │ *.test.{ts,tsx,js,jsx} │ │
│ │ Vitest │ *.test.{ts,tsx,js,jsx} │ │
│ │ Cypress │ *.cy.{ts,tsx,js,jsx} │ ← (v10부터) │
│ │ Playwright │ *.spec.{ts,js} │ │
│ │ Jasmine │ *.spec.{ts,js} │ │
│ │ Angular CLI │ *.spec.ts │ │
│ │ Vue CLI │ *.spec.{ts,js} │ │
│ └──────────────────┴────────────────────────┘ │
│ │
│ 기술적으로 차이는 없다: │
│ → 둘 다 단순한 파일 이름 규칙(naming convention)일 뿐 │
│ → 테스트 러너가 인식하는 패턴만 다를 뿐 │
│ → 둘 중 어떤 것을 쓰든 테스트 기능은 동일 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.2 혼용하는 프로젝트의 규칙
┌─────────────────────────────────────────────────────────────────┐
│ .test와 .spec을 함께 쓰는 전략 │
│ │
│ 일부 프로젝트에서는 의도적으로 구분해서 사용: │
│ │
│ 전략 1: 테스트 유형으로 구분 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ .test.ts = Unit Test (단위 테스트) │ │
│ │ ├── Button.test.tsx → Button 컴포넌트 유닛 테스트 │ │
│ │ ├── useAuth.test.ts → useAuth 훅 유닛 테스트 │ │
│ │ └── formatDate.test.ts → formatDate 유틸 유닛 테스트 │ │
│ │ │ │
│ │ .spec.ts = Integration Test (통합 테스트) │ │
│ │ ├── LoginFlow.spec.tsx → 로그인 전체 흐름 통합 테스트 │ │
│ │ ├── CartCheckout.spec.tsx → 장바구니 결제 통합 테스트 │ │
│ │ └── UserProfile.spec.tsx → 프로필 CRUD 통합 테스트 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 전략 2: 실행 환경으로 구분 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ .test.ts = jsdom 환경 (Jest/Vitest) │ │
│ │ → 빠른 단위/통합 테스트 │ │
│ │ │ │
│ │ .spec.ts = 브라우저 환경 (Playwright/Cypress) │ │
│ │ → 실제 브라우저에서 실행되는 E2E 테스트 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 권장사항: │
│ ├── 팀 내에서 하나의 규칙을 정하고 ESLint로 강제하라 │
│ ├── eslint-plugin-filenames 또는 eslint-plugin-check-file 사용 │
│ └── 규칙 예시: "유닛은 .test, E2E는 .spec"을 CONTRIBUTING.md에 │
│ │
└─────────────────────────────────────────────────────────────────┘
4. 프레임워크별 권장 구조
4.1 React 프로젝트 (CRA / Vite)
┌─────────────────────────────────────────────────────────────────┐
│ React 프로젝트 - 권장 디렉토리 구조 │
│ │
│ Colocation 방식 (가장 널리 사용됨): │
│ │
│ src/ │
│ ├── components/ │
│ │ ├── Button/ │
│ │ │ ├── index.ts ← 배럴 파일 (re-export) │
│ │ │ ├── Button.tsx ← 컴포넌트 구현 │
│ │ │ ├── Button.test.tsx ← 유닛 테스트 │
│ │ │ ├── Button.stories.tsx ← Storybook 스토리 │
│ │ │ ├── Button.module.css ← CSS Modules 스타일 │
│ │ │ └── Button.types.ts ← 타입 정의 (선택) │
│ │ ├── Modal/ │
│ │ │ ├── index.ts │
│ │ │ ├── Modal.tsx │
│ │ │ ├── Modal.test.tsx │
│ │ │ └── Modal.module.css │
│ │ └── Form/ │
│ │ ├── index.ts │
│ │ ├── Form.tsx │
│ │ ├── Form.test.tsx │
│ │ ├── FormField.tsx ← 하위 컴포넌트 │
│ │ └── FormField.test.tsx │
│ ├── hooks/ │
│ │ ├── useAuth.ts │
│ │ ├── useAuth.test.ts │
│ │ ├── useLocalStorage.ts │
│ │ └── useLocalStorage.test.ts │
│ ├── utils/ │
│ │ ├── formatDate.ts │
│ │ ├── formatDate.test.ts │
│ │ ├── validators.ts │
│ │ └── validators.test.ts │
│ └── pages/ │
│ ├── Home/ │
│ │ ├── Home.tsx │
│ │ └── Home.test.tsx │
│ └── Dashboard/ │
│ ├── Dashboard.tsx │
│ └── Dashboard.test.tsx │
│ │
│ React Testing Library + Jest/Vitest 조합이 표준 │
│ Kent C. Dodds가 만든 @testing-library/react가 사실상 표준 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.2 Vue 프로젝트
┌─────────────────────────────────────────────────────────────────┐
│ Vue 프로젝트 - 권장 디렉토리 구조 │
│ │
│ Vue CLI / Vite + Vue 3: │
│ │
│ src/ │
│ ├── components/ │
│ │ ├── Button/ │
│ │ │ ├── Button.vue ← SFC (Single File Component) │
│ │ │ ├── Button.spec.ts ← Vue는 .spec 전통이 강함 │
│ │ │ └── Button.stories.ts ← Storybook │
│ │ ├── Modal/ │
│ │ │ ├── Modal.vue │
│ │ │ └── Modal.spec.ts │
│ │ └── Form/ │
│ │ ├── Form.vue │
│ │ ├── Form.spec.ts │
│ │ ├── FormField.vue │
│ │ └── FormField.spec.ts │
│ ├── composables/ ← Vue 3의 Composition API │
│ │ ├── useAuth.ts │
│ │ ├── useAuth.spec.ts │
│ │ ├── useCounter.ts │
│ │ └── useCounter.spec.ts │
│ ├── stores/ ← Pinia 스토어 │
│ │ ├── auth.ts │
│ │ ├── auth.spec.ts │
│ │ ├── cart.ts │
│ │ └── cart.spec.ts │
│ └── views/ ← Vue Router 페이지 │
│ ├── HomeView.vue │
│ ├── HomeView.spec.ts │
│ └── DashboardView.vue │
│ │
│ Vue의 특징: │
│ ├── Vue CLI가 .spec.ts를 기본으로 생성해서 .spec 전통이 있음 │
│ ├── @vue/test-utils + Vitest 조합이 공식 권장 (2024~) │
│ ├── Vitest가 Vue 팀과 같은 Evan You 생태계라서 호환성 우수 │
│ └── @testing-library/vue도 인기 (React에서 영향 받음) │
│ │
└─────────────────────────────────────────────────────────────────┘
4.3 Next.js 프로젝트
┌─────────────────────────────────────────────────────────────────┐
│ Next.js 프로젝트 - 권장 디렉토리 구조 │
│ │
│ Next.js는 특별한 주의가 필요하다! │
│ │
│ 이유: App Router에서 특수 파일명 규칙이 있기 때문 │
│ ├── page.tsx → 라우트 페이지 │
│ ├── layout.tsx → 레이아웃 │
│ ├── loading.tsx → 로딩 UI │
│ ├── error.tsx → 에러 UI │
│ └── route.ts → API Route │
│ │
│ 문제: app/ 디렉토리 안에 .test.tsx를 두면? │
│ → Next.js가 이를 라우트로 인식하지는 않지만 │
│ → 혼란을 줄 수 있고, 빌드 시 불필요한 파일이 포함될 수 있음 │
│ │
│ Next.js 공식 권장: __tests__ 디렉토리 사용 │
│ │
│ 방법 1: 프로젝트 루트에 __tests__ (Next.js 공식 문서 권장) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ my-app/ │ │
│ │ ├── app/ │ │
│ │ │ ├── page.tsx │ │
│ │ │ ├── layout.tsx │ │
│ │ │ ├── dashboard/ │ │
│ │ │ │ └── page.tsx │ │
│ │ │ └── api/ │ │
│ │ │ └── users/ │ │
│ │ │ └── route.ts │ │
│ │ ├── __tests__/ ← 루트 레벨에 분리 │ │
│ │ │ ├── page.test.tsx │ │
│ │ │ ├── dashboard/ │ │
│ │ │ │ └── page.test.tsx │ │
│ │ │ └── api/ │ │
│ │ │ └── users.test.ts │ │
│ │ ├── components/ ← 공유 컴포넌트 │ │
│ │ │ ├── Button/ │ │
│ │ │ │ ├── Button.tsx │ │
│ │ │ │ └── Button.test.tsx ← 컴포넌트는 colocation│ │
│ │ │ └── Header/ │ │
│ │ │ ├── Header.tsx │ │
│ │ │ └── Header.test.tsx │ │
│ │ └── jest.config.ts │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 방법 2: app 내부에 __tests__ 폴더 사용 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ app/ │ │
│ │ ├── __tests__/ │ │
│ │ │ └── page.test.tsx │ │
│ │ ├── page.tsx │ │
│ │ ├── dashboard/ │ │
│ │ │ ├── __tests__/ │ │
│ │ │ │ └── page.test.tsx │ │
│ │ │ └── page.tsx │ │
│ │ └── layout.tsx │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 핵심 포인트: │
│ ├── App Router의 page/layout/route 파일은 colocation 어려움 │
│ ├── 공유 컴포넌트(components/)는 colocation이 여전히 유효 │
│ ├── Pages Router(pages/)는 colocation이 불가 (모두 라우트) │
│ └── next.config.js에서 pageExtensions 설정으로 해결 가능하지만 │
│ 공식 문서에서는 __tests__ 분리를 더 권장 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.4 Angular 프로젝트
┌─────────────────────────────────────────────────────────────────┐
│ Angular 프로젝트 - 기본 디렉토리 구조 │
│ │
│ Angular CLI는 .spec.ts를 컴포넌트 옆에 자동 생성: │
│ │
│ src/app/ │
│ ├── components/ │
│ │ ├── button/ │
│ │ │ ├── button.component.ts ← 컴포넌트 클래스 │
│ │ │ ├── button.component.html ← 템플릿 │
│ │ │ ├── button.component.css ← 스타일 │
│ │ │ └── button.component.spec.ts ← CLI가 자동 생성! │
│ │ └── modal/ │
│ │ ├── modal.component.ts │
│ │ ├── modal.component.html │
│ │ ├── modal.component.css │
│ │ └── modal.component.spec.ts │
│ ├── services/ │
│ │ ├── auth.service.ts │
│ │ └── auth.service.spec.ts ← 서비스도 자동 생성 │
│ └── pipes/ │
│ ├── format-date.pipe.ts │
│ └── format-date.pipe.spec.ts ← 파이프도 자동 생성 │
│ │
│ Angular의 특징: │
│ ├── CLI가 `ng generate component` 시 .spec.ts를 함께 생성 │
│ ├── Karma + Jasmine이 전통적 조합 (현재는 Jest 전환 추세) │
│ ├── Colocation이 강제됨 (CLI 기본 동작) │
│ └── .spec.ts 사용이 Angular 생태계의 표준 │
│ │
└─────────────────────────────────────────────────────────────────┘
5. E2E 테스트 디렉토리 구조
5.1 E2E 테스트는 항상 분리한다
┌─────────────────────────────────────────────────────────────────┐
│ E2E 테스트를 분리해야 하는 이유 │
│ │
│ Unit/Integration 테스트 vs E2E 테스트: │
│ │
│ ┌──────────────────┬──────────────────┬─────────────────┐ │
│ │ 특성 │ Unit/Integration │ E2E │ │
│ ├──────────────────┼──────────────────┼─────────────────┤ │
│ │ 실행 환경 │ Node.js (jsdom) │ 실제 브라우저 │ │
│ │ 속도 │ 밀리초~초 │ 초~분 │ │
│ │ 의존성 │ 소스 코드 직접 │ 배포된 앱 │ │
│ │ 테스트 대상 │ 함수, 컴포넌트 │ 전체 사용자 흐름 │ │
│ │ 실행 빈도 │ 매 커밋 │ CI/CD, 주기적 │ │
│ │ 설정 파일 │ jest/vitest.config│ playwright.config│ │
│ └──────────────────┴──────────────────┴─────────────────┘ │
│ │
│ 분리 이유: │
│ ├── 1. 실행 환경이 완전히 다르다 │
│ │ → Unit: Node.js에서 실행 │
│ │ → E2E: 실제 Chrome/Firefox/Safari에서 실행 │
│ ├── 2. 설정 파일이 다르다 │
│ │ → Unit: vitest.config.ts 또는 jest.config.ts │
│ │ → E2E: playwright.config.ts 또는 cypress.config.ts │
│ ├── 3. 실행 주기가 다르다 │
│ │ → Unit: 개발 중 수시로, 커밋 시 자동 │
│ │ → E2E: PR 시, 배포 전, 야간 빌드 │
│ ├── 4. 테스트 관점이 다르다 │
│ │ → Unit: "이 함수가 올바른 값을 반환하는가?" │
│ │ → E2E: "사용자가 로그인할 수 있는가?" │
│ └── 5. 팀 역할이 다를 수 있다 │
│ → Unit: 개발자가 작성 │
│ → E2E: QA 엔지니어가 작성하기도 함 │
│ │
│ 권장 위치: │
│ ├── 프로젝트 루트의 e2e/ 디렉토리 │
│ ├── 또는 tests/e2e/ 디렉토리 │
│ └── 절대로 src/ 안에 넣지 않는다 │
│ │
└─────────────────────────────────────────────────────────────────┘
5.2 Playwright 기본 구조
┌─────────────────────────────────────────────────────────────────┐
│ Playwright E2E 테스트 디렉토리 구조 │
│ │
│ Playwright: Microsoft가 만든 E2E 테스트 프레임워크 │
│ 2024~2026년 현재 가장 인기 있는 E2E 도구 │
│ │
│ 기본 구조: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ my-app/ │ │
│ │ ├── src/ ← 소스 코드 │ │
│ │ │ └── ... │ │
│ │ ├── e2e/ ← E2E 테스트 루트 │ │
│ │ │ ├── fixtures/ ← 테스트 데이터/설정 │ │
│ │ │ │ ├── auth.fixture.ts ← 인증 fixture │ │
│ │ │ │ └── test-data.json ← 테스트 데이터 │ │
│ │ │ ├── pages/ ← Page Object Model │ │
│ │ │ │ ├── LoginPage.ts ← 로그인 페이지 객체 │ │
│ │ │ │ ├── DashboardPage.ts ← 대시보드 페이지 객체 │ │
│ │ │ │ └── BasePage.ts ← 공통 페이지 객체 │ │
│ │ │ ├── specs/ ← 실제 테스트 파일 │ │
│ │ │ │ ├── auth/ │ │
│ │ │ │ │ ├── login.spec.ts │ │
│ │ │ │ │ └── signup.spec.ts │ │
│ │ │ │ ├── dashboard/ │ │
│ │ │ │ │ └── widgets.spec.ts │ │
│ │ │ │ └── checkout/ │ │
│ │ │ │ └── payment.spec.ts │ │
│ │ │ └── helpers/ ← 테스트 헬퍼 함수 │ │
│ │ │ ├── api-helper.ts ← API 호출 헬퍼 │ │
│ │ │ └── db-helper.ts ← DB 시딩 헬퍼 │ │
│ │ └── playwright.config.ts ← Playwright 설정 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Page Object Model (POM) 패턴: │
│ ├── 페이지별 클래스를 만들어서 셀렉터와 액션을 캡슐화 │
│ ├── 테스트 코드에서 직접 셀렉터를 쓰지 않음 │
│ ├── UI가 변경되면 Page Object만 수정 → 테스트는 그대로 │
│ └── Playwright 공식 문서에서 강력 권장 │
│ │
│ 예시: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // pages/LoginPage.ts │ │
│ │ export class LoginPage { │ │
│ │ constructor(private page: Page) {} │ │
│ │ │ │
│ │ async login(email: string, password: string) { │ │
│ │ await this.page.fill('#email', email); │ │
│ │ await this.page.fill('#password', password); │ │
│ │ await this.page.click('[data-testid="login-btn"]'); │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ // specs/auth/login.spec.ts │ │
│ │ test('사용자가 로그인할 수 있다', async ({ page }) => { │ │
│ │ const loginPage = new LoginPage(page); │ │
│ │ await loginPage.login('user@test.com', 'pass123'); │ │
│ │ await expect(page).toHaveURL('/dashboard'); │ │
│ │ }); │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
5.3 Cypress 기본 구조
┌─────────────────────────────────────────────────────────────────┐
│ Cypress E2E 테스트 디렉토리 구조 │
│ │
│ Cypress: 프론트엔드 친화적 E2E 테스트 프레임워크 │
│ 2017년부터 인기, 현재도 많은 프로젝트에서 사용 │
│ │
│ 기본 구조 (Cypress v13+): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ my-app/ │ │
│ │ ├── src/ ← 소스 코드 │ │
│ │ │ └── ... │ │
│ │ ├── cypress/ ← Cypress 전용 디렉토리 │ │
│ │ │ ├── e2e/ ← E2E 테스트 파일 │ │
│ │ │ │ ├── auth/ │ │
│ │ │ │ │ ├── login.cy.ts │ │
│ │ │ │ │ └── signup.cy.ts │ │
│ │ │ │ ├── dashboard/ │ │
│ │ │ │ │ └── widgets.cy.ts │ │
│ │ │ │ └── checkout/ │ │
│ │ │ │ └── payment.cy.ts │ │
│ │ │ ├── fixtures/ ← 테스트 데이터 (JSON) │ │
│ │ │ │ ├── users.json │ │
│ │ │ │ └── products.json │ │
│ │ │ ├── support/ ← 커스텀 커맨드/설정 │ │
│ │ │ │ ├── commands.ts ← cy.login() 등 │ │
│ │ │ │ ├── e2e.ts ← E2E 전역 설정 │ │
│ │ │ │ └── component.ts ← 컴포넌트 테스트 설정 │ │
│ │ │ └── downloads/ ← 다운로드 파일 임시 │ │
│ │ └── cypress.config.ts ← Cypress 설정 파일 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Cypress vs Playwright 구조 비교: │
│ ┌──────────────┬──────────────────┬──────────────────┐ │
│ │ 항목 │ Cypress │ Playwright │ │
│ ├──────────────┼──────────────────┼──────────────────┤ │
│ │ 테스트 위치 │ cypress/e2e/ │ e2e/specs/ │ │
│ │ 파일 확장자 │ .cy.ts │ .spec.ts │ │
│ │ 헬퍼 │ cypress/support/ │ e2e/helpers/ │ │
│ │ 데이터 │ cypress/fixtures/ │ e2e/fixtures/ │ │
│ │ 설정 파일 │ cypress.config.ts │ playwright.config │ │
│ │ POM 패턴 │ 선택적 │ 강력 권장 │ │
│ └──────────────┴──────────────────┴──────────────────┘ │
│ │
│ Cypress 고유 기능: │
│ ├── Component Testing: 컴포넌트를 브라우저에서 직접 테스트 │
│ │ → cypress/support/component.ts에서 설정 │
│ │ → src/ 안의 .cy.tsx 파일로 작성 (colocation 가능) │
│ └── cy.intercept(): 네트워크 요청을 가로채서 모킹 가능 │
│ │
└─────────────────────────────────────────────────────────────────┘
6. 테스트 유형별 분리 전략
6.1 전체 테스트 유형과 배치 위치
┌─────────────────────────────────────────────────────────────────┐
│ 테스트 유형별 파일 배치 전략 │
│ │
│ 비유: 병원의 검사 시스템 │
│ ├── 혈액 검사 (Unit Test): 간단하고 빠름, 많이 함 │
│ ├── X-ray 검사 (Integration Test): 여러 부위 확인, 중간 │
│ ├── 종합 건강검진 (E2E Test): 전체 검사, 느리고 비쌈 │
│ └── 외관 검사 (Visual Test): 겉보기가 정상인지 확인 │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Unit Tests (유닛 테스트) │ │
│ │ ├── 위치: 소스 파일 옆 (Colocation) │ │
│ │ ├── 패턴: Button.test.tsx │ │
│ │ ├── 러너: Jest, Vitest │ │
│ │ └── 예시: │ │
│ │ src/components/Button/ │ │
│ │ ├── Button.tsx │ │
│ │ └── Button.test.tsx ← 바로 옆 │ │
│ │ │ │
│ ├───────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ Integration Tests (통합 테스트) │ │
│ │ ├── 위치: 소스 옆 또는 __tests__/integration/ │ │
│ │ ├── 패턴: LoginForm.integration.test.tsx │ │
│ │ │ 또는 __tests__/integration/login.test.tsx │ │
│ │ ├── 러너: Jest, Vitest (+ MSW for API mocking) │ │
│ │ └── 예시: │ │
│ │ src/features/auth/ │ │
│ │ ├── LoginForm.tsx │ │
│ │ ├── LoginForm.test.tsx ← unit │ │
│ │ └── LoginForm.integration.test.tsx ← integration│ │
│ │ │ │
│ ├───────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ E2E Tests (종단간 테스트) │ │
│ │ ├── 위치: 프로젝트 루트 e2e/ (항상 분리) │ │
│ │ ├── 패턴: login.spec.ts │ │
│ │ ├── 러너: Playwright, Cypress │ │
│ │ └── 예시: │ │
│ │ e2e/specs/auth/ │ │
│ │ └── login.spec.ts ← 소스와 완전 분리 │ │
│ │ │ │
│ ├───────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ Visual Tests (시각적 회귀 테스트) │ │
│ │ ├── 위치: Storybook stories 옆 (Colocation) │ │
│ │ ├── 패턴: Button.stories.tsx (+ Chromatic) │ │
│ │ ├── 도구: Storybook + Chromatic, Percy │ │
│ │ └── 예시: │ │
│ │ src/components/Button/ │ │
│ │ ├── Button.tsx │ │
│ │ ├── Button.test.tsx │ │
│ │ └── Button.stories.tsx ← visual test 역할 │ │
│ │ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
6.2 테스트 실행 명령어 분리
┌─────────────────────────────────────────────────────────────────┐
│ package.json scripts 분리 예시 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "scripts": { │ │
│ │ "test": "vitest", │ │
│ │ "test:unit": "vitest --dir src", │ │
│ │ "test:integration": "vitest --dir src │ │
│ │ --testPathPattern=integration", │ │
│ │ "test:e2e": "playwright test", │ │
│ │ "test:e2e:ui": "playwright test --ui", │ │
│ │ "test:visual": "chromatic", │ │
│ │ "test:coverage": "vitest --coverage", │ │
│ │ "test:ci": "vitest run && playwright test" │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ CI/CD에서의 실행 순서: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. npm run test:unit ← 가장 먼저 (빠름) │ │
│ │ 2. npm run test:integration ← 그 다음 (중간) │ │
│ │ 3. npm run test:e2e ← 마지막 (느림) │ │
│ │ 4. npm run test:visual ← PR에서만 (Chromatic) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 빠른 것부터 실행하는 이유: │
│ → Unit이 실패하면 Integration/E2E를 실행할 필요 없음 │
│ → "빠른 피드백 루프"가 개발 생산성의 핵심 │
│ │
└─────────────────────────────────────────────────────────────────┘
7. 모노레포에서의 테스트 구조
7.1 모노레포 기본 개념과 테스트 배치
┌─────────────────────────────────────────────────────────────────┐
│ 모노레포(Monorepo)에서의 테스트 구조 │
│ │
│ 모노레포란? │
│ = 여러 패키지/앱을 하나의 저장소(repository)에서 관리 │
│ = Turborepo, Nx, pnpm workspace 등으로 구성 │
│ │
│ 비유: 한 건물에 여러 회사가 입주한 오피스 빌딩 │
│ ├── 각 회사(패키지)는 독립적으로 운영 │
│ ├── 공용 시설(공유 패키지)은 함께 사용 │
│ └── 건물 전체 점검(E2E)은 빌딩 관리실에서 담당 │
│ │
│ 권장 구조: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ monorepo-root/ │ │
│ │ ├── packages/ ← 공유 패키지 │ │
│ │ │ ├── ui/ ← UI 컴포넌트 라이브러리 │ │
│ │ │ │ ├── src/ │ │
│ │ │ │ │ ├── Button/ │ │
│ │ │ │ │ │ ├── Button.tsx │ │
│ │ │ │ │ │ └── Button.test.tsx ← colocation │ │
│ │ │ │ │ └── Modal/ │ │
│ │ │ │ │ ├── Modal.tsx │ │
│ │ │ │ │ └── Modal.test.tsx │ │
│ │ │ │ ├── vitest.config.ts ← 패키지별 설정 │ │
│ │ │ │ └── package.json │ │
│ │ │ ├── utils/ ← 공유 유틸리티 │ │
│ │ │ │ ├── src/ │ │
│ │ │ │ │ ├── formatDate.ts │ │
│ │ │ │ │ └── formatDate.test.ts │ │
│ │ │ │ ├── vitest.config.ts │ │
│ │ │ │ └── package.json │ │
│ │ │ └── test-utils/ ← 공유 테스트 유틸 │ │
│ │ │ ├── src/ │ │
│ │ │ │ ├── render.tsx ← 커스텀 render │ │
│ │ │ │ ├── mocks/ ← 공유 모킹 │ │
│ │ │ │ └── factories/ ← 데이터 팩토리 │ │
│ │ │ └── package.json │ │
│ │ ├── apps/ ← 애플리케이션 │ │
│ │ │ ├── web/ ← 웹 프론트엔드 │ │
│ │ │ │ ├── src/ │ │
│ │ │ │ │ └── ... (colocation 테스트) │ │
│ │ │ │ ├── e2e/ ← 앱별 E2E 테스트 │ │
│ │ │ │ │ ├── specs/ │ │
│ │ │ │ │ │ ├── login.spec.ts │ │
│ │ │ │ │ │ └── checkout.spec.ts │ │
│ │ │ │ │ └── playwright.config.ts │ │
│ │ │ │ ├── vitest.config.ts │ │
│ │ │ │ └── package.json │ │
│ │ │ └── admin/ ← 관리자 앱 │ │
│ │ │ ├── src/ │ │
│ │ │ │ └── ... │ │
│ │ │ ├── e2e/ │ │
│ │ │ └── package.json │ │
│ │ ├── turbo.json ← Turborepo 설정 │ │
│ │ ├── vitest.workspace.ts ← Vitest 워크스페이스 │ │
│ │ └── package.json │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
7.2 모노레포 테스트 실행 전략
┌─────────────────────────────────────────────────────────────────┐
│ 모노레포에서 테스트 실행하기 │
│ │
│ 패키지별 독립 실행: │
│ ├── cd packages/ui && npm test → UI 패키지만 테스트 │
│ ├── cd apps/web && npm test → 웹 앱만 테스트 │
│ └── cd apps/web && npm run test:e2e→ 웹 앱 E2E만 테스트 │
│ │
│ 루트에서 전체 실행 (Turborepo): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // turbo.json │ │
│ │ { │ │
│ │ "pipeline": { │ │
│ │ "test": { │ │
│ │ "dependsOn": ["^build"], │ │
│ │ "outputs": ["coverage/**"] │ │
│ │ }, │ │
│ │ "test:e2e": { │ │
│ │ "dependsOn": ["build"], │ │
│ │ "outputs": [] │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ // 실행 │ │
│ │ turbo run test → 모든 패키지 unit test 병렬 실행 │ │
│ │ turbo run test:e2e → 모든 앱 E2E test 실행 │ │
│ │ turbo run test --filter=packages/ui → ui만 테스트 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 핵심 원칙: │
│ ├── 1. 각 패키지는 자체 테스트 설정을 가진다 │
│ ├── 2. 공유 테스트 유틸은 별도 패키지로 분리 │
│ ├── 3. E2E는 앱(apps/) 레벨에서만 실행 │
│ └── 4. CI에서는 변경된 패키지만 테스트 (turbo --filter) │
│ │
└─────────────────────────────────────────────────────────────────┘
8. 테스트 유틸리티와 헬퍼 파일 배치
8.1 커스텀 render 함수 (test-utils.ts)
┌─────────────────────────────────────────────────────────────────┐
│ 테스트 유틸리티 파일 배치 │
│ │
│ test-utils.ts란? │
│ = React Testing Library의 render를 감싸서 │
│ Provider(테마, 라우터, 상태관리 등)를 자동으로 추가하는 함수 │
│ = 모든 테스트에서 import해서 사용 │
│ │
│ 위치: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ src/ │ │
│ │ ├── test-utils/ ← 테스트 유틸 전용 │ │
│ │ │ ├── index.ts ← 배럴 파일 │ │
│ │ │ ├── render.tsx ← 커스텀 render 함수 │ │
│ │ │ ├── setup.ts ← 전역 설정 │ │
│ │ │ └── matchers.ts ← 커스텀 matcher │ │
│ │ └── components/ │ │
│ │ └── Button/ │ │
│ │ └── Button.test.tsx │ │
│ │ → import { render } from '@/test-utils' │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 커스텀 render 함수 예시: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // src/test-utils/render.tsx │ │
│ │ import { render, RenderOptions } from │ │
│ │ '@testing-library/react'; │ │
│ │ import { ThemeProvider } from '@/providers/theme'; │ │
│ │ import { QueryClientProvider } from '@tanstack/..'; │ │
│ │ │ │
│ │ function AllProviders({ children }) { │ │
│ │ return ( │ │
│ │ <QueryClientProvider client={queryClient}> │ │
│ │ <ThemeProvider> │ │
│ │ {children} │ │
│ │ </ThemeProvider> │ │
│ │ </QueryClientProvider> │ │
│ │ ); │ │
│ │ } │ │
│ │ │ │
│ │ export function customRender( │ │
│ │ ui: ReactElement, │ │
│ │ options?: RenderOptions │ │
│ │ ) { │ │
│ │ return render(ui, { │ │
│ │ wrapper: AllProviders, ...options │ │
│ │ }); │ │
│ │ } │ │
│ │ │ │
│ │ export * from '@testing-library/react'; │ │
│ │ export { customRender as render }; │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
8.2 모킹 (Mocks) 디렉토리
┌─────────────────────────────────────────────────────────────────┐
│ 모킹 파일 배치 전략 │
│ │
│ 모킹(Mocking)이란? │
│ = 실제 API, DB, 외부 서비스를 가짜로 대체하는 것 │
│ = 테스트를 빠르고 안정적으로 만들기 위해 필수 │
│ │
│ MSW (Mock Service Worker) 기반 구조: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ src/ │ │
│ │ ├── mocks/ ← 모킹 전용 디렉토리 │ │
│ │ │ ├── handlers/ ← API 핸들러 │ │
│ │ │ │ ├── auth.ts ← 인증 API 모킹 │ │
│ │ │ │ ├── users.ts ← 사용자 API 모킹 │ │
│ │ │ │ ├── products.ts ← 상품 API 모킹 │ │
│ │ │ │ └── index.ts ← 모든 핸들러 합치기 │ │
│ │ │ ├── data/ ← 모킹 데이터 │ │
│ │ │ │ ├── users.ts ← 가짜 사용자 데이터 │ │
│ │ │ │ └── products.ts ← 가짜 상품 데이터 │ │
│ │ │ ├── server.ts ← MSW 서버 설정 (test) │ │
│ │ │ └── browser.ts ← MSW 브라우저 (dev) │ │
│ │ └── ... │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ MSW의 장점: │
│ ├── 네트워크 레벨에서 가로채기 → fetch/axios 상관없이 동작 │
│ ├── 같은 핸들러를 개발(browser.ts)과 테스트(server.ts)에서 공유 │
│ ├── REST + GraphQL 모두 지원 │
│ └── Storybook에서도 사용 가능 (msw-storybook-addon) │
│ │
└─────────────────────────────────────────────────────────────────┘
8.3 Fixtures와 Factories
┌─────────────────────────────────────────────────────────────────┐
│ Fixtures와 Factories 배치 │
│ │
│ Fixture (고정값): │
│ = 테스트에서 반복 사용하는 고정된 데이터 │
│ = JSON 파일이나 상수로 정의 │
│ │
│ Factory (생성기): │
│ = 테스트 데이터를 동적으로 생성하는 함수 │
│ = 매번 다른 데이터가 필요할 때 유용 │
│ = faker.js + fishery 같은 라이브러리 활용 │
│ │
│ 배치: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ src/ │ │
│ │ ├── test-utils/ │ │
│ │ │ ├── fixtures/ ← 고정 테스트 데이터 │ │
│ │ │ │ ├── user.fixture.ts │ │
│ │ │ │ │ → export const mockUser = { │ │
│ │ │ │ │ id: '1', name: 'John', ... } │ │
│ │ │ │ ├── product.fixture.ts │ │
│ │ │ │ └── order.fixture.ts │ │
│ │ │ ├── factories/ ← 동적 데이터 생성기 │ │
│ │ │ │ ├── user.factory.ts │ │
│ │ │ │ │ → export const userFactory = Factory.define │ │
│ │ │ │ │ <User>(({ sequence }) => ({ │ │
│ │ │ │ │ id: String(sequence), │ │
│ │ │ │ │ name: faker.person.fullName(), │ │
│ │ │ │ │ email: faker.internet.email(), │ │
│ │ │ │ │ })); │ │
│ │ │ │ └── product.factory.ts │ │
│ │ │ └── index.ts │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Fixture vs Factory 선택 기준: │
│ ┌──────────────┬────────────────────┬──────────────────┐ │
│ │ 상황 │ Fixture │ Factory │ │
│ ├──────────────┼────────────────────┼──────────────────┤ │
│ │ 항상 같은 값 │ 적합 │ 불필요 │ │
│ │ 매번 다른 값 │ 부적합 │ 적합 │ │
│ │ 관계형 데이터 │ 복잡 │ 적합 │ │
│ │ 대량 데이터 │ 불편 │ 적합 │ │
│ │ 스냅샷 비교 │ 적합 │ 비결정적 위험 │ │
│ └──────────────┴────────────────────┴──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
9. 안티패턴
9.1 소스와 동떨어진 거대한 tests 폴더
┌─────────────────────────────────────────────────────────────────┐
│ 안티패턴 1: 거대한 __tests__ 폴더 │
│ │
│ 문제 상황: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ src/ │ │
│ │ ├── components/ │ │
│ │ │ ├── Button.tsx │ │
│ │ │ ├── Modal.tsx │ │
│ │ │ ├── Header.tsx │ │
│ │ │ ├── Footer.tsx │ │
│ │ │ ├── Sidebar.tsx │ │
│ │ │ └── ... (50개 이상의 컴포넌트) │ │
│ │ __tests__/ ← 모든 테스트가 여기! │ │
│ │ ├── Button.test.tsx │ │
│ │ ├── Modal.test.tsx │ │
│ │ ├── Header.test.tsx │ │
│ │ ├── auth.test.tsx │ │
│ │ ├── utils.test.tsx │ │
│ │ └── ... (수십 개의 테스트가 flat하게 나열) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 왜 나쁜가: │
│ ├── 소스와 테스트 사이의 거리가 멀다 │
│ ├── 어떤 컴포넌트의 테스트인지 파악하기 어렵다 │
│ ├── 파일이 많아지면 스크롤만 해도 시간이 걸린다 │
│ ├── 소스 파일 삭제 시 테스트가 고아(orphan)로 남는다 │
│ └── 새 팀원이 "테스트 어디 있어요?" 질문이 반복된다 │
│ │
└─────────────────────────────────────────────────────────────────┘
9.2 테스트 파일명이 소스와 다른 이름
┌─────────────────────────────────────────────────────────────────┐
│ 안티패턴 2: 소스와 다른 이름의 테스트 파일 │
│ │
│ 문제 상황: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ src/components/Button/Button.tsx │ │
│ │ __tests__/btn-test.js ← 이름이 다르다! │ │
│ │ │ │
│ │ src/hooks/useAuth.ts │ │
│ │ __tests__/authentication.spec.ts ← 이름이 다르다! │ │
│ │ │ │
│ │ src/utils/formatDate.ts │ │
│ │ __tests__/date-helpers-test.js ← 이름도, 확장자도! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 왜 나쁜가: │
│ ├── "Button의 테스트는 어디?" → 검색해도 안 나옴 │
│ ├── grep "Button.test" → 결과 없음 (btn-test라서) │
│ ├── 코드 리뷰 시 소스와 테스트 매칭이 안 됨 │
│ └── 새 팀원이 테스트를 못 찾아서 중복으로 작성 │
│ │
│ 규칙: 소스 파일명 = 테스트 파일명 │
│ ├── Button.tsx → Button.test.tsx │
│ ├── useAuth.ts → useAuth.test.ts │
│ └── formatDate.ts → formatDate.test.ts │
│ │
└─────────────────────────────────────────────────────────────────┘
9.3 E2E와 Unit 테스트를 같은 디렉토리에 혼합
┌─────────────────────────────────────────────────────────────────┐
│ 안티패턴 3: E2E와 Unit 테스트 혼합 │
│ │
│ 문제 상황: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ src/components/Button/ │ │
│ │ ├── Button.tsx │ │
│ │ ├── Button.test.tsx ← Jest로 실행 (unit) │ │
│ │ └── Button.e2e.spec.ts ← Playwright로 실행 (e2e) │ │
│ │ │ │
│ │ → npm test 실행 시 두 파일 다 잡히면? │ │
│ │ → E2E 테스트가 Unit 러너에서 실행되어 에러! │ │
│ │ → 또는 느린 E2E가 빠른 Unit 사이에 섞여서 혼란! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 왜 나쁜가: │
│ ├── 실행 환경이 다른 테스트가 섞임 (jsdom vs 브라우저) │
│ ├── 설정 파일 충돌 가능성 │
│ ├── CI에서 분리 실행이 어려움 │
│ └── 느린 E2E가 빠른 유닛 테스트 피드백을 지연시킴 │
│ │
│ 해결: E2E는 반드시 별도 디렉토리에 배치 │
│ ├── Unit/Integration → src/ 안에 (colocation) │
│ └── E2E → 프로젝트 루트 e2e/ 또는 tests/e2e/ │
│ │
└─────────────────────────────────────────────────────────────────┘
9.4 테스트 유틸을 각 테스트 파일에 중복 작성
┌─────────────────────────────────────────────────────────────────┐
│ 안티패턴 4: 테스트 유틸 중복 작성 │
│ │
│ 문제 상황: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // Button.test.tsx │ │
│ │ function renderWithProviders(ui) { │ │
│ │ return render( │ │
│ │ <ThemeProvider><QueryProvider> │ │
│ │ {ui} │ │
│ │ </QueryProvider></ThemeProvider> │ │
│ │ ); │ │
│ │ } │ │
│ │ │ │
│ │ // Modal.test.tsx (같은 함수를 또 작성!) │ │
│ │ function renderWithProviders(ui) { │ │
│ │ return render( │ │
│ │ <ThemeProvider><QueryProvider> │ │
│ │ {ui} │ │
│ │ </QueryProvider></ThemeProvider> │ │
│ │ ); │ │
│ │ } │ │
│ │ │ │
│ │ → 20개 파일에 같은 함수가 복사-붙여넣기 되어 있다면? │ │
│ │ → Provider 변경 시 20곳을 다 수정해야 함! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 해결: 공통 test-utils로 추출 │
│ ├── src/test-utils/render.tsx에 한 번만 정의 │
│ ├── 모든 테스트에서 import { render } from '@/test-utils' │
│ └── Provider 변경 시 한 곳만 수정하면 끝 │
│ │
└─────────────────────────────────────────────────────────────────┘
10. 설정 파일 가이드
10.1 Jest 설정
┌─────────────────────────────────────────────────────────────────┐
│ Jest 설정으로 테스트 파일 인식하기 │
│ │
│ jest.config.ts: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ export default { │ │
│ │ // 테스트 파일 패턴 (어떤 파일을 테스트로 인식?) │ │
│ │ testMatch: [ │ │
│ │ '<rootDir>/src/**/*.test.{ts,tsx}', │ │
│ │ '<rootDir>/src/**/__tests__/**/*.{ts,tsx}', │ │
│ │ ], │ │
│ │ │ │
│ │ // 테스트에서 제외할 패턴 │ │
│ │ testPathIgnorePatterns: [ │ │
│ │ '/node_modules/', │ │
│ │ '/e2e/', // E2E 테스트는 제외 │ │
│ │ '/dist/', │ │
│ │ ], │ │
│ │ │ │
│ │ // 모듈 경로 별칭 (import '@/test-utils' 지원) │ │
│ │ moduleNameMapper: { │ │
│ │ '^@/(.*)$': '<rootDir>/src/$1', │ │
│ │ '^@/test-utils$': '<rootDir>/src/test-utils', │ │
│ │ }, │ │
│ │ │ │
│ │ // 전역 설정 파일 │ │
│ │ setupFilesAfterFramework: [ │ │
│ │ '<rootDir>/src/test-utils/setup.ts', │ │
│ │ ], │ │
│ │ }; │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
10.2 Vitest 설정
┌─────────────────────────────────────────────────────────────────┐
│ Vitest 설정으로 테스트 파일 인식하기 │
│ │
│ Vitest: Vite 기반의 차세대 테스트 러너 │
│ Jest 호환 API + Vite의 빠른 빌드 = 빠른 테스트 │
│ │
│ vitest.config.ts: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ import { defineConfig } from 'vitest/config'; │ │
│ │ │ │
│ │ export default defineConfig({ │ │
│ │ test: { │ │
│ │ // 테스트 파일 포함 패턴 │ │
│ │ include: [ │ │
│ │ 'src/**/*.test.{ts,tsx}', │ │
│ │ ], │ │
│ │ │ │
│ │ // 제외 패턴 │ │
│ │ exclude: [ │ │
│ │ 'node_modules', │ │
│ │ 'e2e', │ │
│ │ 'dist', │ │
│ │ ], │ │
│ │ │ │
│ │ // 브라우저 환경 시뮬레이션 │ │
│ │ environment: 'jsdom', │ │
│ │ │ │
│ │ // 전역 설정 │ │
│ │ setupFiles: ['./src/test-utils/setup.ts'], │ │
│ │ │ │
│ │ // 커버리지 설정 │ │
│ │ coverage: { │ │
│ │ provider: 'v8', │ │
│ │ reporter: ['text', 'html', 'lcov'], │ │
│ │ exclude: [ │ │
│ │ 'src/test-utils/**', // 테스트 유틸 제외 │ │
│ │ 'src/mocks/**', // 모킹 파일 제외 │ │
│ │ '**/*.stories.tsx', // Storybook 제외 │ │
│ │ ], │ │
│ │ }, │ │
│ │ }, │ │
│ │ }); │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Vitest의 장점 (Jest 대비): │
│ ├── Vite 설정 재사용 → 별도 빌드 설정 불필요 │
│ ├── ESM 네이티브 지원 → 변환 오류 적음 │
│ ├── HMR 기반 watch 모드 → 변경된 테스트만 재실행 (빠름) │
│ └── Jest 호환 API → 마이그레이션 쉬움 │
│ │
└─────────────────────────────────────────────────────────────────┘
10.3 tsconfig에서 테스트 파일 제외
┌─────────────────────────────────────────────────────────────────┐
│ TypeScript 설정에서 테스트 파일 관리 │
│ │
│ Colocation 방식을 사용할 때 빌드에서 테스트를 제외해야 한다: │
│ │
│ tsconfig.json (전체 프로젝트): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "compilerOptions": { ... }, │ │
│ │ "include": ["src"], │ │
│ │ "exclude": [ │ │
│ │ "node_modules", │ │
│ │ "**/*.test.ts", │ │
│ │ "**/*.test.tsx", │ │
│ │ "**/*.spec.ts", │ │
│ │ "**/*.spec.tsx", │ │
│ │ "src/test-utils", │ │
│ │ "src/mocks", │ │
│ │ "e2e" │ │
│ │ ] │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ tsconfig.test.json (테스트 전용): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "extends": "./tsconfig.json", │ │
│ │ "compilerOptions": { │ │
│ │ "types": ["vitest/globals", │ │
│ │ "@testing-library/jest-dom"] │ │
│ │ }, │ │
│ │ "include": [ │ │
│ │ "src/**/*.test.ts", │ │
│ │ "src/**/*.test.tsx", │ │
│ │ "src/test-utils/**", │ │
│ │ "src/mocks/**" │ │
│ │ ] │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 이렇게 분리하면: │
│ ├── 프로덕션 빌드에 테스트 코드가 포함되지 않음 │
│ ├── IDE에서 테스트 파일의 타입 체크가 정상 동작 │
│ └── vitest가 tsconfig.test.json을 자동으로 참조 │
│ │
└─────────────────────────────────────────────────────────────────┘
11. 결론: 권장 디렉토리 구조 템플릿
11.1 종합 ASCII art 다이어그램
┌─────────────────────────────────────────────────────────────────┐
│ 종합 권장 디렉토리 구조 (전체 템플릿) │
│ │
│ my-frontend-app/ │
│ │ │
│ ├── src/ ← 소스 코드 루트 │
│ │ ├── components/ ← 공통 UI 컴포넌트 │
│ │ │ ├── Button/ │
│ │ │ │ ├── index.ts ← re-export │
│ │ │ │ ├── Button.tsx ← 컴포넌트 구현 │
│ │ │ │ ├── Button.test.tsx ← 유닛 테스트 (colocation) │
│ │ │ │ ├── Button.stories.tsx← Storybook + visual test │
│ │ │ │ └── Button.module.css ← 스타일 │
│ │ │ ├── Modal/ │
│ │ │ │ ├── index.ts │
│ │ │ │ ├── Modal.tsx │
│ │ │ │ ├── Modal.test.tsx │
│ │ │ │ └── Modal.stories.tsx │
│ │ │ └── Form/ │
│ │ │ ├── index.ts │
│ │ │ ├── Form.tsx │
│ │ │ ├── Form.test.tsx │
│ │ │ ├── FormField.tsx │
│ │ │ └── FormField.test.tsx │
│ │ │ │
│ │ ├── features/ ← 기능별 모듈 │
│ │ │ ├── auth/ │
│ │ │ │ ├── components/ │
│ │ │ │ │ ├── LoginForm.tsx │
│ │ │ │ │ └── LoginForm.test.tsx │
│ │ │ │ ├── hooks/ │
│ │ │ │ │ ├── useAuth.ts │
│ │ │ │ │ └── useAuth.test.ts │
│ │ │ │ └── api/ │
│ │ │ │ ├── authApi.ts │
│ │ │ │ └── authApi.test.ts │
│ │ │ └── cart/ │
│ │ │ ├── components/ │
│ │ │ │ ├── CartItem.tsx │
│ │ │ │ └── CartItem.test.tsx │
│ │ │ └── hooks/ │
│ │ │ ├── useCart.ts │
│ │ │ └── useCart.test.ts │
│ │ │ │
│ │ ├── hooks/ ← 공통 커스텀 훅 │
│ │ │ ├── useLocalStorage.ts │
│ │ │ ├── useLocalStorage.test.ts │
│ │ │ ├── useDebounce.ts │
│ │ │ └── useDebounce.test.ts │
│ │ │ │
│ │ ├── utils/ ← 공통 유틸리티 │
│ │ │ ├── formatDate.ts │
│ │ │ ├── formatDate.test.ts │
│ │ │ ├── validators.ts │
│ │ │ └── validators.test.ts │
│ │ │ │
│ │ ├── test-utils/ ← 테스트 공통 유틸 │
│ │ │ ├── index.ts ← 배럴 파일 │
│ │ │ ├── render.tsx ← 커스텀 render │
│ │ │ ├── setup.ts ← 전역 테스트 설정 │
│ │ │ ├── fixtures/ ← 고정 테스트 데이터 │
│ │ │ │ ├── user.fixture.ts │
│ │ │ │ └── product.fixture.ts │
│ │ │ └── factories/ ← 동적 데이터 생성기 │
│ │ │ ├── user.factory.ts │
│ │ │ └── product.factory.ts │
│ │ │ │
│ │ └── mocks/ ← API 모킹 (MSW) │
│ │ ├── handlers/ │
│ │ │ ├── auth.ts │
│ │ │ ├── users.ts │
│ │ │ └── index.ts │
│ │ ├── data/ │
│ │ │ └── users.ts │
│ │ ├── server.ts ← 테스트용 MSW 서버 │
│ │ └── browser.ts ← 개발용 MSW 브라우저 │
│ │ │
│ ├── e2e/ ← E2E 테스트 (완전 분리) │
│ │ ├── fixtures/ ← E2E 전용 fixture │
│ │ │ └── auth.fixture.ts │
│ │ ├── pages/ ← Page Object Model │
│ │ │ ├── BasePage.ts │
│ │ │ ├── LoginPage.ts │
│ │ │ └── DashboardPage.ts │
│ │ ├── specs/ ← E2E 테스트 파일 │
│ │ │ ├── auth/ │
│ │ │ │ ├── login.spec.ts │
│ │ │ │ └── signup.spec.ts │
│ │ │ └── checkout/ │
│ │ │ └── payment.spec.ts │
│ │ └── helpers/ ← E2E 헬퍼 함수 │
│ │ └── api-helper.ts │
│ │ │
│ ├── vitest.config.ts ← Unit/Integration 설정 │
│ ├── playwright.config.ts ← E2E 설정 │
│ ├── tsconfig.json ← 프로덕션 TS 설정 │
│ ├── tsconfig.test.json ← 테스트 전용 TS 설정 │
│ └── package.json │
│ │
└─────────────────────────────────────────────────────────────────┘
11.2 프로젝트 규모별 권장사항
┌─────────────────────────────────────────────────────────────────┐
│ 프로젝트 규모별 테스트 구조 권장사항 │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 소규모 프로젝트 (1~3명, 컴포넌트 20개 미만) │ │
│ │ ────────────────────────────────────────── │ │
│ │ ├── Colocation만으로 충분 │ │
│ │ ├── 별도 test-utils 불필요 (각 파일에서 직접 설정) │ │
│ │ ├── E2E는 선택사항 (수동 테스트로도 가능) │ │
│ │ ├── 모킹은 간단한 jest.mock()으로 충분 │ │
│ │ └── 구조: │ │
│ │ src/ │ │
│ │ ├── components/ │ │
│ │ │ ├── Button.tsx │ │
│ │ │ └── Button.test.tsx │ │
│ │ └── utils/ │ │
│ │ ├── helpers.ts │ │
│ │ └── helpers.test.ts │ │
│ │ │ │
│ ├───────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ 중규모 프로젝트 (3~10명, 컴포넌트 50~200개) │ │
│ │ ────────────────────────────────────────── │ │
│ │ ├── Colocation + test-utils 분리 │ │
│ │ ├── MSW로 API 모킹 중앙화 │ │
│ │ ├── E2E 테스트 (핵심 흐름만) │ │
│ │ ├── Storybook + Chromatic으로 visual regression │ │
│ │ ├── CI/CD에 테스트 파이프라인 구축 │ │
│ │ └── 구조: │ │
│ │ src/ │ │
│ │ ├── components/ (colocation) │ │
│ │ ├── features/ (feature별 모듈) │ │
│ │ ├── test-utils/ │ │
│ │ └── mocks/ │ │
│ │ e2e/ │ │
│ │ └── specs/ (핵심 플로우) │ │
│ │ │ │
│ ├───────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ 대규모 프로젝트 (10명+, 모노레포) │ │
│ │ ────────────────────────────────────────── │ │
│ │ ├── 모노레포 구조 (Turborepo/Nx) │ │
│ │ ├── 패키지별 독립 테스트 설정 │ │
│ │ ├── 공유 test-utils 패키지 (packages/test-utils) │ │
│ │ ├── 공유 MSW 핸들러 패키지 │ │
│ │ ├── 앱별 E2E 테스트 │ │
│ │ ├── 변경된 패키지만 테스트 (turbo --filter) │ │
│ │ ├── 병렬 테스트 실행으로 CI 시간 단축 │ │
│ │ └── 구조: │ │
│ │ packages/ │ │
│ │ ├── ui/src/ (colocation) │ │
│ │ ├── utils/src/ (colocation) │ │
│ │ └── test-utils/ (공유) │ │
│ │ apps/ │ │
│ │ ├── web/src/ + e2e/ │ │
│ │ └── admin/src/ + e2e/ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11.3 최종 체크리스트
┌─────────────────────────────────────────────────────────────────┐
│ 테스트 디렉토리 구조 최종 체크리스트 │
│ │
│ 프로젝트 시작 시 확인할 사항: │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ [ ] 1. 테스트 배치 전략을 결정했는가? │ │
│ │ → Colocation (권장) 또는 __tests__ 중 하나 선택 │ │
│ │ │ │
│ │ [ ] 2. 파일 이름 규칙을 정했는가? │ │
│ │ → .test.ts 또는 .spec.ts 중 하나 선택 │ │
│ │ → (선택) unit은 .test, e2e는 .spec 등 구분 │ │
│ │ │ │
│ │ [ ] 3. E2E 테스트 디렉토리를 분리했는가? │ │
│ │ → e2e/ 또는 tests/e2e/ 디렉토리 생성 │ │
│ │ │ │
│ │ [ ] 4. 테스트 유틸리티 디렉토리를 만들었는가? │ │
│ │ → test-utils/ (커스텀 render, setup 등) │ │
│ │ │ │
│ │ [ ] 5. 모킹 전략을 정했는가? │ │
│ │ → mocks/ 디렉토리 (MSW 핸들러 등) │ │
│ │ │ │
│ │ [ ] 6. tsconfig에서 테스트 파일을 제외했는가? │ │
│ │ → 프로덕션 빌드에 포함되지 않도록 │ │
│ │ │ │
│ │ [ ] 7. package.json scripts를 분리했는가? │ │
│ │ → test:unit, test:e2e 등 구분 │ │
│ │ │ │
│ │ [ ] 8. CI/CD 파이프라인에 테스트를 추가했는가? │ │
│ │ → unit → integration → e2e 순서로 실행 │ │
│ │ │ │
│ │ [ ] 9. 팀 규칙을 문서화했는가? │ │
│ │ → CONTRIBUTING.md에 테스트 규칙 명시 │ │
│ │ │ │
│ │ [ ] 10. ESLint로 규칙을 강제했는가? │ │
│ │ → 파일명 패턴, import 규칙 등 자동 검사 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 한 줄 요약: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ "유닛 테스트는 소스 옆에(Colocation), │ │
│ │ E2E 테스트는 루트에 분리(Separation), │ │
│ │ 일관된 이름 규칙을 지키고, 문서화하라!" │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
관련 키워드
Colocation, __tests__, .test.ts, .spec.ts, Jest, Vitest, Playwright, Cypress, E2E Test, Unit Test, Integration Test, Visual Test, Kent C. Dodds, React Testing Library, MSW, Mock Service Worker, Page Object Model, Storybook, Chromatic, Monorepo, Turborepo, Nx, test-utils, Fixture, Factory, tsconfig, CI/CD, BDD, TDD, Test Pyramid, Colocation Principle, Orphaned Test, Next.js App Router, Vue Test Utils, Angular CLI, fishery, faker.js