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

Playwright 완벽 가이드 | E2E 테스트·자동화·CI/CD·Visual 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,
  });
});

정리 및 체크리스트

핵심 요약

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

구현 체크리스트

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

같이 보면 좋은 글

  • Vitest 완벽 가이드
  • GitHub Actions CI/CD 완벽 가이드
  • React 18 심화 가이드

이 글에서 다루는 키워드

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. 기본 사용법은 간단합니다. 공식 문서가 훌륭하고 예제가 많습니다.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3