Use Cases (업데이트: 2026. 6. 3.)

Claude Code로 E2E 테스트하기: Playwright/Cypress와 CI 실전 가이드

Claude Code로 Playwright E2E 테스트를 설계하는 CI, 실패 사례, 실행 예제 가이드.

Claude Code로 E2E 테스트하기: Playwright/Cypress와 CI 실전 가이드

Claude Code에 맡기기 전에 보호할 흐름을 정한다

Claude Code가 로그인 테스트를 만들었고 터미널이 초록색이 되었다고 해서 출시 준비가 끝난 것은 아닙니다. E2E 테스트는 실제 브라우저에서 사용자의 흐름을 확인하므로 단위 테스트보다 느리고, 실패 원인을 찾는 비용도 큽니다. 모든 UI 조각을 E2E로 만들면 CI가 느려지고 팀은 실패를 신뢰하지 않게 됩니다.

먼저 깨지면 손해가 큰 흐름을 고릅니다. 로그인, 결제, 상담 폼, 관리자 위험 작업, 그리고 글에서 상품이나 교육 페이지로 이어지는 CTA가 대표적입니다. Masa가 ClaudeCodeLab 운영에서 배운 점은, 페이지가 잘 렌더링되어도 글 끝의 교육 CTA 링크가 깨질 수 있다는 것입니다. 이런 문제는 단순 화면 확인으로 놓치기 쉽지만 수익 경로에는 직접 영향을 줍니다.

이 글은 2026년 6월 기준으로 확인한 Playwright 설치 문서, locator 문서, CI 문서, Trace Viewer를 바탕으로 Claude Code에 요청하는 방법, 실행 가능한 Playwright 코드, CI 설정, headed/headless 차이, Cypress와의 선택 기준을 정리합니다. 관련 내용은 API 테스트 자동화테스트 전략 가이드도 함께 보세요.

flowchart LR
  A["Claude Code에 요청"] --> B["핵심 흐름 3개 선택"]
  B --> C["Playwright 테스트 구현"]
  C --> D["trace와 보고서로 조사"]
  D --> E["CI 필수 체크"]
  C --> F["CTA와 수익 경로 확인"]

Claude Code에 주는 요청 형식

요청에는 범위, 성공 조건, 실패 조건, 수정 가능한 파일, 검증 명령을 넣습니다. 그렇지 않으면 Claude Code가 CSS 클래스에 의존하는 취약한 테스트를 만들기 쉽습니다. Playwright 공식 문서는 getByRole, getByLabel, getByText, getByTestId 같은 locator 사용을 권장합니다. locator는 페이지 요소를 찾는 규칙이며, Playwright의 자동 대기와 재시도 가능한 단언과 잘 맞습니다.

대상: 로그인, 결제, 뉴스레터 가입 E2E 테스트
도구: Playwright Test. 마지막에 Cypress 선택 기준도 정리
조건:
- CSS 클래스보다 role, label, text, testid를 우선한다
- 성공 경로뿐 아니라 입력 오류와 빈 장바구니도 확인한다
- CI에서는 workers: 1, trace: on-first-retry를 사용한다
- 수익 CTA의 href가 사라지지 않았는지 검증한다
수정 가능 파일:
- tests/e2e/**/*.spec.ts
- playwright.config.ts
검증:
- npx playwright test
- 로컬 조사에는 npx playwright test --headed

범위 밖도 분명히 적어야 합니다. 실제 결제, 실제 메일 발송, 광고 클릭, CRM 쓰기는 매번 E2E에서 직접 실행하지 않는 편이 안전합니다. 테스트 모드, mock, API 계약 테스트로 분리하세요. 외부 연동은 webhook 구현분석 구현도 참고할 수 있습니다.

설치와 기본 명령

새 프로젝트는 아래 명령으로 시작할 수 있습니다. Playwright 버전은 특정 브라우저 바이너리와 연결되므로, 업그레이드 후 브라우저도 다시 설치해야 합니다. Linux CI에서는 시스템 의존성이 필요하므로 --with-deps를 사용하는 것이 현실적입니다.

npm init playwright@latest
npx playwright install
npx playwright test
npx playwright test --headed
npx playwright test --ui
npx playwright show-report

--headed는 눈에 보이는 브라우저를 열어 로컬에서 조사할 때 쓰고, --ui는 Playwright의 UI 모드입니다. CI는 보통 headless, 즉 브라우저 창을 띄우지 않는 방식으로 실행합니다. 로컬에서는 통과하고 CI에서 실패하면 애니메이션, 화면 크기, 글꼴, 시간대, CPU 속도, 외부 API 대기를 먼저 의심합니다.

바로 실행할 수 있는 Playwright 예제

아래 spec은 page.setContent로 작은 데모 화면을 만들기 때문에 실제 앱 서버가 없어도 실행됩니다. Playwright를 설치한 뒤 tests/e2e/claude-code-e2e.spec.ts에 복사하면 됩니다. 실제 앱에서는 renderDemoApp(page)page.goto('/login')으로 바꾸고, label과 data-testid를 화면에 맞춥니다.

