hongsoohyuk
HomeResumeGuestbookProjectInstagramBlog

© 2026 hongsoohyuk. All rights reserved.

GitHubLinkedIn
Blog/E2E 테스트 시나리오 선정 기준

E2E 테스트 시나리오 선정 기준

FrontendTest

Last edited: February 24, 2026

"뭘 테스트해야 하지?"

Playwright를 설치하고, 설정 파일을 작성하고, 첫 번째 예제 테스트를 실행하는 것까지는 어렵지 않다. 진짜 어려운 건 그 다음이다.

"어떤 시나리오를 E2E로 테스트해야 하지?"

이 질문에 답하지 못하면 두 가지 극단에 빠지게 된다:

  • 모든 것을 E2E로 테스트하려다가 느리고 불안정한 테스트 스위트를 얻거나
  • 뭘 테스트할지 몰라서 예제 테스트 하나만 남겨둔 채 방치하거나

이 글에서는 E2E 테스트 시나리오를 체계적으로 선정하는 기준을 정리한다.


대원칙: 사용자 여정(User Journey) 중심

E2E 테스트의 핵심은 사용자 관점이다. 코드가 아닌, 사용자가 서비스에서 수행하는 업무 흐름을 기준으로 시나리오를 정한다.

plain text
GOOD: "대시보드에 진입하면 주요 지표가 표시된다"
GOOD: "목록에서 항목을 클릭하면 상세 정보가 나타난다"
GOOD: "폼을 작성하고 저장하면 목록에 반영된다"

BAD:  "API가 200을 반환한다" (→ API 테스트 영역)
BAD:  "버튼 색상이 파란색이다" (→ 시각적 회귀 테스트 영역)
BAD:  "formatDate 함수가 올바른 포맷을 반환한다" (→ 단위 테스트 영역)

판단 플로우차트

mermaid
flowchart TD
    A["테스트하려는 것이 무엇인가?"] --> B{"여러 페이지에 걸친\n사용자 흐름인가?"}
    B -->|YES| C["E2E 테스트"]
    B -->|NO| D{"실제 브라우저 라우팅\n+ API가 필요한가?"}
    D -->|YES| C
    D -->|NO| E{"단일 컴포넌트의\n렌더링/인터랙션인가?"}
    E -->|YES| F["컴포넌트 테스트"]
    E -->|NO| G["단위 테스트"]

선정 기준 체크리스트

아래 기준 중 2개 이상 해당되면 E2E 테스트 작성을 고려하자.

Must-Have: 반드시 테스트해야 하는 것

기준설명예시
크리티컬 패스이 기능이 깨지면 서비스를 사용할 수 없다로그인, 메인 페이지 진입, 핵심 데이터 조회
멀티 페이지 흐름2개 이상 페이지를 거치는 워크플로우목록 → 상세 → 수정 → 저장
라우팅/네비게이션URL 변경, 리다이렉트, 가드 동작잘못된 URL 접근 시 리다이렉트
인증/권한인증 상태에 따른 접근 제어미인증 시 로그인 페이지 리다이렉트, 권한별 메뉴

Should-Have: 테스트하면 좋은 것

기준설명예시
폼 제출 흐름입력 → 유효성 검증 → API 호출 → 결과 확인사용자 등록, 설정 변경, 데이터 입력
테이블 인터랙션필터, 정렬, 페이지네이션의 실제 동작데이터 테이블 필터링 후 결과 갱신
조건부 UI사용자 역할, 설정에 따라 달라지는 UI관리자만 보이는 메뉴, 권한별 버튼 표시
에러/엣지 케이스사용자가 흔히 마주하는 에러 상황404 페이지, 빈 데이터 상태, 네트워크 에러

Nice-to-Have: 여유가 있을 때

기준설명예시
시각적 상태로딩/빈 상태/에러 상태 UI 표시스켈레톤 표시 후 데이터 렌더링
반응형/레이아웃사이드바 접기/펼치기, 반응형 동작모바일 뷰포트에서 네비게이션
접근성키보드 네비게이션, 포커스 관리Tab 키로 메뉴 탐색, 모달 포커스 트랩

