수동으로 브라우저를 켜고 회원가입부터 결제 페이지까지 일일이 클릭하며 테스트하다 지친 경험이 있을 것이다. 다만 막상 E2E(End-to-End) 테스트 자동화를 도입하려 해도 Cypress와 Playwright 중 어떤 도구를 골라야 할지 몰라 망설이는 개발자가 많다. 이번에는 두 도구가 정확히 어떻게 다른지, 왜 필요한지, 그리고 실제 프로젝트에 어떻게 적용하는지 완벽하게 정리해서 소개하겠다.

 

1. E2E 테스트와 두 도구의 동작 원리

E2E 테스트는 사용자의 관점에서 애플리케이션의 처음부터 끝까지 전체 흐름을 검증하는 테스트 방식이다. 단위 테스트(Unit Test)나 통합 테스트(Integration Test)가 개별 함수나 모듈의 동작을 확인한다면, E2E 테스트는 실제 브라우저 환경에서 UI 요소 클릭, 폼 입력, 페이지 이동이 정상적으로 동작하는지 확인한다.

Cypress와 Playwright는 현재 프론트엔드 생태계에서 가장 주목받는 E2E 테스트 프레임워크다. 하지만 내부 동작 방식에는 명확한 차이가 존재한다.

Cypress는 브라우저 내부(In-Browser)에서 직접 테스트 코드를 실행한다. 테스트 실행기와 애플리케이션이 동일한 런타임 수명 주기를 공유하므로 DOM 접근이 매우 빠르고 디버깅이 직관적이다. 반면 Playwright는 브라우저 외부에서 Chrome DevTools Protocol(CDP) 및 각 브라우저의 전용 디버깅 프로토콜을 통해 브라우저를 원격 제어한다. 이 차이로 인해 지원 기능과 성능 특성이 갈리게 된다.

 

2. Cypress vs Playwright 한눈에 비교하기

두 도구의 핵심 사양과 기능을 표로 정리했다. 프로젝트의 우선순위에 따라 적합한 도구를 선택하는 기준이 된다.

비교 항목CypressPlaywright
아키텍처브라우저 내부 실행외부 디버깅 프로토콜 통신
지원 언어JavaScript, TypeScriptJS/TS, Python, Java, C# (.NET)
지원 브라우저Chrome, Edge, Firefox, Electron (Webkit 제한적)Chromium (Chrome, Edge), Firefox, WebKit (Safari)
다중 탭 / 다중 도메인제한적 (동일 출처 정책 영향)완벽 지원 (실시간 다중 디바이스/탭 제어)
실행 속도 및 병렬화보통 (유료 Cloud 서비스 활용 권장)매우 빠름 (기본 멀티 워커 병렬 실행)
자동 대기 (Auto-waiting)기본 지원 (DOM 상태 기반)기본 지원 (Actionability 체크 기반)

 

3. 실전 코드 작성과 작성 패턴 비교

로그인 폼을 입력하고 제출 후 성공 메시지를 확인하는 동일한 시나리오를 두 도구로 작성해보자.

 

Cypress 실전 예제

✗ 잘못된 작성 방식 (하드코딩된 대기시간 사용)

// ✗ 불필요한 cy.wait() 사용으로 테스트 속도 저하 및 불안정성 유발
describe('로그인 테스트', () => {
  it('로그인 성공', () => {
    cy.visit('/login');
    cy.get('#input-email').type('user@example.com');
    cy.get('#input-password').type('secret123');
    cy.get('#btn-submit').click();
    
    // 네트워크 응답을 기다리지 않고 무작정 3초 대기
    cy.wait(3000); 
    cy.get('.welcome-message').should('contain', '환영합니다');
  });
});

✓ 올바른 작성 방식 (자동 대기 및 네트워크 인터셉트 활용)

// ✓ 별도의 wait 없이 요소의 가시성과 네트워크 완료를 자동 대기
describe('로그인 테스트', () => {
  it('로그인 성공', () => {
    cy.intercept('POST', '/api/login').as('loginReq');
    
    cy.visit('/login');
    cy.get('#input-email').type('user@example.com');
    cy.get('#input-password').type('secret123');
    cy.get('#btn-submit').click();
    
    // API 응답 명시적 대기 및 DOM 상태 자동 검증
    cy.wait('@loginReq');
    cy.get('.welcome-message').should('be.visible').and('contain', '환영합니다');
  });
});

실행 결과: Cypress 대시보드 및 타임트래블(Time-travel) 스냅샷을 통해 각 단계별 DOM 상태를 시각적으로 즉각 확인할 수 있다.

 

Playwright 실전 예제

✗ 잘못된 작성 방식 (비동기 처리 누락 또는 잘못된 선택자)

// ✗ await 누락 및 액션 불확실성
import { test, expect } from '@playwright/test';

test('로그인 성공', async ({ page }) => {
  page.goto('/login'); // await 누락으로 실행 순서 꼬임
  page.locator('#input-email').fill('user@example.com');
  await page.click('#btn-submit');
});

✓ 올바른 작성 방식 (async/await 완벽 적용 및 Auto-waiting 활용)

// ✓ async/await와 웹 접근성 기반 locator 활용
import { test, expect } from '@playwright/test';

test('로그인 성공', async ({ page }) => {
  await page.goto('/login');
  
  await page.getByLabel('이메일').fill('user@example.com');
  await page.getByLabel('비밀번호').fill('secret123');
  await page.getByRole('button', { name: '로그인' }).click();
  
  const welcomeText = page.locator('.welcome-message');
  await expect(welcomeText).toBeVisible();
  await expect(welcomeText).toHaveText(/환영합니다/);
});

실행 결과: 헤드리스(Headless) 모드에서 병렬로 고속 실행되며, 실패 시 트레이스 뷰어(Trace Viewer) 파일이 생성되어 네트워크, 콘솔 로그, 스냅샷을 정밀 분석할 수 있다.

 

4. E2E 테스트 작성 시 흔한 실수와 주의사항

E2E 테스트는 단위 테스트에 비해 실행 비용이 크다. 아래 항목들을 체크하지 않으면 테스트가 자주 깨지는 Flaky Test 현상이 발생한다.

✗ 임의의 대기시간(cy.wait(5000) 또는 page.waitForTimeout(5000))을 코드 곳곳에 방치하는 것.
✓ 프레임워크가 제공하는 자동 대기(Auto-waiting) 기능과 특정 요소/네트워크 상태 기반 검증을 사용하자.

✗ 테스트끼리 데이터 상태를 공유하여 이전 테스트 결과에 따라 다음 테스트가 성공/실패하는 구조.
✓ 각 테스트 실행 전 데이터베이스 상태를 격리하거나 API 호출을 통해 독립적인 환경을 초기화하자.

✗ 변경되기 쉬운 CSS 클래스명(.btn-style-v2)이나 복잡한 XPath를 선택자로 지정하는 것.
✓ data-testid 속성이나 접근성 기반 역할(getByRole, getByLabel)을 선택자로 활용하자.

 

5. 마무리

E2E 테스트 도구 선택은 서비스의 품질보증(QA) 속도와 개발 생산성을 결정짓는 핵심 요소다. 프로젝트 상황에 맞는 올바른 도구 선택과 테스트 작성 습관이 모여서 서비스의 안정적인 배포를 만든다는 점을 잊지 말자. 이 글의 비교표와 실전 예제를 참고해 현재 팀의 니즈에 어울리는 도구를 도입해 보면, 브라우저 수동 테스트 스트레스에서 완전히 벗어날 수 있을 것이다.