MCP

Playwright MCP 실전 3편: 프로덕션에서 쓰는 법 — 에러 처리, 병렬 실행, CI 파이프라인 완전 정복

cell-devlog 2026. 5. 27. 11:36
반응형

1·2편으로 설치와 실전 자동화를 익혔다면 이제 "데모 수준"에서 "실제 돌아가는 시스템"으로 넘어갈 차례다. MCP 기반 자동화가 실전에서 무너지는 이유는 대부분 세 가지 — 예측 못 한 에러, 느린 직렬 실행, CI 환경에서의 불안정함이다. 이번 편에서 전부 잡는다.


핵심 요약

→ Playwright MCP는 탐색·생성 레이어, 프로덕션 회귀 테스트 파이프라인은 결정론적 .spec.ts 스크립트로 분리하는 게 정석
에러 핸들링 3원칙 — 오버레이(모달/배너) 먼저 닫기, 재시도는 정확히 2회, 타임아웃은 전역 올리지 말고 해당 테스트만 늘리기
병렬 실행 — workers 옵션으로 단일 머신 내 병렬화, --shard로 CI 머신 간 분산
CI 황금 규칙 — node_modules + 브라우저 바이너리(200~400MB) 캐시, forbidOnly: true, 아티팩트 if: always() 업로드
→ Docker 공식 이미지 mcr.microsoft.com/playwright 로 브라우저 설치 과정 완전히 생략 가능
→ 플레이키(flaky) 테스트 비율 2% 초과 → CI 신뢰도 붕괴 신호, 즉시 대응 필요
MCP 버전 프로덕션에서 반드시 핀 고정 — @playwright/mcp@latest 아닌 @playwright/mcp@1.x.x 형태로


1. MCP vs 결정론적 스크립트 — 무엇을 어디에 쓰나

프로덕션 아키텍처의 핵심은 역할 분리다.

# ✅ 올바른 역할 분리

[Playwright MCP + AI]              [결정론적 Playwright 스크립트]
────────────────────────           ──────────────────────────────
탐색적 테스트                       CI 회귀 테스트
새 기능 플로우 발견                  매 커밋마다 실행
셀렉터 초안 생성                     검증된 getByRole 셀렉터
UI 변경 후 첫 탐색                   안정된 assertion 로직
일회성 자동화 작업                   반복 실행 파이프라인

MCP로 발견한 플로우를 .spec.ts로 코드화 → CI에서 돌린다. 이 두 단계를 섞으면 재현 불가능한 실패가 쌓인다.

// MCP가 탐색으로 발견 → 사람이 검토 → spec으로 확정

// ❌ MCP 결과를 그대로 CI에 넣으면 안 됨
// LLM 출력은 비결정론적 → 같은 프롬프트도 결과가 매번 다를 수 있음

// ✅ MCP가 생성한 초안을 검토 후 확정
import { test, expect } from '@playwright/test';

test('로그인 플로우', async ({ page }) => {
  await page.goto('https://app.example.com/login');
  // MCP가 실제 DOM에서 뽑아준 셀렉터 — 추측 아님
  await page.getByRole('textbox', { name: '이메일' }).fill('test@example.com');
  await page.getByRole('textbox', { name: '비밀번호' }).fill('Test1234!');
  await page.getByRole('button', { name: '로그인' }).click();
  await expect(page.getByText('대시보드')).toBeVisible();
});

2. 에러 핸들링 패턴

2-1. 오버레이 처리 — 가장 흔한 실패 원인

쿠키 배너, 팝업, 모달이 요소를 덮고 있으면 클릭이 차단된다. AI에게 먼저 오버레이를 닫으라고 지시하는 습관이 필요하다.

# ❌ 자주 실패하는 프롬프트
"로그인 버튼 클릭해줘"

# ✅ 오버레이 먼저 처리
"페이지에 쿠키 동의 배너나 팝업이 있으면 먼저 닫고, 그 다음 로그인 버튼 클릭해줘"
// 코드로 처리할 때 — 오버레이 감지 후 무시하고 진행
test('오버레이 안전 클릭', async ({ page }) => {
  await page.goto('https://app.example.com');

  // 쿠키 배너가 있으면 닫기, 없으면 패스
  const cookieBanner = page.getByRole('button', { name: /동의|Accept|닫기/ });
  if (await cookieBanner.isVisible({ timeout: 3000 }).catch(() => false)) {
    await cookieBanner.click();
  }

  // 이후 정상 진행
  await page.getByRole('button', { name: '로그인' }).click();
});

2-2. 재시도 전략 — 2회가 정답

// playwright.config.ts

import { defineConfig } from '@playwright/test';