시나리오 유형별 분류

E2E 테스트 시나리오는 크게 5가지 유형으로 분류할 수 있다.

유형 1: Smoke Test (연기 테스트)

"모든 페이지가 최소한으로 동작하는가?"

가장 기본적이면서 가장 가성비가 높은 테스트. 모든 주요 페이지에 접속하여 에러 없이 로드되는지만 확인한다.

typescript
const pages = [
  { path: '/dashboard', name: '대시보드' },
  { path: '/users', name: '사용자 관리' },
  { path: '/settings', name: '설정' },
]

for (const { path, name } of pages) {
  test(`${name} 페이지가 로드된다`, async ({ page }) => {
    await page.goto(path)
    await expect(page).toHaveURL(new RegExp(path))
    // 최소한의 콘텐츠 존재 확인
    await expect(page.getByRole('navigation')).toBeVisible()
  })
}
🔥
Smoke Test는 가장 먼저 작성해야 할 테스트다. 5-10개의 Smoke Test만으로도 배포 후 치명적인 문제를 조기에 발견할 수 있다.

유형 2: Happy Path (정상 흐름)

"핵심 업무 흐름이 정상 동작하는가?"

사용자가 가장 빈번하게 수행하는 시나리오를 테스트한다. 에러 케이스는 제외하고 정상 동작만 검증한다.

typescript
test('데이터 목록에서 항목을 클릭하면 상세 정보가 표시된다', async ({ page }) => {
  await page.goto('/items')

  // 테이블 로드 확인
  await expect(page.getByRole('table')).toBeVisible()

  // 첫 번째 행 클릭
  await page.getByRole('row').nth(1).click()

  // 상세 패널 표시 확인
  await expect(page.getByRole('complementary')).toBeVisible()
  await expect(page.getByRole('heading', { level: 2 })).toBeVisible()
})

유형 3: Navigation & Routing (네비게이션)

"페이지 이동과 URL 라우팅이 정상 동작하는가?"

SPA에서 라우팅은 클라이언트가 담당하므로, 잘못된 라우팅이 발생해도 서버 에러가 나지 않아 발견이 어렵다.

typescript
test.describe('네비게이션', () => {
  test('사이드바에서 설정 페이지로 이동할 수 있다', async ({ page }) => {
    await page.goto('/dashboard')
    await page.getByRole('link', { name: /설정/i }).click()
    await expect(page).toHaveURL(/\/settings/)
  })

  test('존재하지 않는 URL로 접근하면 404 페이지가 표시된다', async ({ page }) => {
    await page.goto('/nonexistent-page')
    await expect(page.getByText(/찾을 수 없/i)).toBeVisible()
  })

  test('인증 없이 보호된 페이지에 접근하면 리다이렉트된다', async ({ browser }) => {
    // 인증 없는 새 컨텍스트
    const context = await browser.newContext()
    const page = await context.newPage()
    await page.goto('/dashboard')
    await expect(page).toHaveURL(/\/login/)
    await context.close()
  })
})

유형 4: Form Workflow (폼 워크플로우)

"폼 입력 → 유효성 검증 → 제출 → 결과 확인이 동작하는가?"

typescript
test('새 항목을 등록할 수 있다', async ({ page }) => {
  await page.goto('/items/new')

  // 폼 입력
  await page.getByLabel('이름').fill('테스트 항목')
  await page.getByLabel('카테고리').selectOption('A')

  // 제출
  await page.getByRole('button', { name: '저장' }).click()

  // 결과 확인
  await expect(page.getByText('저장되었습니다')).toBeVisible()
  await expect(page).toHaveURL(/\/items/)
})

유형 5: Guard & Permission (가드/권한)

"접근 제어와 권한 체계가 올바르게 동작하는가?"

멀티 테넌트 앱이나 권한 기반 앱에서 특히 중요하다.

