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
'MCP' 카테고리의 다른 글
| MCP Tunnel 완전분석 — 방화벽 열지 않고 내부망 MCP 서버에 연결하는 방법 (0) | 2026.05.28 |
|---|---|
| Playwright MCP 실전 4편: vs 클라우드 브라우저 MCP — 뭘 써야 하나? 2026년 선택 기준 완전 정리 (0) | 2026.05.27 |
| Claude가 로그인까지 처리한다 — Playwright MCP 실전 2편: 자동화 패턴 완전 정복 (0) | 2026.05.27 |
| AI가 브라우저를 직접 조종한다 — Playwright MCP 1편: 설치부터 첫 자동화까지 (0) | 2026.05.27 |
| Supabase + Claude Code MCP 완전 가이드 — AI 에이전트가 Postgres를 올바르게 다루게 만드는 법 (1) | 2026.05.06 |