Advanced (업데이트: 2026. 6. 3.)

Claude Code와 Playwright로 E2E 테스트 실무화하기

Claude Code와 Playwright로 E2E, 모바일 스크린샷, 인증 상태, Trace Viewer, CI 재시도를 설계합니다.

Claude Code와 Playwright로 E2E 테스트 실무화하기

Claude Code에 “Playwright 테스트를 추가해 줘”라고만 말하면 운영에 쓸 만한 E2E 테스트가 나오기 어렵습니다. 한 번은 통과하지만 CSS 클래스에 의존하고, 매 테스트마다 로그인 UI를 거치며, 모바일 스크린샷을 남기지 않고, CI 실패 후 Trace Viewer로 원인을追적할 수 없는 경우가 많습니다.

이 글의 목표는 Claude Code를 단순 코드 생성기가 아니라 테스트 설계 파트너로 쓰는 것입니다. Playwright로 수익 경로, 인증된 화면, 모바일 레이아웃, 코드 블록 표시, Trace Viewer, CI retry를 작고 안정적인 테스트 묶음으로 만들겠습니다.

기준 문서는 Claude Code overview, Claude Code common workflows, Playwright의 Locators, Authentication, Screenshots, Trace Viewer, Retries, CI입니다. ClaudeCodeLab의 관련 글은 테스트 전략, CI/CD 설정, 반응형 디자인을 함께 보세요.

보호할 흐름부터 고르기

E2E는 실제 브라우저를 띄우므로 단위 테스트보다 느립니다. 모든 세부 사항이 아니라 브라우저로만 증명할 수 있는 흐름을 고르는 편이 좋습니다.

유스케이스보호 대상Playwright 확인 포인트
글에서 제품 페이지로 이동독자가 /products/로 이어지는지CTA 링크, URL, 모바일 탭 가능성
로그인 후 대시보드인증된 사용자가 보호된 페이지에 접근하는지storageState, 리다이렉트, 권한
코드 글 레이아웃코드 블록과 표가 모바일 폭을 깨지 않는지모바일 스크린샷, 가로 overflow, trace
flowchart LR
  A["매출 또는 가입 경로"] --> B["Playwright E2E"]
  C["모바일 레이아웃 위험"] --> B
  D["순수 검증 로직"] --> E["단위 테스트"]
  F["API 또는 컴포넌트 경계"] --> G["통합 테스트"]

ClaudeCodeLab 같은 콘텐츠 사이트에서도 이 기준은 유효합니다. CTA가 모바일에서 가려지거나, 긴 코드가 본문 폭을 밀어내거나, 유료 사용자 화면이 CI에 없으면 작아 보이는 버그가 전환율과 신뢰를 깎습니다.

Claude Code에 경계를 명확히 주기

Claude Code는 코드베이스를 읽을 수 있지만 품질 기준을 스스로 결정하지는 않습니다. 대상 URL, 선택자 규칙, 실행 명령, 수정 가능한 파일을 처음부터 알려 주세요.

기존 Astro 사이트를 읽고 Playwright E2E 테스트를 추가하세요.

목표:
- `/ko/blog/claude-code-playwright-testing/`에서 `/products/`와 `/training/`으로 이동되는지 확인
- 390px 모바일 폭에서 글, 표, 코드 블록이 가로로 넘치지 않는지 확인
- 로그인 필요한 테스트는 매번 UI 로그인하지 말고 `storageState` 사용
- CI에서는 retry 2회, trace는 `on-first-retry`

제약:
- `page.waitForTimeout()` 사용 금지
- CSS 클래스 체인보다 role, label, text, test id 우선
- 수정 파일은 `playwright.config.ts`와 `tests/e2e/**`로 제한
- `npx playwright test` 실행 후 실패하면 Trace Viewer 근거로 설명

리뷰할 때는 파일 수보다 실패가 설명 가능한지, 선택자가 사용자 관점인지, 테스트 데이터가 통제되는지를 봐야 합니다.

복사해서 시작하는 설정

아래 설정은 프레임워크가 달라도 BASE_URL과 preview 명령만 바꾸면 사용할 수 있습니다.

cd site
npm i -D @playwright/test
npx playwright install
mkdir tests/e2e
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

const baseURL = process.env.BASE_URL ?? 'http://127.0.0.1:4321';
const hasAuth = Boolean(process.env.TEST_EMAIL && process.env.TEST_PASSWORD);
const authFile = 'playwright/.auth/user.json';

