프론트엔드 테스트 디렉토리 구조 - 최적의 테스트 파일 배치 전략
TL;DR
- Colocation과 Separation 두 가지 테스트 배치 방식을 비교한다.
- 파일 네이밍 규칙(.test/.spec)과 프레임워크 관례를 정리한다.
- E2E 테스트는 별도 디렉토리로 분리하는 원칙을 제시한다.
1. 개념
테스트 파일의 위치와 규칙을 정해 탐색성과 일관성을 확보하는 전략이다.
2. 배경
배치와 명명 불일치로 테스트 검색 비용과 누락이 늘어났다.
3. 이유
테스트 커버리지와 팀 협업 효율을 높이기 위해 필요하다.
4. 특징
Colocation 원칙, tests 분리, 프레임워크별 권장 구조, E2E 분리가 핵심이다.
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