// tests/e2e/claude-code-e2e.spec.ts
import { test, expect, type Page } from '@playwright/test';

async function renderDemoApp(page: Page) {
  await page.setContent(`
    <!doctype html>
    <main>
      <form id="login-form" aria-label="Login form">
        <label>Email <input id="email" aria-label="Email" /></label>
        <label>Password <input id="password" aria-label="Password" type="password" /></label>
        <button type="submit">Log in</button>
        <p id="login-error" role="alert" hidden></p>
      </form>

      <section id="dashboard" hidden>
        <h1>Dashboard</h1>
        <a data-testid="training-cta" href="/training/">Book Claude Code training</a>
        <button id="add-plan">Add Pro plan to cart</button>
        <span data-testid="cart-count">0</span>
        <button id="buy">Complete purchase</button>
        <p data-testid="order-status" role="status"></p>

        <label>Newsletter email <input id="newsletter-email" aria-label="Newsletter email" /></label>
        <button id="newsletter-button" type="button">Join newsletter</button>
        <p id="newsletter-error" role="alert" hidden></p>
        <p data-testid="newsletter-status" role="status"></p>
      </section>
    </main>

    <script>
      const state = { cart: 0 };
      document.querySelector('#login-form').addEventListener('submit', (event) => {
        event.preventDefault();
        const email = document.querySelector('#email').value;
        const password = document.querySelector('#password').value;
        const error = document.querySelector('#login-error');

        if (email === 'masa@example.com' && password === 'password123') {
          document.querySelector('#login-form').hidden = true;
          document.querySelector('#dashboard').hidden = false;
          error.hidden = true;
          return;
        }

        error.textContent = 'Authentication failed';
        error.hidden = false;
      });

      document.querySelector('#add-plan').addEventListener('click', () => {
        state.cart += 1;
        document.querySelector('[data-testid="cart-count"]').textContent = String(state.cart);
      });

      document.querySelector('#buy').addEventListener('click', () => {
        const status = document.querySelector('[data-testid="order-status"]');
        status.textContent = state.cart === 0 ? 'Cart is empty' : 'Order ORD-1001 completed';
      });

      document.querySelector('#newsletter-button').addEventListener('click', () => {
        const email = document.querySelector('#newsletter-email').value;
        const error = document.querySelector('#newsletter-error');
        const status = document.querySelector('[data-testid="newsletter-status"]');

        if (!email.includes('@')) {
          error.textContent = 'Enter a valid email';
          error.hidden = false;
          status.textContent = '';
          return;
        }

        error.hidden = true;
        status.textContent = 'Thanks, we will send the checklist.';
      });
    </script>
  `);
}

async function loginAsDemoUser(page: Page) {
  await page.getByLabel('Email').fill('masa@example.com');
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Log in' }).click();
}

test.describe('Claude Code E2E starter', () => {
  test('use case 1: login shows dashboard and keeps the training CTA', async ({ page }) => {
    await renderDemoApp(page);
    await loginAsDemoUser(page);

    await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
    await expect(page.getByTestId('training-cta')).toHaveAttribute('href', '/training/');
  });

  test('use case 2: checkout flow creates an order number', async ({ page }) => {
    await renderDemoApp(page);
    await loginAsDemoUser(page);

    await page.getByRole('button', { name: 'Add Pro plan to cart' }).click();
    await expect(page.getByTestId('cart-count')).toHaveText('1');
    await page.getByRole('button', { name: 'Complete purchase' }).click();
    await expect(page.getByTestId('order-status')).toContainText('ORD-1001');
  });

  test('use case 3: newsletter validation blocks invalid leads', async ({ page }) => {
    await renderDemoApp(page);
    await loginAsDemoUser(page);

    await page.getByRole('button', { name: 'Join newsletter' }).click();
    await expect(page.getByRole('alert')).toContainText('Enter a valid email');

    await page.getByLabel('Newsletter email').fill('reader@example.com');
    await page.getByRole('button', { name: 'Join newsletter' }).click();
    await expect(page.getByTestId('newsletter-status')).toContainText('Thanks');
  });
});

이 spec은 로그인, 결제, 리드 획득이라는 세 가지 구체적 사용 사례를 확인합니다. 또한 교육 CTA의 href도 확인하므로, 화면은 보이지만 수익 경로가 끊어진 문제를 잡을 수 있습니다.

실무용 Playwright 설정

