Playwright vs Cypress 2026: E2E 테스트 전략 선택 가이드

프론트엔드

PlaywrightCypressE2E 테스트테스트 자동화QA 자동화

이 글은 누구를 위한 것인가

  • E2E 테스트 도구를 처음 선택하는 팀
  • Cypress를 쓰고 있는데 Playwright로 전환을 고려하는 팀
  • 테스트 전략 전체를 재설계하는 QA 엔지니어, 프론트엔드 리드

들어가며

2026년 현재 E2E 테스트 시장은 Playwright(Microsoft)가 빠르게 점유율을 높이고 있다. Cypress가 여전히 강세이지만, 크로스 브라우저 지원과 병렬 실행에서 Playwright의 우세가 명확하다.

둘 다 좋은 도구다. 선택은 팀의 상황, 기술 스택, 테스트 복잡도에 따라 달라진다. 이 글에서는 실용적인 기준으로 선택을 안내한다.

이 글은 bluefoxdev.kr의 프론트엔드 테스트 전략 가이드 를 참고하고, 2026년 도구 비교 관점에서 확장하여 작성했습니다.


1. 핵심 아키텍처 차이

[Cypress 아키텍처]
테스트 코드 ─→ Cypress 프록시 ─→ 브라우저 내 실행
                                  └─ 모든 코드가 같은 루프에서 실행
장점: 브라우저 내부 접근 쉬움, 디버깅 편리
단점: 멀티 탭, 크로스 브라우저 제한

[Playwright 아키텍처]  
테스트 코드 ─→ Node.js 프로세스 ─→ CDP/WebDriver ─→ 브라우저
                                                    └─ 완전히 분리
장점: 멀티 브라우저, 멀티 탭, 병렬 실행 자유로움
단점: 비동기 async/await 항상 필요

2. 기능 비교

항목PlaywrightCypress
브라우저 지원Chrome, Firefox, Safari, EdgeChrome 계열, 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 조합의 테스트 피라미드를 구성하는 것이 실용적이다.