Claude Code로 E2E 테스트하기: Playwright/Cypress와 CI 실전 가이드
Claude Code로 Playwright E2E 테스트를 설계하는 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 선택
| 관점 | Playwright | Cypress |
|---|---|---|
| 브라우저 | Chromium, Firefox, WebKit | Chrome 계열, Firefox, Edge |
| 병렬 실행 | Playwright Test에 내장 | Dashboard 포함 설계가 많음 |
| 여러 탭과 컨텍스트 | 강함 | 제약이 많음 |
| 디버깅 | Trace Viewer, UI mode, HTML report | 친숙한 대화형 GUI |
| 잘 맞는 상황 | CI, 다중 브라우저, 인증 상태 분리 | 이미 Cypress 흐름이 있는 프론트엔드 팀 |
Cypress도 여전히 좋은 선택입니다. 기존 Cypress 자산이 있다면 새 도구라서 바로 옮길 필요는 없습니다. 핵심 흐름 하나에 Playwright를 추가해 흔들림, trace 품질, CI 시간을 비교하면 됩니다. 최종 판단은 Playwright와 Cypress 공식 문서로 확인하세요.
수익 CTA와 실제 확인 결과
E2E는 수익도 보호합니다. 상담 링크, 상품 카드, 무료 자료 폼, 교육 CTA가 깨지면 시각적 버그보다 비용이 클 수 있습니다. 개인은 무료 Claude Code 치트시트로 시작하고, 반복 프롬프트는 제품과 템플릿에서 정리할 수 있습니다. 팀이 CI 문턱, CLAUDE.md, 리뷰 규칙, E2E 책임 범위를 세우려면 Claude Code 교육과 상담이 적합합니다.
이 글의 예제는 Playwright spec과 설정을 TypeScript 코드 조각으로 추출해 문법 검증했습니다. 가장 효과적이었던 습관은 테스트 이름에 “교육 CTA 유지” 같은 비즈니스 기대값을 직접 쓰는 것이었습니다. 실제 프로젝트에서는 로그인, 결제 또는 리드 폼, 글 CTA 세 개만 먼저 만들고 CI에서 안정된 뒤 관리자와 다국어 화면으로 넓히는 것이 현실적입니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Obsidian 메모를 CLAUDE.md로 바꾸는 Claude Code 워크플로
Obsidian 작업 메모를 CLAUDE.md 운영 노트로 정리해 Claude Code 세션의 문맥 반복을 줄입니다.
Claude Code Revenue CTA Routing: 글에서 PDF, Gumroad, 상담으로 보내기
독자 의도에 따라 무료 PDF, Gumroad 상품, 상담으로 나누는 Claude Code CTA 설계입니다.
Claude Code 팀 인계 규칙: 리뷰 증거, 권한, 롤백, 수익 경로까지 넘기는 법
Claude Code 작업을 팀에 넘길 때 필요한 증거, 권한 규칙, 롤백, 무료 PDF, Gumroad, 상담 경로 체크리스트.