본문으로 건너뛰기
Previous
Next
Playwright 완벽 가이드 | E2E 테스트·자동화·CI/CD·Visual Testing

Playwright 완벽 가이드 | E2E 테스트·자동화·CI/CD·Visual Testing

Playwright 완벽 가이드 | E2E 테스트·자동화·CI/CD·Visual Testing

이 글의 핵심

Playwright로 E2E 테스트를 자동화하는 완벽 가이드. 설치, 테스트 작성, 선택자, API 모킹, CI/CD, Visual Testing까지 실전 예제로 정리. Playwright·E2E Testing·Testing 중심으로 설명합니다.

이 글의 핵심

Playwright로 E2E 테스트를 자동화하는 완벽 가이드입니다. 설치부터 테스트 작성, 선택자, API 모킹, CI/CD, Visual Testing까지 실전 예제로 정리했습니다.

실무 경험 공유: 이커머스 플랫폼에 Playwright를 도입하면서, 수동 QA 시간을 80% 줄이고 배포 전 버그를 95% 이상 사전에 발견한 경험을 공유합니다.

들어가며: “수동 테스트가 너무 많아요”

실무 문제 시나리오

시나리오 1: 매번 수동으로 테스트해요

배포 전 수동 테스트에 2시간 걸립니다. Playwright는 5분입니다. 시나리오 2: 브라우저마다 다르게 동작해요

Chrome, Firefox, Safari를 각각 테스트해야 합니다. Playwright는 자동으로 모두 테스트합니다. 시나리오 3: 테스트가 불안정해요

Selenium 테스트가 자주 실패합니다. Playwright는 안정적입니다.

1. Playwright란?

핵심 특징

Playwright는 Microsoft가 만든 E2E 테스트 프레임워크입니다. 주요 장점:

  • 크로스 브라우저: Chromium, Firefox, WebKit 지원
  • 자동 대기: 요소가 준비될 때까지 자동 대기
  • 병렬 실행: 빠른 테스트 실행
  • 강력한 선택자: CSS, XPath, Text, Role 등
  • API 모킹: 네트워크 요청 가로채기

2. 설치 및 설정

설치

npm init playwright@latest

설정 파일

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

3. 기본 테스트 작성

첫 번째 테스트

// tests/example.spec.ts
import { test, expect } from '@playwright/test';
test('홈페이지 제목 확인', async ({ page }) => {
  await page.goto('/');
  
  await expect(page).toHaveTitle(/My App/);
});
test('로그인 테스트', async ({ page }) => {
  await page.goto('/login');
  
  // 입력
  await page.fill('input[name="email"]', '[email protected]');
  await page.fill('input[name="password"]', 'password123');
  
  // 버튼 클릭
  await page.click('button[type="submit"]');
  
  // 리다이렉트 확인
  await expect(page).toHaveURL('/dashboard');
  
  // 환영 메시지 확인
  await expect(page.locator('h1')).toContainText('Welcome');
});

4. 선택자 (Locators)

다양한 선택자

// CSS 선택자
await page.locator('.submit-button').click();
// Text 선택자
await page.locator('text=Submit').click();
// Role 선택자 (권장)
await page.getByRole('button', { name: 'Submit' }).click();
// Label 선택자
await page.getByLabel('Email').fill('[email protected]');
// Placeholder 선택자
await page.getByPlaceholder('Enter your email').fill('[email protected]');
// Test ID 선택자
await page.getByTestId('submit-button').click();

체이닝

// 부모 → 자식
await page
  .locator('.card')
  .filter({ hasText: 'Premium' })
  .getByRole('button', { name: 'Buy' })
  .click();
// 여러 요소 처리
const items = page.locator('.item');
const count = await items.count();
for (let i = 0; i < count; i++) {
  const text = await items.nth(i).textContent();
  console.log(text);
}

5. 상호작용

클릭 및 입력

// 클릭
await page.click('button');
await page.dblclick('button');  // 더블 클릭
await page.click('button', { button: 'right' });  // 우클릭
// 입력
await page.fill('input', 'text');
await page.type('input', 'text', { delay: 100 });  // 천천히 입력
// 선택
await page.selectOption('select', 'value');
await page.check('input[type="checkbox"]');
await page.uncheck('input[type="checkbox"]');
// 파일 업로드
await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');

