je개발 회고

[ E2E ] Playwright + Next.js로 멀티플랫폼 E2E 테스트 환경 구축하기

Je-chan 2025. 6. 24. 10:41

🎯 프로젝트 개요

안녕하세요. 오늘은 여러 플랫폼의 E2E 테스트를 효율적으로 관리할 수 있는 통합 대시보드를 구축한 경험을 공유하려고 합니다.

현대의 개발 환경에서는 하나의 회사가 여러 개의 서비스나 플랫폼을 운영하는 경우가 많습니다. 각각의 서비스마다 다른 도메인을 가지고 있고, 서로 다른 테스트 환경이 필요한데요. 이런 상황에서 E2E 테스트를 개별적으로 관리하다 보면 여러 문제점들이 발생합니다.

해결하고자 했던 문제들

1. 분산된 테스트 관리의 복잡성

  • 각 플랫폼별로 서로 다른 playwright.config.js 설정을 개별 관리
  • 테스트 실행을 위해 여러 터미널을 오가며 명령어 입력
  • 테스트 결과 확인을 위해 각각의 리포트 파일을 개별 확인

2. 테스트 진행 상황의 불투명성

  • 테스트가 실행되는 동안 현재 진행 상황을 파악하기 어려움
  • 테스트 완료 여부와 소요 시간 예측 불가

3. 결과 확인의 번거로움

  • 각 플랫폼별 테스트 결과를 개별적으로 찾아서 확인
  • 통합된 관점에서 전체 테스트 현황 파악 어려움

구현한 핵심 기능

  • 🌐 멀티플랫폼 지원: 각 플랫폼별 독립적인 Playwright 설정 관리
  • 🖱️ UI 기반 테스트 실행: 웹 인터페이스에서 버튼 클릭으로 간편한 테스트 실행
  • 📊 실시간 진행률 표시: Server-Sent Events를 통한 실시간 프로그레스 바와 로그 출력
  • 📄 통합 결과 관리: HTML 리포트 자동 생성 및 웹에서 즉시 확인

🏗️ 시스템 아키텍처

전체 시스템 구조

🏗️ E2E 테스트 시스템 아키텍처

📱 Frontend Layer (Next.js)
메인 대시보드
플랫폼 선택
테스트 실행
⚡ Backend Layer (API)
API 호출
SSE 스트림
프로세스 관리
🎭 Playwright Layer
설정 로드
테스트 실행
리포트 생성

테스트 실행 플로우

🔄 테스트 실행 시퀀스

 
1
사용자 액션
플랫폼 선택 → "테스트 실행" 버튼 클릭
2
API 처리
POST /api/run-test/[platform] → Playwright 프로세스 시작
3
실시간 업데이트
SSE를 통한 진행률 및 로그 전송
4
테스트 완료
HTML 리포트 생성 → 결과 확인 가능

데이터 플로우

📊 데이터 플로우 다이어그램

📝
입력
플랫폼 선택
테스트 설정
실행 명령
⚙️
처리
설정 로드
Playwright 실행
테스트 수행
📤
출력
JSON 결과
HTML 리포트
스크린샷
실시간 피드백: 진행률 업데이트 + 상세 로그 출력

📁 프로젝트 구조

📦 e2e-dashboard/
├── 📂 src/app/                      # Next.js App Router
│   ├── 📂 api/                      # API Routes
│   │   ├── 📂 run-test/[platform]/  # 플랫폼별 테스트 실행
│   │   ├── 📂 serve-report/         # 리포트 서빙
│   │   └── 📂 health/               # 헬스체크
│   ├── 📄 page.tsx                  # 메인 대시보드
│   └── 📄 layout.tsx                # 레이아웃
├── 📂 platforms/                    # 플랫폼별 테스트
│   ├── 📂 service-a/                # 첫 번째 서비스
│   │   ├── ⚙️ playwright.config.ts
│   │   ├── 📂 tests/
│   │   ├── 📂 fixtures/
│   │   └── 📂 utils/
│   └── 📂 service-b/                # 두 번째 서비스
│       ├── ⚙️ playwright.config.ts
│       └── 📂 tests/
├── 📂 src/entities/                 # 도메인 엔티티
│   ├── 📂 log/                      # 로그 관리
│   └── 📂 test/                     # 테스트 관리
├── 📂 src/features/                 # 기능 모듈
│   ├── 📂 runTest/                  # 테스트 실행 기능
│   └── 📂 testManagement/           # 테스트 관리 기능
├── 📂 playwright-report/            # 테스트 결과
│   ├── 📂 service-a/
│   └── 📂 service-b/
└── 🐳 Dockerfile                    # Docker 설정