typescript
test.describe('Feature Guard', () => {
  test('관리자 메뉴가 일반 사용자에게 숨겨진다', async ({ page }) => {
    await page.goto('/dashboard')
    await expect(page.getByRole('link', { name: /관리자 설정/i })).not.toBeVisible()
  })

  test('권한 없는 페이지에 직접 접근하면 접근 거부된다', async ({ page }) => {
    await page.goto('/admin/settings')
    await expect(page).not.toHaveURL(/\/admin\/settings/)
  })
})

우선순위 매트릭스

시나리오를 나열했다면, 이제 어떤 순서로 구현할지 정해야 한다.

P0 ~ P2 정의

등급의미기준목표 커버리지
P0필수서비스 핵심 동작, 깨지면 업무 불가100%
P1중요주요 사용자 흐름, 회귀 위험 높음80% 이상
P2권장편의 기능, UX 품질여유 시

추천 구현 순서

mermaid
flowchart LR
    A["Phase 1\nSmoke Test"] --> B["Phase 2\n네비게이션 + 가드"]
    B --> C["Phase 3\nHappy Path"]
    C --> D["Phase 4\n폼 + 인터랙션"]

Phase 1: Smoke Test

  • 모든 주요 페이지의 로드 확인
  • 인증 설정 (auth setup)
  • 5~10개 테스트로 시작

Phase 2: 네비게이션 & 가드

  • 사이드바 네비게이션 동작
  • URL 직접 접근 시 리다이렉트
  • 권한/역할별 접근 제어

Phase 3: Happy Path

  • 핵심 데이터 조회 흐름
  • 목록 → 상세 → 수정 흐름
  • 메인 테이블 로드 + 필터링

Phase 4: 폼 & 인터랙션

  • 데이터 생성/수정 폼 흐름
  • 테이블 정렬/페이지네이션
  • 에러 상태 처리
💡
Phase 1만 완성해도 배포 안정성이 크게 올라간다. 한 번에 Phase 4까지 하려고 하지 말고, 점진적으로 확장하자.

실전 팁: "이건 E2E로 테스트하지 마세요"

무엇을 테스트할지 정하는 것만큼, 무엇을 테스트하지 않을지 아는 것도 중요하다.

E2E로 테스트하지 말아야 할 것들

시나리오이유대안
유틸 함수의 엣지 케이스E2E로는 경우의 수를 커버하기 어렵고 느림단위 테스트 (Vitest/Jest)
컴포넌트의 props 조합N개의 조합을 브라우저에서 확인하는 건 비효율컴포넌트 테스트 (Testing Library)
API 응답의 스키마 검증프론트엔드 E2E가 아닌 API 테스트 영역API 테스트, 계약 테스트
CSS 픽셀 단위 검증환경마다 렌더링이 미세하게 다름시각적 회귀 테스트 (Chromatic 등)
동시성/레이스 컨디션E2E에서 재현이 어렵고 불안정단위 테스트 + 코드 리뷰

안티패턴 3가지

1. 하나의 테스트에 너무 많은 검증

typescript
// BAD: 하나의 테스트가 모든 것을 검증
test('대시보드 전체 검증', async ({ page }) => {
  await page.goto('/dashboard')
  await expect(page.getByText('환영합니다')).toBeVisible()
  await expect(page.getByRole('table')).toBeVisible()
  await page.getByRole('button', { name: '필터' }).click()
  await page.getByLabel('기간').selectOption('monthly')
  await page.getByRole('link', { name: '설정' }).click()
  await expect(page).toHaveURL(/settings/)
  // ... 50줄 더
})

// GOOD: 하나의 흐름 당 하나의 테스트
test('대시보드가 로드된다', async ({ page }) => {
  await page.goto('/dashboard')
  await expect(page.getByRole('heading', { name: /대시보드/i })).toBeVisible()
})

test('필터를 변경하면 데이터가 갱신된다', async ({ page }) => {
  await page.goto('/dashboard')
  await page.getByLabel('기간').selectOption('monthly')
  await expect(page.getByRole('table')).toBeVisible()
})

2. 테스트 간 상태 의존