키보드 및 마우스

// 키보드
await page.keyboard.press('Enter');
await page.keyboard.press('Control+A');
await page.keyboard.type('Hello World');
// 마우스
await page.mouse.move(100, 200);
await page.mouse.click(100, 200);
await page.mouse.wheel(0, 100);  // 스크롤

6. 대기 및 검증

자동 대기

// Playwright는 자동으로 대기합니다
await page.click('button');  // 버튼이 클릭 가능할 때까지 대기
// 명시적 대기
await page.waitForSelector('.result');
await page.waitForURL('**/dashboard');
await page.waitForLoadState('networkidle');

Assertions

// 텍스트 검증
await expect(page.locator('h1')).toHaveText('Welcome');
await expect(page.locator('h1')).toContainText('Wel');
// 속성 검증
await expect(page.locator('button')).toBeDisabled();
await expect(page.locator('button')).toBeEnabled();
await expect(page.locator('button')).toBeVisible();
// 개수 검증
await expect(page.locator('.item')).toHaveCount(5);
// URL 검증
await expect(page).toHaveURL(/dashboard/);
// 스크린샷 비교
await expect(page).toHaveScreenshot();

7. API 모킹

네트워크 요청 가로채기

test('API 모킹', async ({ page }) => {
  // API 응답 모킹
  await page.route('**/api/users', (route) => {
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([
        { id: 1, name: 'John' },
        { id: 2, name: 'Jane' },
      ]),
    });
  });
  await page.goto('/users');
  
  // 모킹된 데이터 확인
  await expect(page.locator('.user')).toHaveCount(2);
});

요청 대기

test('API 요청 대기', async ({ page }) => {
  await page.goto('/');
  
  // API 요청 대기
  const responsePromise = page.waitForResponse('**/api/data');
  await page.click('button');
  const response = await responsePromise;
  
  expect(response.status()).toBe(200);
  const data = await response.json();
  expect(data).toHaveProperty('id');
});

8. 인증 및 세션

로그인 재사용

// tests/auth.setup.ts
import { test as setup } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.fill('input[name="email"]', '[email protected]');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');
  
  await page.waitForURL('/dashboard');
  
  // 세션 저장
  await page.context().storageState({ path: authFile });
});
// playwright.config.ts
export default defineConfig({
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],
});

9. 실전 예제: 이커머스 테스트

// tests/e-commerce.spec.ts
import { test, expect } from '@playwright/test';
test.describe('이커머스 플로우', () => {
  test('상품 검색 → 장바구니 → 결제', async ({ page }) => {
    // 1. 홈페이지 방문
    await page.goto('/');
    
    // 2. 상품 검색
    await page.fill('input[placeholder="Search"]', 'laptop');
    await page.press('input[placeholder="Search"]', 'Enter');
    
    // 3. 검색 결과 확인
    await expect(page.locator('.product')).toHaveCount(10);
    
    // 4. 첫 번째 상품 클릭
    await page.locator('.product').first().click();
    
    // 5. 상품 상세 페이지
    await expect(page.locator('h1')).toContainText('Laptop');
    
    // 6. 장바구니 추가
    await page.click('button:has-text("Add to Cart")');
    
    // 7. 장바구니 확인
    await expect(page.locator('.cart-count')).toHaveText('1');
    
    // 8. 장바구니 페이지로 이동
    await page.click('.cart-icon');
    await expect(page).toHaveURL(/cart/);
    
    // 9. 결제 진행
    await page.click('button:has-text("Checkout")');
    
    // 10. 배송 정보 입력
    await page.fill('input[name="name"]', 'John Doe');
    await page.fill('input[name="address"]', '123 Main St');
    await page.fill('input[name="city"]', 'New York');
    await page.fill('input[name="zip"]', '10001');
    
    // 11. 결제 정보 입력
    await page.fill('input[name="cardNumber"]', '4242424242424242');
    await page.fill('input[name="expiry"]', '12/25');
    await page.fill('input[name="cvc"]', '123');
    
    // 12. 주문 완료
    await page.click('button:has-text("Place Order")');
    
    // 13. 주문 확인 페이지
    await expect(page).toHaveURL(/order-confirmation/);
    await expect(page.locator('h1')).toContainText('Thank you');
  });
});

