hongsoohyuk
홈이력서방명록프로젝트인스타그램블로그

© 2026 hongsoohyuk. All rights reserved.

GitHubLinkedIn
블로그/Playwright E2E 테스트 입문

Playwright E2E 테스트 입문

FrontendStudyTest

마지막 수정: 2026년 2월 24일

왜 E2E 테스트인가?

프론트엔드 개발을 하다 보면 "잘 되던 기능이 갑자기 안 된다"는 상황을 자주 마주한다. 컴포넌트 하나를 수정했을 뿐인데, 전혀 관련 없어 보이는 페이지에서 버그가 발생하기도 한다.

단위 테스트는 함수나 컴포넌트 단위의 동작을 검증하지만, 사용자가 실제로 경험하는 흐름 — 로그인하고, 페이지를 이동하고, 폼을 제출하는 — 을 검증하기에는 한계가 있다. 이 간극을 채워주는 것이 바로 E2E(End-to-End) 테스트.


테스트 피라미드 이해하기

테스트 전략을 세울 때 가장 먼저 이해해야 할 개념은 테스트 피라미드다.

층위도구 예시속도신뢰도비용
E2EPlaywright, Cypress느림높음높음
통합/컴포넌트Testing Library, Vitest보통보통보통
단위Vitest, Jest빠름낮음낮음

피라미드 아래로 갈수록 테스트 수가 많고, 위로 갈수록 적어야 한다. E2E 테스트는 핵심 사용자 흐름에 집중하는 것이 핵심이다.

💡
E2E 테스트는 "모든 것을 테스트하는 도구"가 아니다. 가장 중요한 Happy Path를 보호하는 안전망이다.

Playwright를 선택한 이유

E2E 테스트 도구는 여러 가지가 있지만, 2025년 현재 Playwright가 가장 강력한 선택지라고 생각한다.

Cypress vs Playwright 비교

항목CypressPlaywright
브라우저 지원Chrome 중심Chromium, Firefox, WebKit
병렬 실행유료(Cloud)내장 지원
멀티 탭/윈도우미지원지원
자동 대기지원지원
디버깅 도구Time TravelTrace Viewer, UI Mode, Codegen
언어JavaScriptJS, TS, Python, Java, C#

Playwright의 가장 큰 장점은 Auto-waiting 메커니즘과 Trace Viewer다. 테스트가 실패하면 각 스텝의 스크린샷, 네트워크 요청, DOM 스냅샷을 타임라인으로 확인할 수 있어 디버깅이 압도적으로 편하다.


프로젝트 설정

설치

bash
# Playwright 설치
npm install -D @playwright/test

# 브라우저 설치 (Chromium만 우선 설치)
npx playwright install --with-deps chromium

설정 파일

typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  reporter: [['html', { outputFolder: './e2e/.report' }]],

  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],

  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
})

핵심 포인트:

  • webServer: 테스트 전에 dev 서버를 자동으로 실행해준다
  • trace: 'on-first-retry': 실패 시 첫 번째 재시도에서 trace를 수집한다
  • fullyParallel: 테스트 파일 간 병렬 실행을 활성화한다

인증 처리 전략

SPA 앱에서 E2E 테스트의 가장 큰 난관은 인증이다. 매 테스트마다 로그인 페이지를 거치면 느리고 불안정해진다.

Playwright는 이 문제를 storageState로 해결한다.

storageState 패턴

javascript
1. Setup 프로젝트에서 1회 로그인
2. 쿠키/로컬스토리지를 JSON 파일로 저장
3. 이후 모든 테스트에서 해당 상태를 자동 복원
typescript
// playwright.config.ts
projects: [
  {
    name: 'auth-setup',
    testMatch: /auth\.setup\.ts/,
  },
  {
    name: 'chromium',
    use: {
      ...devices['Desktop Chrome'],
      storageState: './e2e/.auth/user.json',
    },
    dependencies: ['auth-setup'],
  },
]
typescript
// e2e/auth.setup.ts
import { test as setup } from '@playwright/test'

setup('authenticate', async ({ page, context }) => {
  // 로그인 수행
  await page.goto('/login')
  await page.getByLabel('이메일').fill('user@example.com')
  await page.getByLabel('비밀번호').fill('password')
  await page.getByRole('button', { name: '로그인' }).click()
  await page.waitForURL('/dashboard')

  // 인증 상태 저장
  await context.storageState({ path: './e2e/.auth/user.json' })
})
🔒
.auth/ 디렉토리는 반드시 .gitignore에 추가하자. 인증 토큰이 레포지토리에 커밋되면 보안 사고로 이어질 수 있다.

SSO 환경에서의 인증

외부 SSO를 사용하는 경우, 로그인 페이지를 직접 조작하기 어렵다. 이때는 쿠키를 직접 주입하는 방식을 사용한다:

typescript
setup('authenticate', async ({ context, page }) => {
  const token = process.env.E2E_AUTH_TOKEN

  await context.addCookies([{
    name: 'access_token',
    value: token,
    domain: 'localhost',
    path: '/',
    sameSite: 'Lax',
  }])

  await page.goto('/dashboard')
  await context.storageState({ path: './e2e/.auth/user.json' })
})