export default defineConfig({
  testDir: './tests/e2e',
  timeout: 30_000,
  expect: { timeout: 5_000 },
  fullyParallel: true,
  forbidOnly: Boolean(process.env.CI),
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 2 : undefined,
  reporter: process.env.CI ? [['html'], ['github']] : 'html',
  use: {
    baseURL,
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  ...(process.env.PLAYWRIGHT_WEB_SERVER === '1'
    ? {
        webServer: {
          command: 'npm run preview -- --host 127.0.0.1 --port 4321',
          url: baseURL,
          reuseExistingServer: !process.env.CI,
          timeout: 120_000,
        },
      }
    : {}),
  projects: [
    ...(hasAuth
      ? [
          {
            name: 'setup',
            testMatch: /.*\.setup\.ts/,
          },
        ]
      : []),
    {
      name: 'desktop-chrome',
      use: {
        ...devices['Desktop Chrome'],
        storageState: hasAuth ? authFile : undefined,
      },
      dependencies: hasAuth ? ['setup'] : [],
    },
    {
      name: 'mobile-safari',
      use: {
        ...devices['iPhone 13'],
        storageState: hasAuth ? authFile : undefined,
      },
      dependencies: hasAuth ? ['setup'] : [],
    },
  ],
});

로컬에서는 retry를 끄고 바로 고칩니다. CI에서는 공유 runner의 흔들림이 있으므로 retry를 켜되, trace와 리포트를 함께 남기는 것이 핵심입니다.

인증 상태 저장하기

로그인이 필요한 테스트가 매번 로그인 화면을 통과하면 느리고 잘 깨집니다. Playwright의 storageState로 인증된 브라우저 상태를 저장하고, 이후 테스트에서 재사용하세요. playwright/.auth에는 민감한 쿠키가 들어갈 수 있으므로 git에 올리면 안 됩니다.

// tests/e2e/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import fs from 'node:fs';
import path from 'node:path';

const authFile = path.resolve('playwright/.auth/user.json');
const email = process.env.TEST_EMAIL;
const password = process.env.TEST_PASSWORD;

setup('save signed-in browser state', async ({ page }) => {
  setup.skip(!email || !password, 'Set TEST_EMAIL and TEST_PASSWORD to record auth state.');

  await page.goto('/login');
  await page.getByLabel(/email|メール|e-mail/i).fill(email!);
  await page.getByLabel(/password|パスワード/i).fill(password!);
  await page.getByRole('button', { name: /log in|sign in|ログイン/i }).click();

  await expect(page).toHaveURL(/dashboard|account|admin/);
  await expect(page.locator('body')).toBeVisible();

  fs.mkdirSync(path.dirname(authFile), { recursive: true });
  await page.context().storageState({ path: authFile });
});

Claude Code에는 로그인 자체의 테스트와 로그인 후 기능 테스트를 분리하라고 지시하세요. 그래야 로그인 버튼 문구 하나가 전체 E2E를 망가뜨리지 않습니다.

모바일 스크린샷과 코드 블록 QA

기술 글은 긴 코드 줄, 표, 이미지 때문에 모바일에서 쉽게 깨집니다. 아래 테스트는 CTA 존재, 모바일 스크린샷, 가로 overflow를 함께 확인합니다.

// tests/e2e/article-quality.spec.ts
import { test, expect } from '@playwright/test';

const articlePath = process.env.ARTICLE_PATH ?? '/ko/blog/claude-code-playwright-testing/';

test.describe('article quality checks', () => {
  test('article has monetization CTAs', async ({ page }) => {
    await page.goto(articlePath);

    await expect(page.getByRole('heading', { level: 1 })).toContainText(/Playwright|E2E|Claude Code/i);
    await expect(page.locator('a[href="/products/"], a[href="/products"]').first()).toBeVisible();
    await expect(page.locator('a[href="/training/"], a[href="/training"]').first()).toBeVisible();
  });

  test('mobile layout has no horizontal overflow', async ({ page }, testInfo) => {
    await page.setViewportSize({ width: 390, height: 844 });
    await page.goto(articlePath);
    await expect(page.locator('main, article').first()).toBeVisible();

    const overflow = await page.evaluate(() => ({
      viewport: window.innerWidth,
      documentWidth: document.documentElement.scrollWidth,
      offenders: Array.from(document.querySelectorAll('pre, table, img, iframe, .prose'))
        .filter((node) => {
          const rect = node.getBoundingClientRect();
          return rect.left < -1 || rect.right > window.innerWidth + 1;
        })
        .map((node) => {
          const rect = node.getBoundingClientRect();
          return `${node.tagName.toLowerCase()} ${Math.round(rect.left)}-${Math.round(rect.right)}`;
        }),
    }));

    expect(overflow.documentWidth, JSON.stringify(overflow)).toBeLessThanOrEqual(overflow.viewport + 2);
    expect(overflow.offenders).toEqual([]);
    await page.screenshot({ path: testInfo.outputPath('article-mobile.png'), fullPage: true });
  });

  test('code examples are present and copyable', async ({ page }) => {
    await page.goto(articlePath);

    const blocks = page.locator('pre code');
    await expect(blocks.first()).toBeVisible();
    expect(await blocks.count()).toBeGreaterThanOrEqual(3);
    await expect(blocks.nth(0)).toContainText(/playwright|defineConfig|test/i);
  });
});

스크린샷은 사람이 리뷰하기 위한 자료이고, 숫자 기반 overflow 검사는 CI가 판단하기 위한 기준입니다.

수익 CTA를 테스트에 넣기

콘텐츠 사이트에서는 글을 읽는 것만큼 다음 행동도 중요합니다. /products//training/으로 이어지는 링크가 깨지면 수익과 학습 흐름이 끊깁니다.

// tests/e2e/revenue-flows.spec.ts
import { test, expect } from '@playwright/test';

const articlePath = process.env.ARTICLE_PATH ?? '/ko/blog/claude-code-playwright-testing/';

test.describe('revenue and learning flows', () => {
  test('reader can move from article to products', async ({ page }) => {
    await page.goto(articlePath);

    await page.locator('a[href="/products/"], a[href="/products"]').first().click();
    await expect(page).toHaveURL(/\/products\/?$/);
    await expect(page.locator('main').first()).toBeVisible();
  });

  test('training CTA is reachable on mobile', async ({ page }) => {
    await page.setViewportSize({ width: 390, height: 844 });
    await page.goto(articlePath);

    await page.locator('a[href="/training/"], a[href="/training"]').first().click();
    await expect(page).toHaveURL(/\/training\/?$/);
    await expect(page.locator('main').first()).toBeVisible();
  });

  test('main navigation can open the blog index', async ({ page }) => {
    await page.goto('/');

    await expect(page.getByRole('navigation').first()).toBeVisible();
    await page.getByRole('link', { name: /blog|記事|articles/i }).first().click();
    await expect(page).toHaveURL(/blog/);
  });
});

다국어 사이트에서는 CTA 문구가 바뀔 수 있습니다. href가 중요한 계약이라면 href 기반 검사나 안정적인 data-testid를 함께 쓰세요.

flaky test를 줄이는 선택자

우선순위선택자예시이유
높음role과 namepage.getByRole('button', { name: /save/i })사용자가 보는 의미와 가깝습니다
높음labelpage.getByLabel(/email/i)폼 의미도 함께 확인합니다
중간textpage.getByText(/Start trial/)명확하지만 문구 변경에 약합니다
중간test idpage.getByTestId('checkout-submit')안정적인 비즈니스 액션에 적합합니다
낮음CSS 구조.card:nth-child(3)레이아웃 변경에 쉽게 깨집니다

page.waitForTimeout()은 마지막 수단도 아닙니다. toBeVisible(), toHaveURL(), toContainText()처럼 조건을 기다리는 assertion을 쓰세요.

Trace Viewer로 실패 읽기

trace: 'on-first-retry'를 켜면 실패 후 첫 재시도에서 DOM, 스크린샷, 네트워크, 콘솔 로그가 남습니다.

npx playwright test --trace on
npx playwright show-report
npx playwright show-trace test-results/path-to-trace/trace.zip

Claude Code에 실패를 전달할 때는 테스트 이름, trace에서 보인 화면 상태, 기대한 사용자 행동을 함께 줍니다. 그러면 단순 timeout 증가보다 선택자 개선, 데이터 준비, UI 버그 수정으로 이어질 가능성이 높습니다.

CI 예시

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

on:
  pull_request:
  push:
    branches: [main]

jobs:
  e2e:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    defaults:
      run:
        working-directory: site
    env:
      BASE_URL: http://127.0.0.1:4321
      PLAYWRIGHT_WEB_SERVER: "1"
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
          cache-dependency-path: site/package-lock.json
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm run build
      - run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: site/playwright-report
          retention-days: 7

retry는 flaky test를 고치는 기능이 아니라 분류하고 증거를 남기는 기능입니다. HTML report와 trace zip을 보지 않으면 같은 실패가 반복됩니다.

자주 생기는 함정

첫째, 모든 것을 E2E로 확인하려는 것입니다. 계산, 권한 분기, 필드 검증은 단위 또는 통합 테스트가 더 빠르고 정확합니다.

둘째, 인증 상태 파일을 커밋하는 것입니다. 전용 테스트 계정과 제한된 권한을 쓰고, playwright/.auth는 반드시 제외하세요.

셋째, retry로 불안정성을 숨기는 것입니다. retry로 통과했다면 아직 불안정한 테스트입니다.

넷째, Claude Code에 수정 범위를 넓게 주는 것입니다. 먼저 실패 테스트를 추가하고, trace를 읽고, 가장 작은 제품 수정으로 통과시키는 순서가 안전합니다.

혼자 시작한다면 ClaudeCodeLab products 템플릿으로 프롬프트를 정리하고, 팀 도입이라면 training으로 리뷰 기준과 CI 운영을 맞추는 편이 좋습니다.

이 흐름을 로컬 ClaudeCodeLab 스타일 글 페이지에 적용해 390px 스크린샷, 코드 블록 overflow, /products//training/ CTA, CI retry 설정을 확인했습니다. 가장 먼저 드러난 문제는 Playwright가 아니라 긴 코드 줄과 모호한 링크 이름이었습니다. 그 부분을 고치고 나니 Trace Viewer가 다음 Claude Code 프롬프트에 넣을 수 있는 실질적인 근거가 되었습니다.

#Claude Code #Playwright #E2E 테스트 #테스트 자동화 #품질 보증
무료

무료 PDF: Claude Code 치트시트

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

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

Masa

작성자 소개

Masa

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