설정 파일은 실패 증거를 남기는 역할을 합니다. Playwright CI 문서는 안정성과 재현성을 위해 CI의 workers를 1로 두는 방식을 안내합니다. 충분한 인프라가 생기기 전에는 빠른 병렬보다 재현 가능한 실패가 더 중요합니다.

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests/e2e',
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  use: {
    baseURL: process.env.BASE_URL ?? 'http://127.0.0.1:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});

trace: 'on-first-retry'는 어떤 화면이 보였는지, 어떤 클릭이 실패했는지, 어떤 단언이 시간 초과되었는지 보여줍니다. Claude Code에 원인 분석을 맡길 때도 trace와 HTML 보고서가 있으면 훨씬 정확합니다.

GitHub Actions에 넣기

CI에서 흔한 문제는 테스트 코드가 아니라 브라우저와 Linux 의존성 부족입니다. 먼저 단순한 구성으로 시작하고, 캐시나 분산 실행은 병목이 확인된 뒤 적용합니다.

# .github/workflows/playwright.yml
name: Playwright Tests

on:
  push:
    branches: [main, master]
  pull_request:
    branches: [main, master]

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v6
        with:
          node-version: lts/*
      - name: Install dependencies
        run: npm ci
      - name: Install Playwright browsers
        run: npx playwright install --with-deps
      - name: Run E2E tests
        run: npx playwright test
      - uses: actions/upload-artifact@v5
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

프리뷰 환경을 테스트한다면 BASE_URL을 프리뷰 주소로 넘깁니다. CI 안에서 앱을 직접 띄워야 한다면 Playwright의 webServer 설정을 추가합니다.

E2E로 볼 만한 사용 사례

사용 사례E2E가 필요한 이유중요한 기대값
로그인에서 대시보드인증, 이동, 권한 표시가 연결됨제목, 사용자 상태, 로그아웃, 오류
결제 또는 상담 폼매출과 리드에 직접 영향주문 번호, 확인 화면, 중복 제출 방지
글에서 무료 자료나 교육 페이지로 이동광고 외 수익 경로 보호CTA href, 이벤트, 모바일 표시
관리자 위험 작업오삭제와 권한 누락 방지확인 모달, 감사 로그, 역할별 제한
다국어 페이지번역 중 링크가 깨지기 쉬움locale URL, 자연스러운 문구, 외부 링크

이 표를 먼저 Claude Code에 주면, 단순히 버튼을 누르는 테스트가 아니라 실제 위험을 줄이는 테스트가 나옵니다.

자주 보는 실패 사례

waitForTimeout(3000)에 의존하지 마세요. 고정 대기는 내 컴퓨터에서만 통과하고 CI에서 깨지기 쉽습니다. 요소가 보이는지, URL이 바뀌었는지, 상태 문구가 나타났는지처럼 성공 상태를 기다려야 합니다.

.btn-primary:nth-child(2) 같은 CSS 선택자도 피합니다. 디자인 변경만으로 테스트가 깨집니다. role, label, text, 안정적인 data-testid를 우선합니다.

테스트끼리 상태를 공유하지 마세요. 이전 테스트가 만든 장바구니, Cookie, DB 데이터에 다음 테스트가 의존하면 병렬 실행과 재시도에서 문제가 커집니다.

headed와 headless 차이도 무시하면 안 됩니다. 글꼴, GPU, 스크롤, 클립보드, 파일 업로드, 화면 크기가 CI와 로컬에서 다를 수 있습니다. CI는 headless로 안정화하고, 조사할 때만 --headed--ui를 씁니다.

실제 결제, 메일, 광고, CRM을 매번 직접 호출하지 마세요. 테스트 모드, mock, 별도의 API 계약 테스트가 더 안전합니다.

Playwright와 Cypress 선택

관점PlaywrightCypress
브라우저Chromium, Firefox, WebKitChrome 계열, Firefox, Edge
병렬 실행Playwright Test에 내장Dashboard 포함 설계가 많음
여러 탭과 컨텍스트강함제약이 많음
디버깅Trace Viewer, UI mode, HTML report친숙한 대화형 GUI
잘 맞는 상황CI, 다중 브라우저, 인증 상태 분리이미 Cypress 흐름이 있는 프론트엔드 팀

Cypress도 여전히 좋은 선택입니다. 기존 Cypress 자산이 있다면 새 도구라서 바로 옮길 필요는 없습니다. 핵심 흐름 하나에 Playwright를 추가해 흔들림, trace 품질, CI 시간을 비교하면 됩니다. 최종 판단은 PlaywrightCypress 공식 문서로 확인하세요.

수익 CTA와 실제 확인 결과

E2E는 수익도 보호합니다. 상담 링크, 상품 카드, 무료 자료 폼, 교육 CTA가 깨지면 시각적 버그보다 비용이 클 수 있습니다. 개인은 무료 Claude Code 치트시트로 시작하고, 반복 프롬프트는 제품과 템플릿에서 정리할 수 있습니다. 팀이 CI 문턱, CLAUDE.md, 리뷰 규칙, E2E 책임 범위를 세우려면 Claude Code 교육과 상담이 적합합니다.

이 글의 예제는 Playwright spec과 설정을 TypeScript 코드 조각으로 추출해 문법 검증했습니다. 가장 효과적이었던 습관은 테스트 이름에 “교육 CTA 유지” 같은 비즈니스 기대값을 직접 쓰는 것이었습니다. 실제 프로젝트에서는 로그인, 결제 또는 리드 폼, 글 CTA 세 개만 먼저 만들고 CI에서 안정된 뒤 관리자와 다국어 화면으로 넓히는 것이 현실적입니다.

#Claude Code #E2E 테스트 #Playwright #Cypress #CI
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.