Locator 전략 — 테스트의 안정성을 결정하는 핵심

E2E 테스트가 깨지는 가장 흔한 원인은 불안정한 요소 선택이다.

나쁜 Locator

typescript
// CSS 클래스 — 스타일 변경 시 깨짐
page.locator('.btn-primary-large')

// DOM 구조 의존 — 레이아웃 변경 시 깨짐
page.locator('div > div:nth-child(3) > button')

// ID — 프레임워크가 자동 생성하면 불안정
page.locator('#radix-123')

좋은 Locator

typescript
// Role 기반 — 접근성과 테스트 안정성 동시에 확보
page.getByRole('button', { name: '저장' })
page.getByRole('link', { name: /대시보드/i })
page.getByRole('heading', { name: '설정' })

// Label 기반 — 폼 요소에 최적
page.getByLabel('이메일')
page.getByLabel('기간 선택')

// Text 기반 — 상태 메시지 등
page.getByText('저장되었습니다')
✅
Locator 우선순위: getByRole > getByLabel > getByText > getByPlaceholder > getByTestId

getByRole을 우선 사용하면, 자연스럽게 접근성(Accessibility)도 함께 검증하게 된다.


디버깅 도구 활용

UI Mode — 가장 강력한 디버깅 도구

bash
npx playwright test --ui

UI Mode에서 할 수 있는 것:

  • 테스트를 하나씩 선택하여 실행
  • 각 스텝의 전/후 스크린샷 비교
  • DOM 스냅샷에서 요소를 직접 클릭하여 Locator 확인
  • 네트워크 요청 타임라인 확인

Trace Viewer

테스트 실패 시 자동 수집되는 trace를 열어볼 수 있다:

bash
npx playwright show-trace trace.zip

Trace에는 스크린샷, 네트워크 로그, 콘솔 출력, DOM 스냅샷이 모두 포함된다. 실패 원인을 파악하는 가장 빠른 방법이다.

Codegen — 테스트 코드 자동 생성

bash
npx playwright codegen http://localhost:3000

브라우저에서 클릭, 입력 등을 하면 코드가 자동 생성된다. 처음 테스트를 작성할 때 좋은 시작점이 되지만, 생성된 코드는 반드시 시맨틱 Locator로 리팩토링해야 한다.


자주 하는 실수와 안티패턴

1. 하드코딩된 대기 시간

typescript
// BAD - 환경에 따라 불안정
await page.waitForTimeout(3000)

// GOOD - 조건이 충족되면 즉시 통과
await expect(page.getByText('로딩 완료')).toBeVisible()

Playwright의 assertion은 자동으로 재시도한다 (기본 5초). waitForTimeout은 거의 필요 없다.

2. 테스트 간 상태 의존

typescript
// BAD - 테스트 순서에 의존
test('아이템 생성', async ({ page }) => { /* ... */ })
test('생성된 아이템 수정', async ({ page }) => { /* 위 테스트에 의존! */ })

// GOOD - 각 테스트가 독립적
test('아이템 수정', async ({ page }) => {
  // 테스트에 필요한 상태를 직접 설정
  await page.goto('/items/123/edit')
  // ...
})

3. 모든 것을 E2E로 테스트하려는 시도

E2E 테스트는 비용이 높다. 유틸 함수의 엣지 케이스는 단위 테스트로, 컴포넌트의 상태 변화는 컴포넌트 테스트로 검증하고, E2E는 핵심 사용자 흐름에만 집중하자.


Custom Fixture로 테스트 확장하기

Playwright의 Fixture 시스템은 테스트에 필요한 공통 컨텍스트를 주입하는 강력한 메커니즘이다.

typescript
// e2e/fixtures/base.ts
import { test as base } from '@playwright/test'

export const test = base.extend<{
  entityId: string
}>({
  entityId: ['gcp', { option: true }],
})

export { expect } from '@playwright/test'
typescript
// e2e/dashboard.spec.ts
import { test, expect } from './fixtures/base'

test('대시보드 로드', async ({ page, entityId }) => {
  await page.goto(`/${entityId}/dashboard`)
  await expect(page).toHaveURL(new RegExp(`/${entityId}/dashboard`))
})

이렇게 하면 멀티 테넌트 앱에서 엔티티별 테스트를 일관되게 작성할 수 있다.


마치며

E2E 테스트는 도입 비용이 있지만, 한 번 안정적으로 구축하면 리팩토링과 기능 추가에 대한 자신감을 크게 높여준다.

시작할 때 추천하는 접근법:

  1. 가장 중요한 사용자 흐름 1-2개만 먼저 테스트한다
  2. storageState로 인증을 한 번에 해결한다
  3. getByRole 중심의 안정적인 Locator를 사용한다
  4. UI Mode와 Trace Viewer를 적극 활용한다

완벽한 커버리지를 목표로 하기보다, 깨지지 않는 소수의 테스트로 시작하는 것이 핵심이다.