10. CI/CD 통합

GitHub Actions

# .github/workflows/playwright.yml
name: Playwright Tests
on:
  push:
    branches: [main]
  pull_request:
jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Install Playwright Browsers
        run: npx playwright install --with-deps
      
      - name: Run Playwright tests
        run: npx playwright test
      
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

11. Visual Testing

스크린샷 비교

test('비주얼 테스트', async ({ page }) => {
  await page.goto('/');
  
  // 전체 페이지 스크린샷
  await expect(page).toHaveScreenshot('homepage.png');
  
  // 특정 요소 스크린샷
  await expect(page.locator('.hero')).toHaveScreenshot('hero.png');
  
  // 임계값 설정
  await expect(page).toHaveScreenshot('homepage.png', {
    maxDiffPixels: 100,
  });
});

심화: 러너 아키텍처·CDP·E2E 커버리지·프로덕션

테스트 러너와 브라우저 컨텍스트

@playwright/test는 워커 프로세스로 스펙을 나누고, 브라우저 인스턴스를 띄운 뒤 BrowserContext 단위로 쿠키·스토리지를 격리합니다. 페이지(page)는 컨텍스트 안에서 생성되며, Chromium 계열에는 Chrome DevTools Protocol을 통해 명령이 전달됩니다. 이 구조가 trace·스크린샷·네트워크 가로채기를 동급으로 지원하는 이유입니다.

네트워크 모킹과 대기

page.route로 응답을 고정하거나 waitForResponse로 실제 요청 완료를 기다릴 수 있습니다. 고정 sleep 대신 로케이터 자동 대기 + 네트워크 동기화를 쓰면 플레이크가 줄어듭니다.

E2E와 커버리지

E2E는 단위 테스트처럼 Istanbul 라인 커버리지를 목표로 두기보다, 사용자 여정 커버리지(핵심 플로우 목록) 관리가 실무에 가깝습니다. 프런트 번들 계측이 필요하면 별도 파이프라인에서 병합·해석 비용을 감수해야 합니다.

프로덕션 검증

Playwright는 보통 스테이징·프리뷰 URL 대상으로 돌리며, 프로덕션에는 읽기 전용 스모크·합성 모니터링을 제한적으로 사용합니다. 영문 심화 요약은 [Playwright E2E Testing Guide (English)](/en/blog/playwright-e2e-testing-guide/를 참고하십시오.


정리 및 체크리스트

핵심 요약

  • Playwright: Microsoft의 E2E 테스트 프레임워크
  • 크로스 브라우저: Chromium, Firefox, WebKit 지원
  • 자동 대기: 안정적인 테스트
  • API 모킹: 네트워크 요청 제어
  • Visual Testing: 스크린샷 비교

구현 체크리스트

  • Playwright 설치 및 설정
  • 기본 테스트 작성
  • 선택자 최적화
  • API 모킹 구현
  • 인증 세션 재사용
  • CI/CD 통합
  • Visual Testing 추가

같이 보면 좋은 글


이 글에서 다루는 키워드

Playwright, E2E Testing, Testing, Automation, CI/CD, QA, Frontend

자주 묻는 질문 (FAQ)

Q. Playwright vs Cypress, 어떤 게 나은가요?

A. Playwright가 더 빠르고 크로스 브라우저를 지원합니다. Cypress는 DX가 좋지만 Chromium 계열만 지원합니다.

Q. Selenium에서 마이그레이션이 어렵나요?

A. Playwright API가 더 간단합니다. 자동 대기 기능으로 안정성이 크게 향상됩니다.

Q. 모바일 테스트를 할 수 있나요?

A. 네, 모바일 브라우저 에뮬레이션을 지원합니다. 실제 디바이스는 Appium을 사용하세요.

Q. 학습 곡선이 가파른가요?

A. 기본 사용법은 간단합니다. 공식 문서가 훌륭하고 예제가 많습니다.

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「Playwright 완벽 가이드 | E2E 테스트·자동화·CI/CD·Visual Testing」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「Playwright 완벽 가이드 | E2E 테스트·자동화·CI/CD·Visual Testing」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.