typescript
// BAD: 두 번째 테스트가 첫 번째 테스트의 결과에 의존
test('항목 생성', async ({ page }) => { /* ... */ })
test('생성된 항목 삭제', async ({ page }) => { /* 위에서 생성된 항목이 있어야 함! */ })

// GOOD: 각 테스트가 독립적
test('항목 삭제', async ({ page }) => {
  // 테스트에 필요한 상태를 직접 설정
  await page.goto('/items/known-id')
  // ...
})

3. 불안정한 Locator 사용

typescript
// BAD: 구현 세부사항에 의존
page.locator('.MuiButton-containedPrimary')
page.locator('div:nth-child(3) > span')

// GOOD: 사용자 관점의 Locator
page.getByRole('button', { name: '저장' })
page.getByLabel('이메일')

시나리오 작성 템플릿

새 테스트 파일을 만들 때 참고할 수 있는 구조:

typescript
import { test, expect } from '@playwright/test'

test.describe('[기능명]', () => {
  // Smoke: 페이지 로드 확인 (P0)
  test('[기능] 페이지가 정상적으로 로드된다', async ({ page }) => {
    await page.goto('/[feature-path]')
    await expect(page).toHaveURL(/[feature-path]/)
    await expect(
      page.getByRole('heading', { name: /[페이지 제목]/i })
    ).toBeVisible()
  })

  // Happy Path: 핵심 인터랙션 (P1)
  test('[동작]하면 [결과]한다', async ({ page }) => {
    await page.goto('/[feature-path]')
    await page.getByRole('button', { name: /[동작]/i }).click()
    await expect(page.getByText('[기대 결과]')).toBeVisible()
  })

  // Edge Case: 예외 상황 (P2)
  test('데이터가 없을 때 빈 상태 메시지가 표시된다', async ({ page }) => {
    await page.goto('/[feature-path]?empty=true')
    await expect(page.getByText(/데이터가 없습니다/i)).toBeVisible()
  })
})

테스트 이름 작성법

typescript
// GOOD: 행동과 결과를 명확히
test('날짜 필터를 변경하면 테이블이 갱신된다', ...)
test('존재하지 않는 페이지에 접근하면 404가 표시된다', ...)
test('검색어를 입력하면 실시간으로 결과가 필터링된다', ...)

// BAD: 모호한 이름
test('테스트 1', ...)
test('필터 동작', ...)
test('페이지 테스트', ...)

파일 구조 가이드

plain text
e2e/
├── auth.setup.ts          ← 인증 설정 (1회 실행)
├── fixtures/
│   └── base.ts            ← 커스텀 fixture
├── helpers/
│   └── auth.ts            ← 인증 헬퍼
├── smoke.spec.ts          ← 전체 페이지 Smoke Test
├── navigation.spec.ts     ← 네비게이션 + 라우팅
├── dashboard.spec.ts      ← 대시보드 기능
├── user-management.spec.ts← 사용자 관리 기능
└── settings.spec.ts       ← 설정 기능

기능별로 파일을 분리하되, 하나의 파일이 너무 커지면 흐름별로 다시 분리한다:

plain text
# 파일이 커지면
user-management.spec.ts
↓
# 흐름별로 분리
user-management-list.spec.ts
user-management-form.spec.ts
user-management-permission.spec.ts

마치며

E2E 테스트 시나리오 선정은 "이 기능이 깨졌을 때 사용자가 얼마나 큰 영향을 받는가?"라는 질문으로 귀결된다.

정리하면:

  1. Smoke Test부터 시작하라 — 모든 페이지가 로드되는지 확인하는 것만으로도 가치가 크다
  2. 크리티컬 패스에 집중하라 — 서비스의 핵심 업무 흐름 1~3개를 우선 보호하라
  3. 점진적으로 확장하라 — 완벽한 커버리지가 아닌, 안정적인 소수의 테스트가 목표다
  4. 테스트 피라미드를 존중하라 — E2E는 비싸다. 단위/컴포넌트 테스트로 해결할 수 있는 건 그쪽에 맡겨라