본문으로 건너뛰기 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% 이상 사전에 발견한 경험을 공유합니다.

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

실무에서 마주치는 문제들

매번 수동으로 테스트해요

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

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

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"]', 'test@example.com');
  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('test@example.com');
// Placeholder 선택자
await page.getByPlaceholder('Enter your email').fill('test@example.com');
// 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"]', 'test@example.com');
  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. 기본 사용법은 간단합니다. 공식 문서가 훌륭하고 예제가 많습니다.