⚙️ 핵심 구현 사항

1. 플랫폼별 Playwright 설정 관리

각 플랫폼마다 독립적인 설정을 가질 수 있도록 구조화했습니다:

// platforms/service-a/playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

const BASE_URL = process.env.SERVICE_A_URL || 'https://service-a.example.com';

export default defineConfig({
  testDir: './tests',
  timeout: 30 * 1000,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : 2,

  reporter: [
    ['list'],
    ['html', { outputFolder: '../../playwright-report/service-a', open: 'never' }],
    ['json', { outputFile: '../../playwright-report/service-a/results.json' }],
  ],

  use: {
    baseURL: BASE_URL,
    headless: true,
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    viewport: { width: 1920, height: 1080 },
    ignoreHTTPSErrors: true,
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});

 

2. 실시간 테스트 실행 API

Server-Sent Events를 활용하여 테스트 진행 상황을 실시간으로 전송합니다:

// src/app/api/run-test/[platform]/route.ts
export async function GET(request: NextRequest, { params }: { params: { platform: string } }) {
  const platform = params.platform;
  const supportedPlatforms = ['service-a', 'service-b'];
  
  if (!supportedPlatforms.includes(platform)) {
    return Response.json({ success: false, error: 'Unsupported platform' }, { status: 400 });
  }

  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    start(controller) {
      try {
        const configPath = path.join(process.cwd(), 'platforms', platform, 'playwright.config.ts');
        const playwrightArgs = [
          'playwright',
          'test',
          '--config',
          configPath,
          '--reporter=list,json,html',
        ];

        const process = spawn('npx', playwrightArgs, {
          cwd: path.join(process.cwd(), 'platforms', platform)
        });

        process.stdout.on('data', (data) => {
          const message = data.toString();
          controller.enqueue(
            encoder.encode(`data: ${JSON.stringify({ 
              type: 'log', 
              message 
            })}\n\n`)
          );
        });

        process.on('close', (code) => {
          controller.enqueue(
            encoder.encode(`data: ${JSON.stringify({ 
              type: 'complete', 
              code 
            })}\n\n`)
          );
          controller.close();
        });

      } catch (error) {
        controller.error(error);
      }
    }
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  });
}

 

3. 실시간 진행률 표시 컴포넌트

 

💻 React 컴포넌트 구조
State 관리
• isRunning: 실행 상태
• logs: 실시간 로그
• progress: 진행률
SSE 연결
• EventSource 생성
• 메시지 수신 처리
• 실시간 UI 업데이트

 

 

React에서 SSE를 활용하여 실시간으로 테스트 진행 상황을 표시합니다:

// src/features/runTest/TestRunner.tsx
'use client';

import { useState, useEffect } from 'react';

export default function TestRunner({ platform }: { platform: string }) {
  const [isRunning, setIsRunning] = useState(false);
  const [logs, setLogs] = useState<string[]>([]);
  const [progress, setProgress] = useState(0);

  const runTest = async () => {
    setIsRunning(true);
    setLogs([]);
    setProgress(0);

    const eventSource = new EventSource(`/api/run-test/${platform}`);

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      
      if (data.type === 'log') {
        setLogs(prev => [...prev, data.message]);
        updateProgress(data.message);
      } else if (data.type === 'complete') {
        setIsRunning(false);
        setProgress(100);
        eventSource.close();
      }
    };

    eventSource.onerror = () => {
      setIsRunning(false);
      eventSource.close();
    };
  };

  const updateProgress = (logMessage: string) => {
    // 로그 메시지를 파싱하여 진행률 계산
    if (logMessage.includes('Running test')) {
      setProgress(prev => Math.min(prev + 10, 90));
    }
  };

  return (
    <div className="space-y-4">
      <button 
        onClick={runTest}
        disabled={isRunning}
        className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
      >
        {isRunning ? '테스트 실행 중...' : `${platform} 테스트 실행`}
      </button>

      {isRunning && (
        <div className="w-full bg-gray-200 rounded-full h-2.5">
          <div 
            className="bg-blue-600 h-2.5 rounded-full transition-all duration-300"
            style={{ width: `${progress}%` }}
          />
        </div>
      )}

      <div className="h-64 overflow-y-auto bg-black text-green-400 p-4 font-mono text-sm">
        {logs.map((log, index) => (
          <div key={index}>{log}</div>
        ))}
      </div>
    </div>
  );
}

 

4. 환경 변수 설정

각 플랫폼별로 환경 변수를 분리하여 관리합니다:

# .env.local
SERVICE_A_URL="https://service-a.example.com"
SERVICE_A_LOGIN_ID="test_user"
SERVICE_A_LOGIN_PW="test_password"

SERVICE_B_URL="https://service-b.example.com"
SERVICE_B_LOGIN_ID="admin_user"
SERVICE_B_LOGIN_PW="admin_password"

NODE_ENV="development"

🐳 Docker 배포

프로덕션 환경에서의 일관성을 위해 Docker를 활용했습니다:

# Dockerfile
FROM mcr.microsoft.com/playwright:v1.52.0-noble AS base

WORKDIR /app

# pnpm 설치
RUN npm install -g pnpm

# 의존성 설치
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

# 소스 코드 복사 및 빌드
COPY . .
RUN pnpm run build

# 리포트 볼륨 설정
VOLUME [ "/app/playwright-report" ]

# 환경 변수 설정
ENV NODE_ENV="production"
ENV SERVICE_A_URL="https://service-a.example.com"
ENV SERVICE_B_URL="https://service-b.example.com"

EXPOSE 3000

ENTRYPOINT [ "pnpm", "run", "start" ]
# docker-compose.yml
version: '3.8'

services:
  e2e-dashboard:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - ./playwright-report:/app/playwright-report
      - ./platforms:/app/platforms:ro
    environment:
      - NODE_ENV=production
    restart: unless-stopped

📊 실제 사용 경험

개선된 워크플로우

❌ 이전 방식
  • 여러 터미널 실행
  • 명령어 개별 입력
  • 진행 상황 불투명
  • 결과 파일 개별 확인
✅ 개선된 방식
  • 통합 웹 대시보드
  • 버튼 클릭으로 실행
  • 실시간 진행률 확인
  • 통합 결과 확인

 

얻은 효과

  1. 테스트 실행의 접근성 향상: 개발자가 아닌 팀원도 쉽게 E2E 테스트 실행 가능
  2. 시간 절약: 여러 터미널과 파일을 오가는 시간 단축
  3. 투명성 증대: 실시간 진행 상황으로 테스트 현황 명확히 파악
  4. 결과 확인 효율성: 통합된 인터페이스에서 모든 결과 한 번에 확인

 

향후 개선 계획

🚀 로드맵 & 개선 계획

📅 단기 계획 (1-2개월)
  • 🔔 슬랙 알림 연동
  • ⏰ 테스트 스케줄링
  • 📊 대시보드 통계
  • 🔄 자동 재실행 옵션
🎯 중기 계획 (3-6개월)
  • 📈 히스토리 관리
  • 🤖 AI 기반 분석
  • 📱 모바일 앱 지원
  • 🔗 CI/CD 파이프라인 통합

 

🔍 트러블슈팅 & 팁

자주 발생하는 문제들

⚠️ 주요 트러블슈팅 가이드
문제: SSE 연결이 자주 끊어짐
해결: Keep-alive 헤더 추가 및 reconnection 로직 구현
문제: Docker 환경에서 Playwright 브라우저 실행 실패
해결: --no-sandbox, --disable-dev-shm-usage 옵션 추가
문제: 여러 테스트 동시 실행 시 리소스 부족
해결: 동시 실행 제한 및 큐 시스템 도입

 

성능 최적화 팁

💡 성능 최적화 체크리스트
⚡ 실행 속도
• headless 모드 활용
• 불필요한 리소스 로딩 차단
• 병렬 테스트 worker 조정
💾 메모리 관리
• 테스트 간 컨텍스트 정리
• 스크린샷/비디오 제한
• 로그 파일 크기 관리

🎯 마무리

🎉 프로젝트 완료 후기

이번 프로젝트를 통해 여러 플랫폼의 E2E 테스트를 효율적으로 관리할 수 있는 통합 환경을 구축할 수 있었습니다. Playwright의 강력한 테스트 기능과 Next.js의 현대적인 웹 애플리케이션 개발 능력을 결합하여, 개발팀의 생산성을 크게 향상시킬 수 있었습니다.

🚀
생산성 향상
👥
팀 협업 개선
품질 관리 강화

특히 Server-Sent Events를 활용한 실시간 피드백 시스템은 테스트 진행 상황의 투명성을 높여주어, 팀원들이 더 적극적으로 E2E 테스트를 활용하게 되었습니다.

 

 

비슷한 환경에서 E2E 테스트 관리에 어려움을 겪고 있는 팀에게 도움이 되었으면 좋겠습니다.

궁금한 점이나 개선 아이디어가 있으시면 언제든 댓글로 남겨주세요! 여러분의 피드백이 더 나은 개발 환경을 만드는 데 큰 도움이 됩니다.