export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  // ✅ CI에서 2회 재시도 — 네트워크/타이밍 노이즈 흡수
  // ❌ 3회 이상은 실제 버그를 숨김

  use: {
    // 기본 타임아웃 설정
    actionTimeout: 10_000,    // 클릭/입력 등 단일 액션: 10초
    navigationTimeout: 30_000, // 페이지 이동: 30초
  },

  timeout: 30_000, // 테스트 1개당 기본 타임아웃: 30초
  // ⚠️ 전역 타임아웃을 올려서 느린 테스트 덮으면 안 됨
  // 해당 테스트만 개별 설정할 것
});
// 복잡한 UI 플로우는 해당 테스트만 타임아웃 늘리기
test('복잡한 체크아웃 플로우', async ({ page }) => {
  test.setTimeout(90_000); // 이 테스트만 90초로 늘림

  // ... 긴 플로우
});

2-3. trace + video로 실패 재현

// playwright.config.ts

export default defineConfig({
  use: {
    trace: 'on-first-retry',        // 첫 재시도 때만 트레이스 수집 (스토리지 절약)
    video: 'retain-on-failure',     // 실패한 테스트만 비디오 저장
    screenshot: 'only-on-failure',  // 실패 시에만 스크린샷
  },
});
# 트레이스 파일 로컬에서 분석
npx playwright show-trace test-results/trace.zip
# → 브라우저에서 타임라인 + DOM 스냅샷 + 네트워크 요청 전체 확인 가능

2-4. MCP 버전 프로덕션 핀 고정

// ❌ latest는 언제 바뀔지 모름
{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest"]
    }
  }
}

// ✅ 버전 고정 — 예상치 못한 동작 변경 방지
{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@1.50.0"]
      // package.json에서 현재 버전 확인: npx @playwright/mcp --version
    }
  }
}

3. 병렬 실행 — 실행 시간 단축

3-1. 단일 머신 내 병렬화 (workers)

// playwright.config.ts

export default defineConfig({
  // 로컬: CPU 코어의 절반 사용
  workers: process.env.CI ? 1 : undefined,
  // ↑ CI에서 workers=1 권장 — 불안정한 공유 환경에서 안정성 우선
  // 로컬에서는 undefined (Playwright가 CPU 코어 수 기반 자동 설정)

  fullyParallel: true, // 파일 간 완전 병렬 실행
});
// 테스트 간 상태 격리 — 병렬 실행의 핵심 조건
test.describe('사용자 설정', () => {
  // 각 테스트가 독립적인 브라우저 컨텍스트 사용
  test.use({ storageState: './auth/user-session.json' });

  test('프로필 수정', async ({ page }) => { /* ... */ });
  test('알림 설정', async ({ page }) => { /* ... */ });
  // ↑ 두 테스트가 동시에 실행돼도 서로 간섭 없음
});

3-2. CI 머신 간 샤딩 (GitHub Actions)

테스트 스위트가 10분 이상이면 샤딩으로 여러 CI 머신에 분산한다.

# .github/workflows/playwright.yml

name: Playwright E2E Tests

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

jobs:
  playwright:
    name: "Playwright 샤드 ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}"
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false  # 한 샤드 실패해도 나머지 계속 실행
      matrix:
        shardIndex: [1, 2, 3, 4]  # 4개 머신에 분산
        shardTotal: [4]

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'npm'  # node_modules 캐시

      - name: 의존성 설치
        run: npm ci

      # 브라우저 바이너리 캐시 (200~400MB — 캐시 없으면 매번 다운로드)
      - name: 브라우저 캐시 로드
        uses: actions/cache@v4
        id: playwright-cache
        with:
          path: ~/.cache/ms-playwright
          key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}

      - name: Playwright 브라우저 설치
        if: steps.playwright-cache.outputs.cache-hit != 'true'
        run: npx playwright install --with-deps chromium

      - name: 테스트 실행 (샤드)
        run: |
          npx playwright test \
            --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} \
            --reporter=blob  # 샤드별 결과를 blob으로 저장 (나중에 합침)
        env:
          CI: true

      # 실패해도 아티팩트 반드시 업로드
      - name: 테스트 결과 업로드
        if: always()  # ← 이게 핵심 — 실패해도 올림
        uses: actions/upload-artifact@v4
        with:
          name: playwright-blob-${{ matrix.shardIndex }}
          path: blob-report/
          retention-days: 7

  # 전체 샤드 완료 후 리포트 합치기
  merge-reports:
    needs: playwright
    runs-on: ubuntu-latest
    if: always()

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'npm'
      - run: npm ci

      - name: 각 샤드 결과 다운로드
        uses: actions/download-artifact@v4
        with:
          path: all-blob-reports/
          pattern: playwright-blob-*
          merge-multiple: true

      - name: 리포트 병합
        run: npx playwright merge-reports --reporter html ./all-blob-reports

      - name: 최종 HTML 리포트 업로드
        uses: actions/upload-artifact@v4
        with:
          name: playwright-html-report
          path: playwright-report/
          retention-days: 14

4. Docker로 환경 완전 통일

로컬에서 되는데 CI에서 안 되는 현상의 80%는 브라우저 환경 차이 때문이다. Docker로 완전히 통일한다.

