이 글은 누구를 위한 것인가
- E2E 테스트 도구를 처음 선택하는 팀
- Cypress를 쓰고 있는데 Playwright로 전환을 고려하는 팀
- 테스트 전략 전체를 재설계하는 QA 엔지니어, 프론트엔드 리드
들어가며
2026년 현재 E2E 테스트 시장은 Playwright(Microsoft)가 빠르게 점유율을 높이고 있다. Cypress가 여전히 강세이지만, 크로스 브라우저 지원과 병렬 실행에서 Playwright의 우세가 명확하다.
둘 다 좋은 도구다. 선택은 팀의 상황, 기술 스택, 테스트 복잡도에 따라 달라진다. 이 글에서는 실용적인 기준으로 선택을 안내한다.
이 글은 bluefoxdev.kr의 프론트엔드 테스트 전략 가이드 를 참고하고, 2026년 도구 비교 관점에서 확장하여 작성했습니다.
1. 핵심 아키텍처 차이
[Cypress 아키텍처]
테스트 코드 ─→ Cypress 프록시 ─→ 브라우저 내 실행
└─ 모든 코드가 같은 루프에서 실행
장점: 브라우저 내부 접근 쉬움, 디버깅 편리
단점: 멀티 탭, 크로스 브라우저 제한
[Playwright 아키텍처]
테스트 코드 ─→ Node.js 프로세스 ─→ CDP/WebDriver ─→ 브라우저
└─ 완전히 분리
장점: 멀티 브라우저, 멀티 탭, 병렬 실행 자유로움
단점: 비동기 async/await 항상 필요
2. 기능 비교
| 항목 | Playwright | Cypress |
|---|---|---|
| 브라우저 지원 | Chrome, Firefox, Safari, Edge | Chrome 계열, Firefox (제한적) |
| 모바일 에뮬레이션 | ✅ 완전 지원 | 제한적 |
| 병렬 실행 | 내장 지원 | 유료 플랜 |
| 컴포넌트 테스트 | ✅ | ✅ |
| 네트워크 인터셉트 | ✅ | ✅ |
| 파일 업로드 | ✅ 쉬움 | 다소 복잡 |
| iframe | ✅ | 제한적 |
| 멀티 탭/창 | ✅ | ❌ |
| 학습 곡선 | 중간 | 낮음 |
| 디버깅 UX | 좋음 | 매우 좋음 |
| 문서/생태계 | 빠르게 성장 | 성숙 |
3. Playwright 실전 예시
3.1 기본 설정
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
timeout: 30000,
retries: process.env.CI ? 2 : 0,
// 병렬 실행 설정
fullyParallel: true,
workers: process.env.CI ? 4 : 2,
// 여러 브라우저 동시 테스트
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'safari', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 7'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 15'] } },
],
// 기본 설정
use: {
baseURL: 'http://localhost:3000',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'retain-on-failure',
},
// 테스트 전 개발 서버 시작
webServer: {
command: 'npm run build && npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
3.2 Page Object Model
// tests/e2e/pages/LoginPage.ts
import { Page, Locator, expect } 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.getByRole('alert');
}
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 expectError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
}
// tests/e2e/auth.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
test.describe('인증', () => {
test('유효한 자격증명으로 로그인', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL('/dashboard');
await expect(page.getByText('환영합니다')).toBeVisible();
});
test('잘못된 비밀번호로 로그인 실패', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'wrong');
await loginPage.expectError('이메일 또는 비밀번호가 올바르지 않습니다');
});
});
3.3 API 모킹
test('API 에러 상황 테스트', async ({ page }) => {
// API 인터셉트
await page.route('/api/products', route => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Internal Server Error' }),
});
});
await page.goto('/products');
await expect(page.getByText('오류가 발생했습니다')).toBeVisible();
});
test('느린 네트워크 테스트', async ({ page }) => {
await page.route('/api/products', async route => {
await page.waitForTimeout(3000); // 3초 지연
await route.continue();
});
await page.goto('/products');
await expect(page.getByRole('progressbar')).toBeVisible();
});
4. Cypress 실전 예시
4.1 Command와 Intercept
// cypress/support/commands.ts
Cypress.Commands.add('login', (email: string, password: string) => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: { email, password },
}).then(response => {
localStorage.setItem('token', response.body.token);
});
});
// cypress/e2e/products.cy.ts
describe('상품 목록', () => {
beforeEach(() => {
cy.login('user@example.com', 'password123');
// API 인터셉트
cy.intercept('GET', '/api/products', { fixture: 'products.json' }).as('getProducts');
cy.visit('/products');
cy.wait('@getProducts');
});
it('상품 목록이 표시됨', () => {
cy.get('[data-testid="product-card"]').should('have.length', 3);
});
it('검색 기능', () => {
cy.get('[data-testid="search-input"]').type('노트북');
cy.intercept('GET', '/api/products?q=노트북', { fixture: 'laptops.json' }).as('search');
cy.get('[data-testid="search-button"]').click();
cy.wait('@search');
cy.get('[data-testid="product-card"]').should('have.length', 1);
});
});
5. 컴포넌트 테스트
// Playwright Component Test
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from '@/components/Button';
test('버튼 클릭 시 onClick 호출', async ({ mount }) => {
let clicked = false;
const component = await mount(
<Button onClick={() => { clicked = true; }}>클릭</Button>
);
await component.click();
expect(clicked).toBe(true);
});
// Cypress Component Test
import { Button } from '@/components/Button';
describe('Button', () => {
it('클릭 이벤트', () => {
const onClick = cy.stub();
cy.mount(<Button onClick={onClick}>클릭</Button>);
cy.get('button').click();
expect(onClick).to.be.calledOnce;
});
});
6. CI/CD 통합
# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]
jobs:
playwright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run E2E tests
run: npx playwright test
env:
CI: true
- name: Upload test results
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
7. 선택 가이드
[Playwright를 선택해야 하는 경우]
✅ Safari/Firefox 지원이 중요한 경우
✅ 병렬 실행으로 CI 속도가 중요한 경우
✅ 모바일 에뮬레이션 테스트 필요
✅ 멀티 탭/창 시나리오 (예: OAuth 팝업)
✅ 팀이 TypeScript async/await에 익숙한 경우
✅ 새로 시작하는 프로젝트 (2026년 표준)
[Cypress를 선택해야 하는 경우]
✅ 빠른 학습 곡선이 중요 (QA 비개발자 포함)
✅ 이미 Cypress 기반 테스트가 많은 기존 프로젝트
✅ Chrome 전용이고 디버깅 UX가 우선
✅ Cypress Studio를 통한 테스트 녹화 활용
마무리
2026년 신규 프로젝트라면 Playwright가 더 미래 지향적인 선택이다. 브라우저 지원, 병렬 실행, 타입스크립트 통합이 더 강력하다.
기존 Cypress 팀이라면 굳이 전환할 필요는 없다. 다만 크로스 브라우저 지원이나 병렬 실행에서 제약을 느낀다면 Playwright로의 점진적 전환을 검토할 시점이다.
두 도구 모두 컴포넌트 테스트를 지원하므로, Vitest/Jest 단위 테스트 + Playwright/Cypress E2E 조합의 테스트 피라미드를 구성하는 것이 실용적이다.