Playwright 완벽 가이드: 차세대 E2E 테스팅 도구
이 글의 핵심
Playwright는 Microsoft가 개발한 차세대 E2E 테스팅 프레임워크로 Chromium, Firefox, WebKit을 모두 지원합니다. 자동 대기, 병렬 실행, 강력한 디버깅 도구로 Selenium/Cypress 대비 높은 생산성을 제공합니다. Trace Viewer, Codegen, Inspector 등의 도구로 테스트 작성 및 디버깅이 매우 편리합니다.
Playwright란?
Playwright는 Microsoft가 개발한 오픈소스 E2E(End-to-End) 테스팅 프레임워크입니다. 2020년 출시 이후 빠르게 성장하며 Selenium, Cypress와 함께 3대 웹 테스팅 도구로 자리잡았습니다.
핵심 특징
-
멀티 브라우저 지원
- Chromium, Firefox, WebKit 모두 지원
- 브라우저별 자동 설치 및 관리
- 실제 브라우저 엔진 사용
-
자동 대기(Auto-waiting)
- 요소가 준비될 때까지 자동 대기
- 명시적
sleep()불필요 - Flaky 테스트 최소화
-
병렬 실행
- Worker 기반 병렬 테스트
- Sharding으로 분산 실행
- 빠른 테스트 실행 시간
-
강력한 디버깅 도구
- Trace Viewer: 타임라인 기반 디버깅
- Codegen: 자동 테스트 코드 생성
- Inspector: 스텝별 실행 및 디버깅
Selenium vs Cypress vs Playwright 비교
| 항목 | Selenium | Cypress | Playwright |
|---|---|---|---|
| 브라우저 지원 | Chrome, Firefox, Safari, Edge | Chrome, Edge, Firefox (베타) | Chromium, Firefox, WebKit |
| 언어 지원 | Java, Python, C#, JS 등 | JavaScript/TypeScript | JavaScript, TypeScript, Python, C#, Java |
| 자동 대기 | ❌ 수동 구현 필요 | ✅ 자동 | ✅ 자동 |
| 병렬 실행 | ⚠️ 외부 도구 필요 | ⚠️ 유료 플랜 | ✅ 기본 지원 |
| 탭/윈도우 전환 | ✅ | ❌ | ✅ |
| 네트워크 모킹 | ❌ | ✅ | ✅ |
| 비디오 녹화 | ❌ | ✅ | ✅ |
| 학습 곡선 | 높음 | 낮음 | 중간 |
| CI/CD 통합 | ✅ | ✅ | ✅ |
설치 및 초기 설정
Playwright를 시작하는 가장 쉬운 방법은 공식 CLI를 사용하는 것입니다. 대화형 설치 프로세스가 프로젝트 설정을 자동으로 처리해주므로 몇 분 만에 테스트를 작성할 수 있습니다.
프로젝트 생성
npm init playwright@latest 명령어는 프로젝트를 자동으로 설정합니다. TypeScript를 사용할지, 테스트 폴더를 어디에 둘지, CI/CD 설정을 추가할지 등을 대화형으로 선택할 수 있습니다. 모든 설정이 완료되면 Chromium, Firefox, WebKit 브라우저가 자동으로 다운로드됩니다.
브라우저 다운로드는 시간이 걸릴 수 있지만, 한 번만 하면 되고 이후에는 로컬에서 빠르게 실행됩니다. 각 브라우저는 약 100-300MB 정도의 용량을 차지합니다.
# npm으로 설치 (권장)
npm init playwright@latest
# 대화형 설정
# - TypeScript 사용 여부
# - 테스트 폴더 위치
# - GitHub Actions 워크플로우 추가 여부
# - Playwright 브라우저 설치
수동 설치
기존 프로젝트에 Playwright를 추가하거나 더 세밀한 제어가 필요한 경우 수동으로 설치할 수 있습니다. 먼저 패키지를 설치하고, 그 다음 브라우저를 설치합니다.
npm install -D @playwright/test
npx playwright install
프로젝트 구조
my-project/
├── tests/
│ ├── example.spec.ts
│ └── auth.spec.ts
├── playwright.config.ts
├── package.json
└── .gitignore
기본 테스트 작성
첫 번째 테스트
// tests/example.spec.ts
import { test, expect } from '@playwright/test';
test('기본 네비게이션 테스트', async ({ page }) => {
// 페이지 이동
await page.goto('https://playwright.dev/');
// 제목 확인
await expect(page).toHaveTitle(/Playwright/);
// 링크 클릭
await page.getByRole('link', { name: 'Get started' }).click();
// URL 확인
await expect(page).toHaveURL(/.*intro/);
});
Locator 사용법
test('다양한 Locator 방법', async ({ page }) => {
await page.goto('https://example.com');
// Role 기반 (권장)
await page.getByRole('button', { name: '제출' }).click();
// Text 기반
await page.getByText('로그인').click();
// Label 기반 (폼 요소)
await page.getByLabel('이메일').fill('[email protected]');
// Placeholder 기반
await page.getByPlaceholder('검색어를 입력하세요').fill('playwright');
// TestId 기반 (안정적)
await page.getByTestId('submit-button').click();
// CSS Selector
await page.locator('.submit-btn').click();
// XPath (비권장)
await page.locator('xpath=//button[@type="submit"]').click();
});
자동 대기의 힘
test('자동 대기 예제', async ({ page }) => {
await page.goto('https://example.com');
// 요소가 나타날 때까지 자동 대기
await page.getByRole('button', { name: '로드' }).click();
// 로딩 완료 후 요소가 visible할 때까지 대기
const result = page.getByTestId('result');
await expect(result).toBeVisible();
await expect(result).toHaveText('완료');
// 명시적 sleep() 불필요!
// Playwright가 알아서 대기합니다
});
고급 테스트 패턴
Page Object Model (POM)
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('이메일');
this.passwordInput = page.getByLabel('비밀번호');
this.submitButton = page.getByRole('button', { name: '로그인' });
this.errorMessage = page.getByTestId('error-message');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async getErrorMessage() {
return await this.errorMessage.textContent();
}
}
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('로그인 실패 시 에러 메시지 표시', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('[email protected]', 'wrongpassword');
const error = await loginPage.getErrorMessage();
expect(error).toContain('로그인 실패');
});
Fixtures로 재사용 가능한 Setup
// fixtures/auth.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
type AuthFixtures = {
authenticatedPage: Page;
};
export const test = base.extend<AuthFixtures>({
authenticatedPage: async ({ page }, use) => {
// 모든 테스트 전에 로그인
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('[email protected]', 'password123');
// 로그인된 상태로 테스트 실행
await use(page);
// 테스트 후 정리 작업 (옵션)
},
});
// tests/dashboard.spec.ts
import { test } from '../fixtures/auth';
import { expect } from '@playwright/test';
test('대시보드 접근 (인증 필요)', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/dashboard');
await expect(authenticatedPage).toHaveURL('/dashboard');
await expect(authenticatedPage.getByRole('heading', { name: '대시보드' })).toBeVisible();
});
API 테스트
test('REST API 테스트', async ({ request }) => {
// GET 요청
const response = await request.get('https://api.example.com/users/1');
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const user = await response.json();
expect(user.id).toBe(1);
expect(user.email).toBe('[email protected]');
// POST 요청
const createResponse = await request.post('https://api.example.com/users', {
data: {
name: 'New User',
email: '[email protected]'
}
});
expect(createResponse.ok()).toBeTruthy();
const newUser = await createResponse.json();
expect(newUser.id).toBeDefined();
});
test('E2E와 API 혼합 테스트', async ({ page, request }) => {
// API로 테스트 데이터 준비
const response = await request.post('https://api.example.com/posts', {
data: { title: '테스트 포스트', content: '내용...' }
});
const post = await response.json();
// UI로 확인
await page.goto(`/posts/${post.id}`);
await expect(page.getByRole('heading')).toHaveText('테스트 포스트');
});
네트워크 모킹 및 인터셉트
test('API 응답 모킹', async ({ page }) => {
// API 요청 인터셉트
await page.route('**/api/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
users: [
{ id: 1, name: 'Mock User 1' },
{ id: 2, name: 'Mock User 2' }
]
})
});
});
await page.goto('/users');
await expect(page.getByText('Mock User 1')).toBeVisible();
});
test('네트워크 요청 감시', async ({ page }) => {
// 특정 요청 대기
const responsePromise = page.waitForResponse('**/api/data');
await page.goto('/');
await page.getByRole('button', { name: '데이터 로드' }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.items).toHaveLength(10);
});
Playwright 설정 최적화
playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// 테스트 파일 위치
testDir: './tests',
// 테스트 타임아웃 (30초)
timeout: 30 * 1000,
// 각 expect 타임아웃 (5초)
expect: {
timeout: 5000
},
// 실패 시 재시도
retries: process.env.CI ? 2 : 0,
// 병렬 실행 워커 수
workers: process.env.CI ? 1 : undefined,
// 리포터 설정
reporter: [
['html'],
['json', { outputFile: 'test-results.json' }],
['junit', { outputFile: 'test-results.xml' }]
],
// 모든 프로젝트 공통 설정
use: {
// Base URL
baseURL: 'http://localhost:3000',
// 실패 시 스크린샷
screenshot: 'only-on-failure',
// 실패 시 비디오
video: 'retain-on-failure',
// Trace 수집
trace: 'on-first-retry',
// 브라우저 옵션
viewport: { width: 1280, height: 720 },
ignoreHTTPSErrors: true,
// 헤드리스 모드
headless: true,
},
// 브라우저별 프로젝트
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'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
// 로컬 개발 서버 자동 시작
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
});
CI/CD 통합
GitHub Actions
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
- 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
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: test-results/
retention-days: 30
Docker 통합
# Dockerfile
FROM mcr.microsoft.com/playwright:v1.40.0-jammy
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test"]
# docker-compose.yml
version: '3.8'
services:
playwright:
build: .
volumes:
- ./tests:/app/tests
- ./test-results:/app/test-results
- ./playwright-report:/app/playwright-report
environment:
- CI=true
GitLab CI
# .gitlab-ci.yml
image: mcr.microsoft.com/playwright:v1.40.0-jammy
stages:
- test
playwright-tests:
stage: test
script:
- npm ci
- npx playwright install
- npx playwright test
artifacts:
when: always
paths:
- playwright-report/
- test-results/
expire_in: 1 week
cache:
paths:
- node_modules/
디버깅 도구
Playwright Inspector
# 디버그 모드로 테스트 실행
npx playwright test --debug
# 특정 테스트만 디버그
npx playwright test example.spec.ts --debug
# 특정 라인부터 디버그
npx playwright test example.spec.ts:10 --debug
Trace Viewer
// 코드에서 trace 수동 시작
test('trace 예제', async ({ page }) => {
await page.context().tracing.start({ screenshots: true, snapshots: true });
await page.goto('https://example.com');
await page.getByRole('button', { name: '제출' }).click();
await page.context().tracing.stop({ path: 'trace.zip' });
});
# Trace 파일 열기
npx playwright show-trace trace.zip
Codegen - 자동 테스트 생성
# URL에서 테스트 자동 생성
npx playwright codegen https://example.com
# 특정 브라우저로 생성
npx playwright codegen --browser=webkit https://example.com
# 모바일 기기 에뮬레이션
npx playwright codegen --device="iPhone 12" https://example.com
# 인증 상태 저장
npx playwright codegen --save-storage=auth.json https://example.com
UI Mode (대화형 실행)
# UI 모드로 테스트 실행
npx playwright test --ui
# 특정 프로젝트만
npx playwright test --ui --project=chromium
비주얼 리그레션 테스팅
스크린샷 비교
test('비주얼 리그레션 테스트', async ({ page }) => {
await page.goto('/');
// 전체 페이지 스크린샷
await expect(page).toHaveScreenshot('homepage.png');
// 특정 요소만 스크린샷
const header = page.getByRole('banner');
await expect(header).toHaveScreenshot('header.png');
// 픽셀 차이 허용 (0-1 사이)
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 100
});
});
반응형 테스트
const viewports = [
{ width: 375, height: 667, name: 'mobile' },
{ width: 768, height: 1024, name: 'tablet' },
{ width: 1920, height: 1080, name: 'desktop' }
];
for (const viewport of viewports) {
test(`${viewport.name} 레이아웃 테스트`, async ({ page }) => {
await page.setViewportSize(viewport);
await page.goto('/');
await expect(page).toHaveScreenshot(`homepage-${viewport.name}.png`);
});
}
성능 및 최적화
병렬 실행 최적화
// playwright.config.ts
export default defineConfig({
// CPU 코어 수만큼 워커 사용
workers: '50%',
// 또는 고정 숫자
workers: 4,
// 테스트 간 격리
fullyParallel: true,
});
Sharding (분산 실행)
# 4개 머신에 분산
npx playwright test --shard=1/4
npx playwright test --shard=2/4
npx playwright test --shard=3/4
npx playwright test --shard=4/4
테스트 그룹화
// 순차 실행이 필요한 테스트
test.describe.serial('순차 실행 그룹', () => {
test('첫 번째 테스트', async ({ page }) => {
// ...
});
test('두 번째 테스트', async ({ page }) => {
// 첫 번째 테스트 완료 후 실행
});
});
// 병렬 실행 강제
test.describe.parallel('병렬 실행 그룹', () => {
test('테스트 A', async ({ page }) => {
// ...
});
test('테스트 B', async ({ page }) => {
// 동시 실행
});
});
실전 사용 사례
E-Commerce 체크아웃 플로우
test('전체 구매 플로우', async ({ page }) => {
// 1. 상품 페이지 이동
await page.goto('/products/laptop');
await expect(page.getByRole('heading')).toContain('노트북');
// 2. 장바구니 추가
await page.getByRole('button', { name: '장바구니 담기' }).click();
await expect(page.getByText('장바구니에 추가되었습니다')).toBeVisible();
// 3. 장바구니 확인
await page.getByRole('link', { name: '장바구니' }).click();
await expect(page.getByTestId('cart-item')).toHaveCount(1);
// 4. 체크아웃
await page.getByRole('button', { name: '주문하기' }).click();
// 5. 배송 정보 입력
await page.getByLabel('이름').fill('홍길동');
await page.getByLabel('주소').fill('서울시 강남구');
await page.getByLabel('전화번호').fill('010-1234-5678');
// 6. 결제 정보 입력
await page.getByLabel('카드번호').fill('1234-5678-9012-3456');
await page.getByLabel('유효기간').fill('12/25');
await page.getByLabel('CVV').fill('123');
// 7. 주문 완료
await page.getByRole('button', { name: '결제하기' }).click();
// 8. 주문 확인 페이지
await expect(page).toHaveURL(/.*order-complete/);
await expect(page.getByText('주문이 완료되었습니다')).toBeVisible();
// 9. 주문번호 추출
const orderNumber = await page.getByTestId('order-number').textContent();
expect(orderNumber).toMatch(/ORD-\d+/);
});
무한 스크롤 테스트
test('무한 스크롤 리스트', async ({ page }) => {
await page.goto('/posts');
let previousCount = 0;
const maxScrolls = 5;
for (let i = 0; i < maxScrolls; i++) {
// 현재 아이템 수
const items = page.getByTestId('post-item');
const currentCount = await items.count();
expect(currentCount).toBeGreaterThan(previousCount);
previousCount = currentCount;
// 페이지 끝까지 스크롤
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
// 새 아이템 로딩 대기
await page.waitForTimeout(1000);
}
// 최소 아이템 수 확인
const finalCount = await page.getByTestId('post-item').count();
expect(finalCount).toBeGreaterThanOrEqual(30);
});
파일 업로드 및 다운로드
test('파일 업로드', async ({ page }) => {
await page.goto('/upload');
// 파일 선택
const fileInput = page.getByLabel('파일 선택');
await fileInput.setInputFiles('test-files/sample.pdf');
// 업로드 실행
await page.getByRole('button', { name: '업로드' }).click();
// 성공 메시지
await expect(page.getByText('업로드 완료')).toBeVisible();
});
test('파일 다운로드', async ({ page }) => {
await page.goto('/downloads');
// 다운로드 시작
const downloadPromise = page.waitForEvent('download');
await page.getByRole('link', { name: '보고서 다운로드' }).click();
const download = await downloadPromise;
// 파일명 확인
expect(download.suggestedFilename()).toBe('report.pdf');
// 파일 저장
await download.saveAs('downloads/' + download.suggestedFilename());
});
베스트 프랙티스
1. Locator 우선순위
// ✅ 좋음: Role 기반 (접근성 우선)
await page.getByRole('button', { name: '제출' });
// ✅ 좋음: TestId (안정적)
await page.getByTestId('submit-button');
// ⚠️ 주의: Text 기반 (번역 시 변경)
await page.getByText('제출');
// ❌ 나쁨: CSS Selector (취약함)
await page.locator('.btn.btn-primary');
2. 명시적 Assertion 사용
// ✅ 좋음: 명확한 기대값
await expect(page.getByRole('alert')).toHaveText('저장되었습니다');
await expect(page).toHaveURL('/dashboard');
// ❌ 나쁨: 불명확한 대기
await page.waitForTimeout(3000);
3. 재사용 가능한 Helper 함수
// utils/helpers.ts
export async function login(page: Page, email: string, password: string) {
await page.goto('/login');
await page.getByLabel('이메일').fill(email);
await page.getByLabel('비밀번호').fill(password);
await page.getByRole('button', { name: '로그인' }).click();
await expect(page).toHaveURL('/dashboard');
}
// 사용
import { login } from './utils/helpers';
test('대시보드 테스트', async ({ page }) => {
await login(page, '[email protected]', 'password');
// 테스트 로직...
});
4. 환경별 설정 분리
// playwright.config.ts
const config = {
development: {
baseURL: 'http://localhost:3000',
workers: 3,
},
staging: {
baseURL: 'https://staging.example.com',
workers: 2,
},
production: {
baseURL: 'https://example.com',
workers: 1,
}
};
const env = process.env.TEST_ENV || 'development';
export default defineConfig({
...config[env],
// 공통 설정...
});
실전 사례: 전자상거래 체크아웃 테스트
실무에서 가장 중요한 테스트 시나리오 중 하나는 결제 프로세스입니다. 여러 단계를 거쳐야 하고, 네트워크 요청이 많으며, 타이밍 이슈가 발생하기 쉽습니다. Playwright의 자동 대기와 강력한 assertion으로 안정적인 테스트를 작성할 수 있습니다.
장바구니에서 결제까지 전체 플로우
test('체크아웃 전체 플로우', async ({ page }) => {
// 1. 로그인
await page.goto('/login');
await page.getByLabel('이메일').fill('[email protected]');
await page.getByLabel('비밀번호').fill('password123');
await page.getByRole('button', { name: '로그인' }).click();
await expect(page).toHaveURL('/');
// 2. 상품 검색 및 장바구니 담기
await page.getByPlaceholder('검색').fill('노트북');
await page.getByRole('button', { name: '검색' }).click();
// 첫 번째 상품 클릭
await page.getByRole('link', { name: /노트북/ }).first().click();
await page.getByRole('button', { name: '장바구니 담기' }).click();
// 알림 확인
await expect(page.getByText('장바구니에 추가되었습니다')).toBeVisible();
// 3. 장바구니 확인
await page.getByRole('link', { name: '장바구니' }).click();
await expect(page.getByTestId('cart-item')).toHaveCount(1);
// 수량 변경
await page.getByLabel('수량').fill('2');
await expect(page.getByTestId('subtotal')).toContainText('3,000,000원');
// 4. 결제 진행
await page.getByRole('button', { name: '결제하기' }).click();
// 배송 정보 입력
await page.getByLabel('받는 사람').fill('홍길동');
await page.getByLabel('연락처').fill('010-1234-5678');
await page.getByLabel('주소').fill('서울시 강남구');
// 결제 수단 선택
await page.getByRole('radio', { name: '신용카드' }).check();
// 최종 결제
await page.getByRole('button', { name: '결제 완료' }).click();
// 결제 성공 확인
await expect(page).toHaveURL(/\/order\/success/);
await expect(page.getByText('주문이 완료되었습니다')).toBeVisible();
// 주문 번호 확인
const orderNumber = await page.getByTestId('order-number').textContent();
expect(orderNumber).toMatch(/ORD-\d+/);
});
이 테스트는 실제 사용자 플로우를 그대로 재현하며, 각 단계마다 적절한 검증을 포함합니다. Playwright의 자동 대기 덕분에 waitForTimeout 같은 취약한 대기 없이 안정적으로 동작합니다.
트러블슈팅
Flaky 테스트 해결
Flaky 테스트는 CI/CD에서 가장 큰 골칫거리입니다. 때로는 성공하고 때로는 실패하는 테스트는 신뢰성을 떨어뜨립니다. 대부분의 Flaky 테스트는 타이밍 문제에서 비롯됩니다.
고정된 시간(2초, 3초 등)을 기다리는 것은 나쁜 패턴입니다. 네트워크가 느리면 실패하고, 빠르면 불필요하게 기다립니다. 대신 특정 조건(요소가 보임, 텍스트 포함 등)을 기다려야 합니다.
// ❌ 문제: 타이밍 이슈
test('검색 결과', async ({ page }) => {
await page.goto('/search');
await page.fill('#search', 'playwright');
await page.click('#submit');
await page.waitForTimeout(2000); // ❌ 고정 대기
const results = await page.textContent('.results');
expect(results).toContain('10개');
});
// ✅ 해결: 조건 기반 대기
test('검색 결과', async ({ page }) => {
await page.goto('/search');
await page.fill('#search', 'playwright');
await page.click('#submit');
// 특정 상태까지 대기
await page.waitForSelector('.results', { state: 'visible' });
const results = page.locator('.results');
await expect(results).toContainText('10개');
});
Playwright의 Locator는 기본적으로 자동 대기를 하므로 대부분의 경우 명시적 대기가 필요 없습니다.
메모리 누수 방지
// Context를 명시적으로 관리
test('컨텍스트 관리', async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
try {
await page.goto('/');
// 테스트 로직...
} finally {
await context.close(); // 명시적 정리
}
});
CI 환경 최적화
// playwright.config.ts
export default defineConfig({
// CI에서 재시도
retries: process.env.CI ? 2 : 0,
// CI에서 워커 제한
workers: process.env.CI ? 1 : undefined,
// CI에서 헤드리스 강제
use: {
headless: process.env.CI ? true : false,
},
});
관련 리소스
주의사항과 팁
1. 테스트 격리 유지
각 테스트는 독립적으로 실행되어야 합니다. 테스트 간 데이터 공유나 순서 의존성이 있으면 Flaky 테스트가 됩니다. beforeEach에서 필요한 상태를 설정하세요.
2. 적절한 Locator 선택
TestId > Role > Text > CSS Selector 순서로 선택하세요. CSS Selector는 DOM 구조 변경에 취약하므로 최후의 수단으로만 사용하세요.
3. 비용 고려
Playwright는 실제 브라우저를 실행하므로 리소스를 많이 사용합니다. CI/CD에서 Worker 수를 제한하고, 불필요한 테스트는 건너뛰도록 설정하세요.
다음 단계
-
Visual Testing 도구 통합
- Percy, Chromatic 등과 연동하여 UI 회귀 테스트를 자동화할 수 있습니다. 픽셀 단위로 변경 사항을 감지하여 의도하지 않은 스타일 변경을 막을 수 있습니다.
-
성능 테스팅
- Lighthouse CI와 통합하면 모든 PR에서 자동으로 성능 점수를 측정할 수 있습니다. Core Web Vitals(LCP, FID, CLS)를 추적하여 사용자 경험을 보장하세요.
-
접근성 테스팅
- axe-core를 통합하면 WCAG 접근성 기준을 자동으로 검증할 수 있습니다. 색상 대비, 키보드 네비게이션, 스크린 리더 지원을 테스트하세요.
Playwright는 강력하고 빠르며 안정적인 E2E 테스팅 도구입니다. 자동 대기, 병렬 실행, 멀티 브라우저 지원으로 생산성을 크게 향상시킬 수 있습니다. 실무에서는 중요한 사용자 플로우(로그인, 결제, 회원가입 등)부터 테스트를 작성하고, 점진적으로 커버리지를 확대하는 것을 권장합니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Playwright를 활용한 E2E 테스팅 완벽 가이드. Selenium, Cypress 대비 장점, 설치부터 고급 테스팅 패턴, CI/CD 통합, 병렬 테스트, 비주얼 리그레션 테스팅까지 실전 예제로 학습합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
이 글에서 다루는 키워드 (관련 검색어)
Playwright, E2E Testing, Testing, Automation, Web Testing, CI/CD, JavaScript, TypeScript 등으로 검색하시면 이 글이 도움이 됩니다.