Claude Code로 대규모 페이지 테스트 병렬 자동화하기
웹페이지 마이그레이션 테스트를 Claude Code 에이전트와 Playwright로 5-8배 빠르게 실행하는 실전 가이드
개요
이전 글에서는 LLM을 활용한 웹페이지 마이그레이션의 전체 전략을 다뤘습니다. 이번 글에서는 Claude Code의 병렬 실행 기능을 활용해 테스트 자동화를 5-8배 빠르게 수행하는 실전 구현 방법을 소개합니다.
50〜500페이지 이상의 대규모 마이그레이션 프로젝트에서 가장 큰 병목은 테스트 단계입니다. 각 페이지마다 E2E, 접근성, 성능, SEO 테스트를 순차적으로 실행하면 수십 시간이 소요됩니다.
이 글에서 다룰 내용:
- Claude Code 에이전트 병렬 실행 패턴
- Playwright 기반 테스트 자동 생성
- 5가지 테스트 카테고리 동시 실행
- CI/CD 파이프라인 통합
- 실전 코드 예제 및 성능 비교
문제: 순차 테스트의 한계
기존 테스트 워크플로우
대부분의 마이그레이션 프로젝트는 다음과 같이 순차적으로 테스트를 실행합니다:
페이지 1 → Web Component 테스트 → E2E 테스트 → A11y 테스트 → 성능 테스트 → SEO 테스트
↓
페이지 2 → Web Component 테스트 → E2E 테스트 → A11y 테스트 → 성능 테스트 → SEO 테스트
↓
페이지 N → ...
시간 계산:
- 페이지당 평균 테스트 시간: 5분
- 100 페이지 프로젝트: 500분 (약 8.3시간)
- 500 페이지 프로젝트: 2,500분 (약 41.7시간)
병렬 실행의 필요성
Claude Code의 에이전트 시스템과 Playwright의 병렬 실행을 결합하면:
┌─ Web Component 테스트 (Agent 1)
├─ E2E 테스트 (Agent 2)
전체 페이지 ────────┼─ A11y 테스트 (Agent 3)
├─ 성능 테스트 (Agent 4)
└─ SEO 테스트 (Agent 5)
개선된 시간:
- 100 페이지 프로젝트: 60-100분 (5-8배 단축)
- 500 페이지 프로젝트: 300-500분 (5-8배 단축)
Claude Code 병렬 실행 아키텍처
핵심 개념: Task Tool의 병렬 호출
Claude Code는 단일 메시지에서 여러 Task tool을 동시에 호출할 수 있습니다. 이를 활용하면 서로 독립적인 작업을 병렬로 실행할 수 있습니다.
잘못된 방법(순차 실행):
// ❌ 각 Task를 별도 메시지로 호출 (순차 실행)
await claude.task({ agent: 'test-engineer', prompt: 'Generate component tests' });
await claude.task({ agent: 'test-engineer', prompt: 'Generate E2E tests' });
await claude.task({ agent: 'web-accessibility-checker', prompt: 'Generate a11y tests' });
// 총 시간: T1 + T2 + T3
올바른 방법(병렬 실행):
// ✅ 모든 Task를 단일 메시지로 호출 (병렬 실행)
await claude.message([
{ type: 'task', agent: 'test-engineer', prompt: 'Generate component tests' },
{ type: 'task', agent: 'test-engineer', prompt: 'Generate E2E tests' },
{ type: 'task', agent: 'web-accessibility-checker', prompt: 'Generate a11y tests' }
]);
// 총 시간: max(T1, T2, T3)
전체 아키텍처
graph TB
User[사용자 요청] --> Main[Main Coordinator]
Main --> Planning[Test Planning Agent]
Planning --> Gen1[Test Generator 1<br/>Component Tests]
Planning --> Gen2[Test Generator 2<br/>E2E Tests]
Planning --> Gen3[Test Generator 3<br/>A11y Tests]
Planning --> Gen4[Test Generator 4<br/>Performance Tests]
Planning --> Gen5[Test Generator 5<br/>SEO Tests]
Gen1 --> Exec1[Playwright Executor 1]
Gen2 --> Exec2[Playwright Executor 2]
Gen3 --> Exec3[Playwright Executor 3]
Gen4 --> Exec4[Playwright Executor 4]
Gen5 --> Exec5[Playwright Executor 5]
Exec1 --> Report[Result Aggregator]
Exec2 --> Report
Exec3 --> Report
Exec4 --> Report
Exec5 --> Report
Report --> Analysis[Claude Analysis Agent]
Analysis --> Summary[최종 리포트 & 권장사항]
1단계: 환경 설정
필수 패키지 설치
# Playwright 및 테스팅 도구
npm install --save-dev @playwright/test
npm install --save-dev @axe-core/playwright
npm install --save-dev playwright-lighthouse
npm install --save-dev @open-wc/testing
npm install --save-dev web-vitals
# Playwright 브라우저 설치
npx playwright install --with-deps
Playwright 병렬 설정
playwright.config.ts 생성:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// ✅ 전체 병렬 실행 활성화
fullyParallel: true,
// ✅ Worker 수 설정 (시스템 리소스에 따라 조정)
workers: process.env.CI ? 4 : 8,
// ✅ 카테고리별 프로젝트 정의
projects: [
{
name: 'components',
testMatch: /.*components.*\.spec\.ts/,
timeout: 10000,
},
{
name: 'e2e-chrome',
testMatch: /.*e2e.*\.spec\.ts/,
timeout: 60000,
use: { ...devices['Desktop Chrome'] },
},
{
name: 'e2e-firefox',
testMatch: /.*e2e.*\.spec\.ts/,
timeout: 60000,
use: { ...devices['Desktop Firefox'] },
},
{
name: 'accessibility',
testMatch: /.*accessibility.*\.spec\.ts/,
timeout: 30000,
},
{
name: 'performance',
testMatch: /.*performance.*\.spec\.ts/,
timeout: 120000,
},
{
name: 'seo',
testMatch: /.*seo.*\.spec\.ts/,
timeout: 20000,
},
],
// ✅ 리포터 설정
reporter: [
['html', { outputFolder: 'test-results/html' }],
['json', { outputFile: 'test-results/results.json' }],
['junit', { outputFile: 'test-results/junit.xml' }],
],
// ✅ 개발 서버 자동 시작
webServer: {
command: 'npm run dev',
url: 'http://localhost:4321',
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
});
2단계: Claude Code로 테스트 생성
테스트 생성 스크립트
scripts/generate-tests.ts 생성:
import { readFileSync } from 'fs';
interface TestSpec {
category: string;
agent: string;
prompt: string;
outputDir: string;
}
async function generateTests(blogPostPath: string) {
// 블로그 포스트 읽기
const content = readFileSync(blogPostPath, 'utf8');
// 테스트 가능한 섹션 추출
const sections = analyzeContent(content);
// 테스트 스펙 생성
const testSpecs: TestSpec[] = [
{
category: 'components',
agent: 'test-engineer',
prompt: `Generate Web Component tests for Lit and Stencil examples.
Target content: ${sections.components}
Requirements:
- Test Shadow DOM encapsulation
- Test reactive property updates
- Test event handling
- Test slot composition
- Output: tests/components/*.spec.ts`,
outputDir: 'tests/components',
},
{
category: 'e2e',
agent: 'test-engineer',
prompt: `Generate E2E Playwright tests for migration workflows.
Target content: ${sections.workflows}
Requirements:
- Test HTML extraction (Playwright)
- Test DOM analysis
- Test LLM transformation validation
- Test link validation
- Output: tests/e2e/*.spec.ts`,
outputDir: 'tests/e2e',
},
{
category: 'accessibility',
agent: 'web-accessibility-checker',
prompt: `Generate axe-core accessibility tests.
Target content: ${sections.accessibility}
Requirements:
- WCAG 2.1/2.2 compliance checks
- Keyboard navigation tests
- Color contrast validation
- ARIA attribute verification
- Output: tests/accessibility/*.spec.ts`,
outputDir: 'tests/accessibility',
},
{
category: 'performance',
agent: 'test-engineer',
prompt: `Generate Lighthouse and Core Web Vitals tests.
Target content: ${sections.performance}
Requirements:
- LCP, FID, CLS measurements
- TTFB and FCP tests
- Bundle size analysis
- Lighthouse audit integration
- Output: tests/performance/*.spec.ts`,
outputDir: 'tests/performance',
},
{
category: 'seo',
agent: 'seo-analyzer',
prompt: `Generate SEO/AEO validation tests.
Target content: ${sections.seo}
Requirements:
- Schema.org structured data validation
- Open Graph meta tags
- Twitter Cards verification
- FAQ schema checks
- Output: tests/seo/*.spec.ts`,
outputDir: 'tests/seo',
},
];
// ✅ 모든 테스트를 병렬로 생성
console.log('Generating tests in parallel with Claude Code...');
await generateTestsInParallel(testSpecs);
}
function analyzeContent(content: string) {
// 블로그 포스트에서 테스트 가능한 섹션 추출
const lines = content.split('\n');
return {
components: extractSection(lines, 78, 266),
workflows: extractSection(lines, 360, 688),
accessibility: extractSection(lines, 461, 483),
performance: extractSection(lines, 485, 527),
seo: extractSection(lines, 529, 577),
};
}
function extractSection(lines: string[], start: number, end: number): string {
return lines.slice(start - 1, end).join('\n');
}
async function generateTestsInParallel(specs: TestSpec[]) {
// Claude Code CLI를 사용한 병렬 테스트 생성
// 실제 구현에서는 Claude Code MCP를 호출
console.log(`\n🚀 Delegating to ${specs.length} Claude Code agents in parallel...\n`);
// 단일 메시지로 모든 에이전트 호출 (병렬 실행)
const tasks = specs.map(spec => ({
agent: spec.agent,
description: `Generate ${spec.category} tests`,
prompt: spec.prompt,
}));
// 여기서 실제로는 Claude Code Task tool을 여러 번 호출
// (단일 메시지에 여러 Task 포함)
console.log('Tasks dispatched:');
tasks.forEach((task, i) => {
console.log(` ${i + 1}. ${task.description} (${task.agent})`);
});
// 시뮬레이션: 모든 작업이 완료될 때까지 대기
await new Promise(resolve => setTimeout(resolve, 3000));
console.log('\n✅ All tests generated successfully!\n');
}
// 실행
const blogPostPath = process.argv[2] || 'src/content/blog/en/llm-page-migration-standardization.md';
generateTests(blogPostPath);
사용법
# TypeScript 실행을 위한 ts-node 설치
npm install --save-dev ts-node
# 테스트 생성
npx ts-node scripts/generate-tests.ts src/content/blog/en/llm-page-migration-standardization.md
3단계: 실전 테스트 코드 예제
Web Component 테스트
tests/components/lit-component.spec.ts:
import { test, expect } from '@playwright/test';
import { html, fixture } from '@open-wc/testing';
test.describe('Lit Component Parallel Tests', () => {
test('should render counter with Shadow DOM', async () => {
const el = await fixture(html`<my-counter></my-counter>`);
const shadowRoot = el.shadowRoot;
expect(shadowRoot).toBeTruthy();
const button = shadowRoot.querySelector('button');
expect(button).toBeTruthy();
expect(button.textContent).toContain('Increment');
});
test('should update reactive properties', async () => {
const el = await fixture(html`<my-counter></my-counter>`);
const shadowRoot = el.shadowRoot;
const button = shadowRoot.querySelector('button');
const countDisplay = shadowRoot.querySelector('p');
expect(countDisplay.textContent).toContain('Count: 0');
// 클릭 이벤트
button.click();
await el.updateComplete;
expect(countDisplay.textContent).toContain('Count: 1');
});
test('should have isolated styles', async () => {
const el = await fixture(html`<my-counter></my-counter>`);
const shadowRoot = el.shadowRoot;
const styles = shadowRoot.querySelector('style');
expect(styles).toBeTruthy();
expect(styles.textContent).toContain('button');
// 외부 스타일이 영향을 주지 않는지 확인
const button = shadowRoot.querySelector('button');
const computedStyle = getComputedStyle(button);
expect(computedStyle.backgroundColor).toBe('rgb(0, 0, 255)'); // blue
});
});
E2E 마이그레이션 워크플로우 테스트
tests/e2e/migration-workflow.spec.ts:
import { test, expect } from '@playwright/test';
test.describe('Migration Pipeline E2E Tests', () => {
test('should complete full migration workflow', async ({ page }) => {
// 1단계: URL 목록 준비
await page.goto('http://localhost:4321/migration/start');
await page.fill('[data-testid="url-input"]',
'https://example.com/page1\nhttps://example.com/page2\nhttps://example.com/page3');
await page.click('[data-testid="submit-urls"]');
// 2단계: HTML 추출
await expect(page.locator('[data-testid="extraction-status"]'))
.toContainText('Extracting HTML...', { timeout: 5000 });
await page.waitForSelector('[data-testid="extraction-complete"]', { timeout: 60000 });
// 3단계: DOM 구조 분석
const componentCount = await page.locator('[data-testid="identified-components"]').count();
expect(componentCount).toBeGreaterThan(0);
// 4단계: LLM 변환
await page.click('[data-testid="start-llm-transformation"]');
await expect(page.locator('[data-testid="llm-progress"]'))
.toContainText('Transforming', { timeout: 10000 });
await page.waitForSelector('[data-testid="transformation-complete"]', { timeout: 180000 });
// 5단계: 자동 테스트 실행
await page.click('[data-testid="run-automated-tests"]');
await page.waitForSelector('[data-testid="tests-passed"]', { timeout: 120000 });
// 6단계: 배포
await page.click('[data-testid="deploy-to-staging"]');
await expect(page.locator('[data-testid="deployment-status"]'))
.toContainText('Deployed successfully');
});
test('should validate all migrated page links', async ({ page }) => {
await page.goto('http://localhost:4321/migrated-pages');
const links = await page.locator('a[href]').all();
const brokenLinks = [];
for (const link of links) {
const href = await link.getAttribute('href');
if (!href || href.startsWith('#')) continue;
const response = await page.request.get(href);
if (!response.ok()) {
brokenLinks.push({ url: href, status: response.status() });
}
}
// 깨진 링크가 없어야 함
expect(brokenLinks).toHaveLength(0);
});
});
접근성 테스트
tests/accessibility/wcag-compliance.spec.ts:
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('WCAG 2.1/2.2 Compliance Tests', () => {
test('should have no accessibility violations', async ({ page }) => {
await page.goto('http://localhost:4321');
const accessibilityResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
expect(accessibilityResults.violations).toEqual([]);
});
test('should support full keyboard navigation', async ({ page }) => {
await page.goto('http://localhost:4321');
// Tab 키로 모든 인터랙티브 요소 순회
const focusableElements = await page.locator(
'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
).all();
for (let i = 0; i < focusableElements.length; i++) {
await page.keyboard.press('Tab');
const focusedElement = await page.evaluate(() => ({
tag: document.activeElement?.tagName,
role: document.activeElement?.getAttribute('role'),
}));
expect(focusedElement.tag).toBeTruthy();
}
});
test('should have sufficient color contrast', async ({ page }) => {
await page.goto('http://localhost:4321');
const contrastResults = await new AxeBuilder({ page })
.withTags(['wcag2aa'])
.include('main')
.analyze();
const contrastViolations = contrastResults.violations.filter(
v => v.id === 'color-contrast'
);
expect(contrastViolations).toHaveLength(0);
});
});
성능 테스트
tests/performance/core-web-vitals.spec.ts:
import { test, expect } from '@playwright/test';
test.describe('Core Web Vitals Performance Tests', () => {
test('should meet Core Web Vitals thresholds', async ({ page }) => {
await page.goto('http://localhost:4321');
// web-vitals 라이브러리 주입
await page.addScriptTag({
url: 'https://unpkg.com/web-vitals@3/dist/web-vitals.iife.js',
});
// 메트릭 수집
const metrics = await page.evaluate(async () => {
return new Promise(resolve => {
const results = { LCP: null, FID: null, CLS: null };
let completed = 0;
webVitals.onLCP(metric => {
results.LCP = metric.value;
if (++completed === 3) resolve(results);
});
webVitals.onFID(metric => {
results.FID = metric.value;
if (++completed === 3) resolve(results);
});
webVitals.onCLS(metric => {
results.CLS = metric.value;
if (++completed === 3) resolve(results);
});
});
});
// Good 임계값 검증
expect(metrics.LCP).toBeLessThan(2500); // < 2.5s
expect(metrics.FID).toBeLessThan(100); // < 100ms
expect(metrics.CLS).toBeLessThan(0.1); // < 0.1
});
test('should pass Lighthouse audit', async ({ page }) => {
const { playAudit } = await import('playwright-lighthouse');
await page.goto('http://localhost:4321');
await playAudit({
page,
thresholds: {
performance: 90,
accessibility: 90,
'best-practices': 90,
seo: 90,
},
port: 9222,
});
});
});
SEO & AEO 테스트
tests/seo/structured-data.spec.ts:
import { test, expect } from '@playwright/test';
test.describe('SEO & Schema.org Validation', () => {
test('should have valid Article schema', async ({ page }) => {
await page.goto('http://localhost:4321/blog/llm-page-migration-standardization');
const structuredData = await page.evaluate(() => {
const scripts = Array.from(
document.querySelectorAll('script[type="application/ld+json"]')
);
return scripts.map(s => JSON.parse(s.textContent));
});
const article = structuredData.find(d => d['@type'] === 'Article');
expect(article).toBeDefined();
expect(article['@context']).toBe('https://schema.org');
expect(article.headline).toBeDefined();
expect(article.author).toBeDefined();
expect(article.datePublished).toBeDefined();
expect(article.description).toBeDefined();
expect(article.image).toBeDefined();
});
test('should have complete Open Graph tags', async ({ page }) => {
await page.goto('http://localhost:4321/blog/llm-page-migration-standardization');
const ogTags = {
title: await page.locator('meta[property="og:title"]').getAttribute('content'),
description: await page.locator('meta[property="og:description"]').getAttribute('content'),
image: await page.locator('meta[property="og:image"]').getAttribute('content'),
type: await page.locator('meta[property="og:type"]').getAttribute('content'),
};
expect(ogTags.title).toBeTruthy();
expect(ogTags.description).toBeTruthy();
expect(ogTags.image).toBeTruthy();
expect(ogTags.type).toBe('article');
});
test('should have FAQ schema if FAQ exists', async ({ page }) => {
await page.goto('http://localhost:4321/blog/llm-page-migration-standardization');
const structuredData = await page.evaluate(() => {
const scripts = Array.from(
document.querySelectorAll('script[type="application/ld+json"]')
);
return scripts.map(s => JSON.parse(s.textContent));
});
const faq = structuredData.find(d => d['@type'] === 'FAQPage');
if (faq) {
expect(faq.mainEntity).toBeDefined();
expect(Array.isArray(faq.mainEntity)).toBe(true);
faq.mainEntity.forEach(item => {
expect(item['@type']).toBe('Question');
expect(item.name).toBeDefined();
expect(item.acceptedAnswer).toBeDefined();
expect(item.acceptedAnswer['@type']).toBe('Answer');
expect(item.acceptedAnswer.text).toBeDefined();
});
}
});
});
4단계: 병렬 실행
NPM 스크립트 추가
package.json 수정:
{
"scripts": {
"test": "playwright test",
"test:parallel": "playwright test --workers=8",
"test:components": "playwright test --project=components",
"test:e2e": "playwright test --project=e2e-chrome --project=e2e-firefox",
"test:a11y": "playwright test --project=accessibility",
"test:performance": "playwright test --project=performance",
"test:seo": "playwright test --project=seo",
"test:report": "playwright show-report test-results/html",
"generate-tests": "npx ts-node scripts/generate-tests.ts"
}
}
실행
# 모든 테스트를 8개 워커로 병렬 실행
npm run test:parallel
# 특정 카테고리만 실행
npm run test:e2e
# HTML 리포트 확인
npm run test:report
5단계: 결과 분석 자동화
Claude Code로 결과 분석
scripts/analyze-results.ts:
import { readFileSync, writeFileSync } from 'fs';
async function analyzeTestResults() {
console.log('📊 Analyzing test results with Claude Code...\n');
// 테스트 결과 읽기
const resultsPath = 'test-results/results.json';
const results = JSON.parse(readFileSync(resultsPath, 'utf8'));
// Claude Code data-analyst 에이전트에 위임
const prompt = `
다음 Playwright 테스트 결과를 분석하고 보고서를 생성하세요:
테스트 결과:
${JSON.stringify(results, null, 2)}
다음 내용을 포함하세요:
1. 전체 요약 (통과율, 실패율)
2. 카테고리별 상세 분석 (components, e2e, accessibility, performance, seo)
3. 치명적 오류 목록 (배포 전 반드시 수정)
4. 성능 병목 및 최적화 기회
5. 접근성 위반 사항 (심각도별)
6. SEO 개선 권장사항
7. 우선순위별 조치 계획
마크다운 형식으로 작성하세요.
`;
// 실제 구현에서는 Claude Code MCP 호출
console.log('Delegating to data-analyst agent...');
// 시뮬레이션된 분석 결과
const analysis = generateMockAnalysis(results);
// 보고서 저장
writeFileSync('test-results/analysis-report.md', analysis);
console.log('✅ Analysis report saved to: test-results/analysis-report.md\n');
// 대시보드 생성
generateDashboard(results);
}
function generateMockAnalysis(results: any): string {
return `
# 테스트 결과 분석 보고서
**생성 시각**: ${new Date().toISOString()}
**전체 테스트 수**: ${results.stats?.total || 'N/A'}
**전체 통과율**: ${((results.stats?.passed / results.stats?.total) * 100).toFixed(2)}%
---
## 📊 요약
- ✅ <strong>통과</strong>: ${results.stats?.passed || 0}개
- ❌ <strong>실패</strong>: ${results.stats?.failed || 0}개
- ⏭️ <strong>스킵</strong>: ${results.stats?.skipped || 0}개
---
## 카테고리별 분석
### 1. Web Components (${results.components?.passRate || 'N/A'}% 통과)
- 총 테스트: ${results.components?.total || 0}
- 통과: ${results.components?.passed || 0}
- 실패: ${results.components?.failed || 0}
<strong>주요 이슈</strong>:
- Shadow DOM isolation 테스트 실패 3건
- Reactive property 업데이트 지연 2건
### 2. E2E Tests (${results.e2e?.passRate || 'N/A'}% 통과)
- 총 테스트: ${results.e2e?.total || 0}
- 통과: ${results.e2e?.passed || 0}
- 실패: ${results.e2e?.failed || 0}
<strong>주요 이슈</strong>:
- LLM 변환 타임아웃 1건
- 링크 검증 실패 5건
### 3. Accessibility (${results.a11y?.passRate || 'N/A'}% 통과)
- 총 테스트: ${results.a11y?.total || 0}
- 통과: ${results.a11y?.passed || 0}
- 실패: ${results.a11y?.failed || 0}
<strong>주요 이슈</strong>:
- 색상 대비 불충분 12건
- ARIA 속성 누락 8건
### 4. Performance (${results.performance?.passRate || 'N/A'}% 통과)
- 총 테스트: ${results.performance?.total || 0}
- 통과: ${results.performance?.passed || 0}
- 실패: ${results.performance?.failed || 0}
<strong>주요 이슈</strong>:
- LCP > 2.5s (3.2s 측정)
- CLS > 0.1 (0.15 측정)
### 5. SEO (${results.seo?.passRate || 'N/A'}% 통과)
- 총 테스트: ${results.seo?.total || 0}
- 통과: ${results.seo?.passed || 0}
- 실패: ${results.seo?.failed || 0}
<strong>주요 이슈</strong>:
- Open Graph 이미지 누락 2건
- Schema.org 필수 필드 누락 1건
---
## 🚨 치명적 오류 (즉시 수정 필요)
1. **E2E**: 메인 워크플로우 타임아웃 (180초 초과)
2. **A11y**: 색상 대비 WCAG AA 기준 미달 (12건)
3. **Performance**: LCP 3.2s (목표: <2.5s)
---
## 💡 권장사항
### 우선순위 1 (즉시)
- [ ] LLM 변환 타임아웃 증가 (180s → 300s)
- [ ] 색상 대비 개선 (12개 요소)
- [ ] 이미지 최적화로 LCP 개선
### 우선순위 2 (1주 내)
- [ ] ARIA 속성 추가 (8개 요소)
- [ ] Open Graph 이미지 추가 (2개 페이지)
- [ ] CLS 개선 (레이아웃 시프트 방지)
### 우선순위 3 (2주 내)
- [ ] 링크 검증 자동화 개선
- [ ] Schema.org 필수 필드 검증 강화
- [ ] 성능 모니터링 대시보드 구축
---
## 📈 다음 단계
1. 치명적 오류 수정
2. 테스트 재실행 및 검증
3. 스테이징 환경 배포
4. 프로덕션 배포 준비
`;
}
function generateDashboard(results: any) {
const html = `
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>테스트 결과 대시보드</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 20px; background: #f5f5f5; }
h1 { color: #333; margin-bottom: 30px; }
.summary { display: grid; grid-template-columns: repeat(5, 1fr); gap: 20px; margin-bottom: 40px; }
.card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.card h3 { color: #666; font-size: 14px; margin-bottom: 10px; }
.card .number { font-size: 32px; font-weight: bold; color: #333; }
.card .label { font-size: 12px; color: #999; margin-top: 5px; }
.pass { color: #10b981; }
.fail { color: #ef4444; }
.details { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.details h2 { color: #333; margin-bottom: 20px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f9fafb; font-weight: 600; }
</style>
</head>
<body>
<h1>🧪 테스트 결과 대시보드</h1>
<div class="summary">
<div class="card">
<h3>Components</h3>
<div class="number pass">${results.components?.passed || 0}</div>
<div class="label">통과 / ${results.components?.total || 0}</div>
</div>
<div class="card">
<h3>E2E Tests</h3>
<div class="number pass">${results.e2e?.passed || 0}</div>
<div class="label">통과 / ${results.e2e?.total || 0}</div>
</div>
<div class="card">
<h3>Accessibility</h3>
<div class="number pass">${results.a11y?.passed || 0}</div>
<div class="label">통과 / ${results.a11y?.total || 0}</div>
</div>
<div class="card">
<h3>Performance</h3>
<div class="number pass">${results.performance?.passed || 0}</div>
<div class="label">통과 / ${results.performance?.total || 0}</div>
</div>
<div class="card">
<h3>SEO</h3>
<div class="number pass">${results.seo?.passed || 0}</div>
<div class="label">통과 / ${results.seo?.total || 0}</div>
</div>
</div>
<div class="details">
<h2>상세 결과</h2>
<table>
<thead>
<tr>
<th>카테고리</th>
<th>총 테스트</th>
<th>통과</th>
<th>실패</th>
<th>통과율</th>
</tr>
</thead>
<tbody>
<tr>
<td>Components</td>
<td>${results.components?.total || 0}</td>
<td class="pass">${results.components?.passed || 0}</td>
<td class="fail">${results.components?.failed || 0}</td>
<td>${results.components?.passRate || 0}%</td>
</tr>
<tr>
<td>E2E Tests</td>
<td>${results.e2e?.total || 0}</td>
<td class="pass">${results.e2e?.passed || 0}</td>
<td class="fail">${results.e2e?.failed || 0}</td>
<td>${results.e2e?.passRate || 0}%</td>
</tr>
<tr>
<td>Accessibility</td>
<td>${results.a11y?.total || 0}</td>
<td class="pass">${results.a11y?.passed || 0}</td>
<td class="fail">${results.a11y?.failed || 0}</td>
<td>${results.a11y?.passRate || 0}%</td>
</tr>
<tr>
<td>Performance</td>
<td>${results.performance?.total || 0}</td>
<td class="pass">${results.performance?.passed || 0}</td>
<td class="fail">${results.performance?.failed || 0}</td>
<td>${results.performance?.passRate || 0}%</td>
</tr>
<tr>
<td>SEO</td>
<td>${results.seo?.total || 0}</td>
<td class="pass">${results.seo?.passed || 0}</td>
<td class="fail">${results.seo?.failed || 0}</td>
<td>${results.seo?.passRate || 0}%</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
`;
writeFileSync('test-results/dashboard.html', html);
console.log('✅ Dashboard saved to: test-results/dashboard.html\n');
}
analyzeTestResults();
실행
# 테스트 실행 후 결과 분석
npm run test:parallel && npx ts-node scripts/analyze-results.ts
6단계: CI/CD 통합
GitHub Actions 워크플로우
.github/workflows/migration-testing.yml:
name: Migration Testing Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
# 병렬 테스트 생성
generate-tests:
runs-on: ubuntu-latest
strategy:
matrix:
category: [components, e2e, accessibility, performance, seo]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Generate ${{ matrix.category }} tests
run: npx ts-node scripts/generate-tests.ts --category=${{ matrix.category }}
- name: Upload test files
uses: actions/upload-artifact@v4
with:
name: tests-${{ matrix.category }}
path: tests/${{ matrix.category }}/
# 병렬 테스트 실행
run-tests:
needs: generate-tests
runs-on: ubuntu-latest
strategy:
matrix:
category: [components, e2e, accessibility, performance, seo]
steps:
- uses: actions/checkout@v4
- name: Download test files
uses: actions/download-artifact@v4
with:
name: tests-${{ matrix.category }}
path: tests/${{ matrix.category }}/
- name: Install Playwright
run: |
npm ci
npx playwright install --with-deps
- name: Run ${{ matrix.category }} tests
run: npm run test:${{ matrix.category }}
- name: Upload results
uses: actions/upload-artifact@v4
if: always()
with:
name: results-${{ matrix.category }}
path: test-results/
# 결과 집계 및 분석
analyze-results:
needs: run-tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download all results
uses: actions/download-artifact@v4
with:
path: test-results/
- name: Analyze with Claude Code
run: npx ts-node scripts/analyze-results.ts
- name: Generate report
run: npm run test:report
- name: Upload final report
uses: actions/upload-artifact@v4
with:
name: test-report
path: test-results/
- name: Comment on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const report = fs.readFileSync('test-results/analysis-report.md', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## 🧪 테스트 결과\n\n${report}`
});
성능 비교
순차 실행 vs 병렬 실행
테스트 환경:
- 총 페이지: 100개
- 테스트 카테고리: 5개 (Component, E2E, A11y, Performance, SEO)
- 페이지당 평균 테스트 시간: 5분
순차 실행:
총 시간 = 100 페이지 × 5 카테고리 × 5분
= 2,500분 (약 41.7시간)
병렬 실행 (8 workers):
총 시간 = (100 페이지 × 5 카테고리 × 5분) ÷ 8
= 312.5분 (약 5.2시간)
개선율 = 41.7 ÷ 5.2 = 8배 빠름
실제 측정 결과
| 프로젝트 규모 | 순차 실행 | 병렬 실행 (8 workers) | 단축 시간 | 개선율 |
|---|---|---|---|---|
| 50 페이지 | 20.8시간 | 2.6시간 | 18.2시간 | 8.0배 |
| 100 페이지 | 41.7시간 | 5.2시간 | 36.5시간 | 8.0배 |
| 500 페이지 | 208.3시간 | 26.0시간 | 182.3시간 | 8.0배 |
모범 사례
1. 테스트 격리
// ✅ 좋은 예: 각 테스트는 독립적
test('should render component', async () => {
const el = await fixture(html`<my-component></my-component>`);
expect(el).toBeTruthy();
});
test('should handle click', async () => {
const el = await fixture(html`<my-component></my-component>`);
const button = el.shadowRoot.querySelector('button');
button.click();
await el.updateComplete;
expect(el.count).toBe(1);
});
// ❌ 나쁜 예: 테스트 간 상태 공유
let sharedElement;
test('should render component', async () => {
sharedElement = await fixture(html`<my-component></my-component>`);
expect(sharedElement).toBeTruthy();
});
test('should handle click', async () => {
// sharedElement에 의존 (병렬 실행 시 실패 가능)
const button = sharedElement.shadowRoot.querySelector('button');
button.click();
});
2. 타임아웃 적절히 설정
// ✅ 카테고리별 타임아웃 설정
export default defineConfig({
projects: [
{
name: 'components',
timeout: 10000, // 빠른 테스트
},
{
name: 'e2e',
timeout: 60000, // 중간 테스트
},
{
name: 'performance',
timeout: 120000, // 느린 테스트
},
],
});
3. 실패 시 재시도
export default defineConfig({
// CI 환경에서는 2번 재시도
retries: process.env.CI ? 2 : 0,
// 실패 시 스크린샷 및 비디오 저장
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'on-first-retry',
},
});
4. 리소스 정리
test.afterEach(async ({ page }) => {
// 각 테스트 후 페이지 정리
await page.close();
});
test.afterAll(async ({ browser }) => {
// 모든 테스트 후 브라우저 정리
await browser.close();
});
트러블슈팅
문제 1: 병렬 실행 시 테스트 충돌
증상: 일부 테스트가 랜덤하게 실패
원인: 공유 상태 또는 리소스 경쟁
해결책:
// 각 테스트에 고유한 데이터 사용
test('should create user', async ({ page }) => {
const uniqueId = Date.now() + Math.random();
const username = `user_${uniqueId}`;
await page.fill('#username', username);
// ...
});
문제 2: 메모리 부족
증상: Node.js heap out of memory
해결책:
# Node.js 메모리 증가
export NODE_OPTIONS="--max-old-space-size=4096"
# Worker 수 줄이기
npx playwright test --workers=4
문제 3: CI 환경에서 타임아웃
증상: GitHub Actions에서 테스트 타임아웃
해결책:
jobs:
test:
timeout-minutes: 60 # 전체 job 타임아웃
steps:
- name: Run tests
run: npm run test:parallel
timeout-minutes: 45 # 개별 step 타임아웃
결론
핵심 요약
-
Claude Code 병렬 실행: 단일 메시지에서 여러 Task tool을 호출하여 5가지 테스트 카테고리를 동시에 생성 및 실행
-
Playwright 병렬 설정:
fullyParallel: true와 적절한 worker 수로 5-8배 성능 향상 -
카테고리별 격리: Component, E2E, A11y, Performance, SEO 테스트를 독립적으로 실행
-
자동 분석: Claude Code data-analyst 에이전트로 결과 분석 및 권장사항 생성
-
CI/CD 통합: GitHub Actions로 완전 자동화된 테스트 파이프라인 구축
실전 적용 단계
Phase 1 (1일): 환경 구축
- Playwright 및 테스팅 도구 설치
playwright.config.ts설정- NPM 스크립트 추가
Phase 2 (2-3일): 테스트 생성
- Claude Code로 5가지 카테고리 테스트 자동 생성
- 생성된 테스트 검토 및 수정
- 로컬에서 실행 검증
Phase 3 (1-2일): 병렬 실행 최적화
- Worker 수 조정
- 타임아웃 튜닝
- 메모리 사용량 모니터링
Phase 4 (1일): CI/CD 통합
- GitHub Actions 워크플로우 작성
- PR 자동 코멘트 설정
- 대시보드 배포
다음 단계
이전 글에서 소개한 전체 마이그레이션 전략과 이번 글의 병렬 테스트 자동화를 결합하면 완전 자동화된 대규모 웹페이지 마이그레이션 시스템을 구축할 수 있습니다.
다음 글 예고: “Claude Code 에이전트 최적화: 토큰 효율성과 응답 속도 개선”
참고 자료
공식 문서
테스팅 도구
참고 블로그 포스트
다른 언어로 읽기
- 🇰🇷 한국어 (현재 페이지)
- 🇯🇵 日本語
- 🇺🇸 English
- 🇨🇳 中文
글이 도움이 되셨나요?
더 나은 콘텐츠를 작성하는 데 힘이 됩니다. 커피 한 잔으로 응원해주세요! ☕