# .github/workflows/playwright-docker.yml

jobs:
  playwright:
    runs-on: ubuntu-latest
    container:
      image: mcr.microsoft.com/playwright:v1.50.0-jammy
      # ↑ Microsoft 공식 Playwright Docker 이미지 — 브라우저 설치 불필요
      # 버전 고정 필수 (latest 쓰면 언제 바뀔지 모름)

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'npm'
      - run: npm ci

      - name: 테스트 실행
        run: npx playwright test
        env:
          CI: true
          HOME: /root  # Docker 환경에서 필수

      - name: 결과 업로드
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
# Docker 사용의 장점
✅ 브라우저 설치 시간 0초 (이미지에 포함)
✅ 로컬과 CI 환경 100% 동일 → "로컬엔 되는데 CI에서 안 됨" 없앰
✅ 비주얼 리그레션 테스트에서 픽셀 완벽 일치 가능 (OS 폰트 렌더링 차이 제거)

5. 플레이키 테스트 — 진짜 버그 vs 노이즈 구분

// playwright.config.ts

export default defineConfig({
  retries: 2,

  reporter: [
    ['html'],
    ['json', { outputFile: 'test-results/results.json' }],
    // ↑ JSON 리포트로 플레이키 비율 추적 가능
  ],
});
# 플레이키 테스트 판별 기준
- 2번 재시도 후 통과 → 플레이키 (불안정 환경 노이즈)
  → Playwright가 HTML 리포트에 "flaky" 표시
  → 즉시 원인 분석 필요 (보통 타이밍 문제 or 상태 누수)

- 3번 모두 실패 → 진짜 버그
  → 코드/UI 변경 확인

# 플레이키 비율 2% 초과 = CI 신뢰도 붕괴 신호
# 팀이 "어차피 재실행하면 되지" 멘탈이 되기 시작함 → 즉각 대응
// 가장 흔한 플레이키 원인과 해결법

// ❌ 고정 wait — 환경마다 다른 로딩 속도에 무너짐
await page.waitForTimeout(2000);

// ✅ 조건부 wait — 실제 상태 변화를 기다림
await page.waitForSelector('[data-testid="dashboard"]');
await expect(page.getByText('로딩 완료')).toBeVisible();

// ❌ 순서 의존 테스트 — 병렬 실행 시 서로 간섭
test('상품 추가 후 삭제', async ({ page }) => {
  // 이전 테스트가 추가한 상품이 있다고 가정 — 병렬에서 깨짐
});

// ✅ 각 테스트가 자신의 상태를 직접 준비
test('상품 삭제', async ({ page }) => {
  await createTestProduct(); // 이 테스트에서 직접 생성
  // ... 삭제 로직
});

6. MCP + CI 하이브리드 아키텍처 전체 그림

# 프로덕션 추천 아키텍처

개발 중
└── Playwright MCP + Claude
    ├── 새 기능 UI 탐색
    ├── 셀렉터 초안 생성
    └── 일회성 자동화 처리
          ↓ 검토·확정
PR 단계
└── GitHub Actions (샤딩 4개)
    ├── 결정론적 .spec.ts 실행
    ├── 실패 시 trace + video 수집
    └── HTML 리포트 업로드

나이틀리 빌드
└── GitHub Actions + Docker
    ├── 전체 회귀 테스트
    ├── 비주얼 리그레션
    └── 플레이키 비율 모니터링 (2% 기준)

✅ 프로덕션 체크리스트

✅ MCP 버전 핀 고정 (@playwright/mcp@1.x.x)
✅ retries: 2 — CI 노이즈 흡수, 3 이상은 버그 은폐
✅ 브라우저 바이너리 캐시 설정 (200~400MB 절약)
✅ if: always() 아티팩트 업로드 — 실패해도 리포트 확인 가능
✅ forbidOnly: true — test.only 실수로 CI 통과되는 것 방지
✅ trace: 'on-first-retry' — 첫 재시도 때만 트레이스 수집
✅ Docker 이미지로 로컬·CI 환경 완전 통일
✅ 플레이키 비율 주기적 모니터링

❌ 전역 타임아웃 올려서 느린 테스트 덮기
❌ waitForTimeout 고정 대기 사용
❌ CI에서 LLM 직접 호출로 회귀 테스트 — 비결정론적, 토큰 낭비

 

✅ 시리즈 전체 요약

 1편 — Playwright MCP 설치 + Claude Desktop/Cursor/VS Code 연결 https://cell-devlog.tistory.com/277
 2편 — 자연어 스크래핑·폼 자동화·로그인 세션 영구 유지 https://cell-devlog.tistory.com/278
 3편 — 에러 핸들링·샤딩·Docker·GitHub Actions CI 파이프라인 https://cell-devlog.tistory.com/279
 4편 — 클라우드 브라우저 MCP 생태계 비교 + 상황별 선택 기준 https://cell-devlog.tistory.com/280

 

반응형