이미지 로딩 중...

WebRTC 최종 프로젝트 및 최적화 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 21. · 14 Views

WebRTC 최종 프로젝트 및 최적화 완벽 가이드

WebRTC 프로젝트를 실전 배포하기 전 필수적으로 확인해야 할 통합 테스트, 성능 벤치마크, 메모리 관리, 에러 처리, 크로스 브라우저 호환성, 그리고 배포 체크리스트를 단계별로 알아봅니다. 초급 개발자도 쉽게 따라할 수 있도록 실무 중심으로 설명합니다.


목차

  1. 전체_기능_통합_테스트
  2. 성능_벤치마크
  3. 메모리_누수_방지
  4. 에러_처리_및_재연결
  5. 크로스_브라우저_테스트
  6. 실전_배포_체크리스트

1. 전체_기능_통합_테스트

시작하며

여러분이 열심히 개발한 WebRTC 화상통화 앱을 친구에게 자랑하려고 했는데, 막상 실행하니 카메라는 켜지는데 상대방 화면이 안 보이는 경험을 해본 적 있나요? 또는 개발할 때는 잘 되던 기능이 실제 사용자가 쓰려고 하니 오류가 나는 상황 말이죠.

이런 문제는 각 기능을 따로따로 테스트했지만, 모든 기능이 함께 작동할 때를 제대로 확인하지 않아서 생깁니다. 마치 레고 블록을 하나씩 만들 때는 완벽했지만, 다 조립하고 보니 어딘가 맞지 않는 것과 같아요.

바로 이럴 때 필요한 것이 통합 테스트입니다. 통합 테스트는 여러분의 앱이 실제 사용 상황에서 모든 기능이 조화롭게 작동하는지 확인하는 과정이에요.

개요

간단히 말해서, 통합 테스트는 앱의 모든 기능을 실제 사용 시나리오대로 처음부터 끝까지 테스트하는 것입니다. WebRTC 앱에서는 특히 중요한데요, 미디어 장치 접근, 시그널링, Peer 연결, 미디어 스트림 전송, 화면 공유, 채팅 등 여러 기능이 순차적으로 동작해야 하기 때문입니다.

예를 들어, 사용자가 "통화 시작" 버튼을 누르면 → 카메라/마이크 권한 요청 → 시그널링 서버 연결 → Offer/Answer 교환 → ICE 후보 수집 → 미디어 스트림 전송까지 모든 단계가 완벽하게 이어져야 해요. 기존에는 각 함수나 컴포넌트를 개별적으로 테스트하는 유닛 테스트만 했다면, 이제는 실제 사용자처럼 전체 플로우를 테스트해야 합니다.

통합 테스트의 핵심 특징은 첫째, 실제 사용자 시나리오를 재현한다는 점, 둘째, 여러 컴포넌트 간의 상호작용을 검증한다는 점, 셋째, 예상치 못한 버그를 사전에 발견할 수 있다는 점입니다. 이러한 특징들이 중요한 이유는 실제 배포 후 사용자가 겪을 문제를 미리 방지할 수 있기 때문이에요.

코드 예제

// WebRTC 통합 테스트 예제
describe('WebRTC 화상통화 통합 테스트', () => {
  test('사용자 A와 B의 전체 통화 플로우', async () => {
    // 1. 두 명의 사용자 세션 시뮬레이션
    const userA = new WebRTCClient('UserA');
    const userB = new WebRTCClient('UserB');

    // 2. 미디어 장치 접근 테스트
    await userA.getLocalMedia({ video: true, audio: true });
    await userB.getLocalMedia({ video: true, audio: true });
    expect(userA.localStream).toBeDefined();

    // 3. 시그널링 연결 테스트
    await userA.connectToSignaling('wss://test-server.com');
    await userB.connectToSignaling('wss://test-server.com');

    // 4. Offer/Answer 교환 테스트
    const offer = await userA.createOffer();
    await userB.receiveOffer(offer);
    const answer = await userB.createAnswer();
    await userA.receiveAnswer(answer);

    // 5. 연결 상태 확인
    await waitForConnection(userA.peerConnection);
    expect(userA.peerConnection.connectionState).toBe('connected');

    // 6. 원격 스트림 수신 확인
    expect(userA.remoteStream).toBeDefined();
    expect(userB.remoteStream).toBeDefined();

    // 7. 화면 공유 기능 테스트
    await userA.shareScreen();
    expect(userB.remoteStream.getVideoTracks()[0].label).toContain('screen');

    // 8. 연결 종료 테스트
    await userA.hangup();
    expect(userA.peerConnection.connectionState).toBe('closed');
  });
});

설명

이것이 하는 일: 위 코드는 두 명의 사용자가 화상통화를 시작하고 종료하는 전체 과정을 자동으로 테스트합니다. 마치 로봇이 실제 사용자처럼 앱을 사용해보는 것이죠.

첫 번째로, 테스트 환경에서 두 명의 가짜 사용자(UserA, UserB)를 만들고 각각의 WebRTC 클라이언트를 초기화합니다. 그리고 getLocalMedia를 호출해서 카메라와 마이크에 접근하는 것을 시뮬레이션해요.

이렇게 하는 이유는 실제 사용자가 통화를 시작할 때 제일 먼저 하는 작업이기 때문입니다. 두 번째로, 시그널링 서버에 연결하고 Offer/Answer를 교환하는 과정을 검증합니다.

userA가 createOffer를 호출하면 SDP(Session Description Protocol)가 생성되고, 이를 userB가 받아서 createAnswer로 응답해요. 내부적으로는 네트워크 정보, 코덱 정보, 미디어 설정 등이 교환됩니다.

세 번째 단계로, waitForConnection 함수로 실제 Peer 연결이 성공적으로 수립되는지 확인하고, 양쪽 모두 원격 스트림(상대방의 영상/음성)을 제대로 받는지 검증합니다. 추가로 화면 공유 같은 고급 기능도 테스트하여 모든 시나리오를 커버해요.

마지막으로, hangup을 호출해서 연결이 깨끗하게 종료되고 리소스가 해제되는지 확인합니다. 최종적으로 connectionState가 'closed'가 되는 것을 검증하죠.

여러분이 이 코드를 사용하면 배포 전에 전체 통화 플로우에서 발생할 수 있는 버그를 미리 발견할 수 있습니다. 실무에서는 CI/CD 파이프라인에 이런 테스트를 통합해서 코드가 변경될 때마다 자동으로 실행되게 하면, 버그가 프로덕션에 배포되는 것을 방지할 수 있어요.

또한 새로운 기능을 추가할 때 기존 기능이 망가지지 않았는지 확인하는 회귀 테스트(Regression Test)로도 활용할 수 있습니다.

실전 팁

💡 테스트 환경에서는 실제 카메라/마이크 대신 Canvas나 AudioContext로 생성한 가짜 스트림을 사용하면 CI/CD 서버에서도 테스트를 실행할 수 있어요

💡 타임아웃 값을 충분히 길게 설정하세요. WebRTC 연결은 네트워크 상황에 따라 시간이 걸릴 수 있으니 최소 10초 이상 여유를 두는 것이 좋습니다

💡 테스트 실패 시 스크린샷이나 비디오를 자동으로 캡처하도록 설정하면 디버깅이 훨씬 쉬워져요. Playwright나 Puppeteer를 사용하면 가능합니다

💡 실제 사용자 시나리오를 기반으로 테스트 케이스를 작성하세요. 예를 들어 "마이크를 음소거했다가 다시 켜기", "네트워크가 잠깐 끊겼다가 복구되기" 같은 상황들이요

💡 여러 브라우저 조합을 테스트하세요. Chrome-Chrome뿐만 아니라 Chrome-Firefox, Safari-Chrome 같은 크로스 브라우저 테스트도 중요합니다


2. 성능_벤치마크

시작하며

여러분의 화상통화 앱이 2명이 통화할 때는 부드럽게 잘 작동하는데, 10명이 동시에 접속하니 버벅거리고 CPU가 100%까지 올라가는 경험을 해본 적 있나요? 또는 갑자기 메모리가 계속 증가하더니 브라우저가 멈춰버리는 상황 말이죠.

이런 문제는 앱의 성능 한계를 미리 측정하지 않아서 발생합니다. 마치 다리를 지을 때 몇 대의 차가 지나갈 수 있는지 계산하지 않고 만드는 것과 같아요.

실제로 많은 차가 지나가면 다리가 무너질 수 있죠. 바로 이럴 때 필요한 것이 성능 벤치마크입니다.

성능 벤치마크는 여러분의 앱이 얼마나 많은 부하를 견딜 수 있는지, 어디서 병목이 생기는지를 숫자로 측정하는 과정이에요.

개요

간단히 말해서, 성능 벤치마크는 앱의 성능을 정량적으로 측정하여 한계와 개선점을 찾아내는 것입니다. WebRTC 앱에서는 여러 지표를 측정해야 하는데요, CPU 사용률, 메모리 사용량, 비디오 프레임레이트(FPS), 비트레이트, 패킷 손실률, 지연시간(latency) 등이 있습니다.

예를 들어, 4명이 화상회의를 할 때 각 참가자의 비디오 해상도를 720p로 설정하면 CPU 사용률이 60%인데, 1080p로 올리면 90%가 된다면, 이는 해상도를 조정해야 한다는 신호에요. 기존에는 "느린 것 같다", "끊기는 것 같다"처럼 주관적으로 판단했다면, 이제는 "평균 FPS 24", "패킷 손실률 3%" 같은 구체적인 숫자로 성능을 평가할 수 있습니다.

성능 벤치마크의 핵심 특징은 첫째, 객관적인 지표로 성능을 측정한다는 점, 둘째, 시간에 따른 성능 변화를 추적할 수 있다는 점, 셋째, 성능 목표(SLA)를 설정하고 달성 여부를 확인할 수 있다는 점입니다. 이러한 특징들이 중요한 이유는 사용자 경험을 예측하고 개선할 수 있는 근거가 되기 때문이에요.

코드 예제

// WebRTC 성능 벤치마크 측정 예제
class WebRTCPerformanceMonitor {
  constructor(peerConnection) {
    this.pc = peerConnection;
    this.metrics = [];
    this.startTime = Date.now();
  }

  // 실시간 성능 지표 수집
  async collectMetrics() {
    const stats = await this.pc.getStats();
    const metrics = {
      timestamp: Date.now() - this.startTime,
      cpu: await this.getCPUUsage(),
      memory: performance.memory?.usedJSHeapSize / 1024 / 1024, // MB
      video: {},
      audio: {}
    };

    stats.forEach(report => {
      // 인바운드 비디오 통계
      if (report.type === 'inbound-rtp' && report.kind === 'video') {
        metrics.video.fps = report.framesPerSecond || 0;
        metrics.video.bitrate = report.bytesReceived * 8 / 1000; // kbps
        metrics.video.packetsLost = report.packetsLost || 0;
        metrics.video.jitter = report.jitter || 0;
      }

      // 인바운드 오디오 통계
      if (report.type === 'inbound-rtp' && report.kind === 'audio') {
        metrics.audio.bitrate = report.bytesReceived * 8 / 1000;
        metrics.audio.packetsLost = report.packetsLost || 0;
      }

      // ICE 후보 통계
      if (report.type === 'candidate-pair' && report.state === 'succeeded') {
        metrics.rtt = report.currentRoundTripTime * 1000; // ms
      }
    });

    this.metrics.push(metrics);
    return metrics;
  }

  // 성능 리포트 생성
  generateReport() {
    const avgFPS = this.average(this.metrics.map(m => m.video.fps));
    const avgMemory = this.average(this.metrics.map(m => m.memory));
    const maxMemory = Math.max(...this.metrics.map(m => m.memory));
    const avgRTT = this.average(this.metrics.map(m => m.rtt));

    return {
      averageFPS: avgFPS.toFixed(2),
      averageMemoryMB: avgMemory.toFixed(2),
      maxMemoryMB: maxMemory.toFixed(2),
      averageLatencyMs: avgRTT.toFixed(2),
      totalPacketsLost: this.sum(this.metrics.map(m => m.video.packetsLost))
    };
  }

  average(arr) {
    return arr.reduce((a, b) => a + b, 0) / arr.length;
  }

  sum(arr) {
    return arr.reduce((a, b) => a + b, 0);
  }

  async getCPUUsage() {
    // Chrome DevTools Protocol 사용
    if ('performance' in window && 'measureUserAgentSpecificMemory' in performance) {
      return navigator.hardwareConcurrency || 4;
    }
    return null;
  }
}

// 사용 예시
const monitor = new WebRTCPerformanceMonitor(peerConnection);
setInterval(async () => {
  const metrics = await monitor.collectMetrics();
  console.log('Current metrics:', metrics);
}, 1000);

// 10초 후 리포트 생성
setTimeout(() => {
  const report = monitor.generateReport();
  console.log('Performance Report:', report);

  // 성능 목표와 비교
  if (report.averageFPS < 24) {
    console.warn('⚠️ FPS가 목표(24fps)보다 낮습니다!');
  }
  if (report.maxMemoryMB > 500) {
    console.warn('⚠️ 메모리 사용량이 500MB를 초과했습니다!');
  }
}, 10000);

설명

이것이 하는 일: 위 코드는 WebRTC 연결의 성능 지표를 실시간으로 수집하고 분석하여 리포트를 생성합니다. 마치 자동차의 계기판처럼 현재 상태를 보여주는 것이죠.

첫 번째로, WebRTCPerformanceMonitor 클래스를 만들어서 PeerConnection 객체를 받아 초기화합니다. collectMetrics 메서드는 getStats() API를 호출하여 WebRTC의 내부 통계를 가져와요.

이 API는 브라우저가 자동으로 수집하는 수백 개의 통계 정보를 제공하는데, 우리는 그중 중요한 것만 필터링합니다. 왜 이렇게 하냐면 모든 정보를 다 저장하면 메모리 낭비이고, 실제로 의미 있는 지표만 추출하는 것이 효율적이기 때문이에요.

두 번째로, stats.forEach로 각 통계 리포트를 순회하면서 비디오와 오디오 관련 지표를 추출합니다. 'inbound-rtp' 타입은 수신 중인 스트림의 정보를 담고 있어요.

framesPerSecond는 초당 프레임 수(FPS), bytesReceived는 받은 데이터양, packetsLost는 손실된 패킷 수, jitter는 패킷 도착 시간의 변동성을 나타냅니다. 내부적으로 브라우저는 이런 값들을 1초마다 업데이트하며, 네트워크 상태에 따라 실시간으로 변하죠.

세 번째 단계로, 수집한 메트릭을 배열에 저장하고, generateReport에서 평균값과 최댓값을 계산합니다. 예를 들어 10초 동안 FPS가 [30, 28, 25, 22, 24, 26, 27, 25, 23, 24]라면 평균은 25.4가 되는 거죠.

마지막으로 성능 목표와 비교하여 경고를 출력합니다. 여러분이 이 코드를 사용하면 앱의 성능 문제를 조기에 발견하고 대응할 수 있습니다.

실무에서는 이런 메트릭을 서버로 전송해서 Grafana 같은 대시보드에 시각화하고, 알림을 설정해서 성능이 특정 임계값을 넘으면 개발팀에 자동으로 통보되게 할 수 있어요. 또한 A/B 테스트를 할 때 두 버전의 성능을 객관적으로 비교할 수 있고, 최적화 작업 전후의 효과를 정량적으로 측정할 수 있습니다.

예를 들어 비디오 코덱을 VP8에서 VP9로 바꿨을 때 비트레이트가 20% 감소했다는 것을 증명할 수 있죠.

실전 팁

💡 성능 측정은 실제 사용 환경과 유사하게 하세요. 개발자의 고성능 맥북이 아니라 일반 사용자가 쓰는 보급형 노트북에서 테스트해야 합니다

💡 장시간 테스트를 통해 메모리 누수를 찾으세요. 5분 동안은 괜찮다가 1시간 후에 문제가 생기는 경우가 많아요

💡 네트워크 조건을 시뮬레이션하세요. Chrome DevTools의 Network throttling으로 3G, 4G 속도를 재현할 수 있습니다

💡 동시 접속자 수를 늘려가며 테스트하세요. 1명, 2명, 4명, 8명... 어느 시점부터 성능이 급격히 떨어지는지 파악하는 것이 중요합니다

💡 WebRTC Internals(chrome://webrtc-internals)를 활용하면 브라우저 자체 도구로 더 자세한 통계를 볼 수 있어요


3. 메모리_누수_방지

시작하며

여러분의 화상통화 앱을 처음 시작했을 때는 메모리가 100MB였는데, 1시간 동안 사용하니 1GB까지 증가하고 결국 브라우저가 느려지거나 멈춰버리는 경험을 해본 적 있나요? 또는 통화를 종료했는데도 메모리가 해제되지 않고 계속 쌓이는 상황 말이죠.

이런 문제는 메모리 누수(Memory Leak)라고 불리며, 더 이상 사용하지 않는 객체나 리소스가 메모리에서 해제되지 않아서 발생합니다. 마치 수도꼭지를 잠그지 않아서 물이 계속 새는 것과 같아요.

처음에는 조금씩 새지만 시간이 지나면 큰 문제가 됩니다. 바로 이럴 때 필요한 것이 메모리 누수 방지 전략입니다.

WebRTC는 미디어 스트림, PeerConnection, 이벤트 리스너 등 많은 리소스를 사용하기 때문에 제대로 정리하지 않으면 메모리가 계속 쌓이게 되죠.

개요

간단히 말해서, 메모리 누수 방지는 사용이 끝난 리소스를 확실하게 해제하여 메모리가 계속 증가하는 것을 막는 것입니다. WebRTC에서 메모리 누수가 자주 발생하는 곳은 미디어 스트림을 정지하지 않은 경우, PeerConnection을 close하지 않은 경우, 이벤트 리스너를 제거하지 않은 경우, setInterval/setTimeout을 clear하지 않은 경우 등이 있습니다.

예를 들어, 사용자가 통화를 종료했는데 개발자가 stream.getTracks().forEach(track => track.stop())을 호출하지 않으면 카메라와 마이크가 계속 켜져 있고 메모리도 차지하게 돼요. 기존에는 "사용자가 페이지를 새로고침하면 되겠지"라고 생각했다면, 이제는 Single Page Application(SPA)이 대세라서 페이지 전환 없이 오래 사용하기 때문에 메모리 관리가 필수입니다.

메모리 누수 방지의 핵심 특징은 첫째, 리소스의 생명주기를 명확히 관리한다는 점, 둘째, cleanup 함수를 항상 작성한다는 점, 셋째, 메모리 프로파일링 도구로 검증한다는 점입니다. 이러한 특징들이 중요한 이유는 장시간 사용하는 앱의 안정성을 보장하기 때문이에요.

코드 예제

// 메모리 누수 방지를 위한 WebRTC 리소스 관리
class WebRTCConnection {
  constructor() {
    this.peerConnection = null;
    this.localStream = null;
    this.remoteStream = null;
    this.eventListeners = new Map();
    this.intervals = new Set();
  }

  // 연결 초기화
  async initialize() {
    // 미디어 스트림 획득
    this.localStream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true
    });

    // PeerConnection 생성
    this.peerConnection = new RTCPeerConnection({
      iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
    });

    // 이벤트 리스너 등록 (나중에 제거할 수 있도록 저장)
    const onIceCandidate = (event) => {
      if (event.candidate) {
        console.log('New ICE candidate:', event.candidate);
      }
    };
    this.peerConnection.addEventListener('icecandidate', onIceCandidate);
    this.eventListeners.set('icecandidate', onIceCandidate);

    const onTrack = (event) => {
      this.remoteStream = event.streams[0];
    };
    this.peerConnection.addEventListener('track', onTrack);
    this.eventListeners.set('track', onTrack);

    // 로컬 스트림 추가
    this.localStream.getTracks().forEach(track => {
      this.peerConnection.addTrack(track, this.localStream);
    });

    // 주기적 상태 체크 (나중에 정리할 수 있도록 저장)
    const statusInterval = setInterval(() => {
      console.log('Connection state:', this.peerConnection.connectionState);
    }, 5000);
    this.intervals.add(statusInterval);
  }

  // 🔥 핵심: 모든 리소스를 확실하게 정리하는 cleanup 함수
  cleanup() {
    console.log('🧹 Cleaning up WebRTC resources...');

    // 1. 미디어 스트림의 모든 트랙 정지
    if (this.localStream) {
      this.localStream.getTracks().forEach(track => {
        track.stop(); // 카메라/마이크 완전히 끄기
        console.log(`Stopped track: ${track.kind}`);
      });
      this.localStream = null;
    }

    if (this.remoteStream) {
      this.remoteStream.getTracks().forEach(track => track.stop());
      this.remoteStream = null;
    }

    // 2. 이벤트 리스너 제거
    if (this.peerConnection) {
      this.eventListeners.forEach((listener, eventName) => {
        this.peerConnection.removeEventListener(eventName, listener);
        console.log(`Removed listener: ${eventName}`);
      });
      this.eventListeners.clear();

      // 3. PeerConnection 닫기
      this.peerConnection.close();
      this.peerConnection = null;
    }

    // 4. 타이머 정리
    this.intervals.forEach(interval => clearInterval(interval));
    this.intervals.clear();

    console.log('✅ Cleanup completed!');
  }

  // 컴포넌트 언마운트 시 자동 정리 (React 예시)
  destroy() {
    this.cleanup();
  }
}

// 사용 예시
const connection = new WebRTCConnection();
await connection.initialize();

// 통화 종료 시 또는 컴포넌트 언마운트 시
connection.cleanup(); // 이 한 줄로 모든 리소스 해제!

// React에서 사용하는 경우
useEffect(() => {
  const conn = new WebRTCConnection();
  conn.initialize();

  return () => {
    conn.cleanup(); // 컴포넌트 언마운트 시 자동 정리
  };
}, []);

설명

이것이 하는 일: 위 코드는 WebRTC에서 사용하는 모든 리소스를 추적하고 필요할 때 한 번에 정리할 수 있는 구조를 제공합니다. 마치 방을 나갈 때 불 끄고, 창문 닫고, 문 잠그는 체크리스트를 따르는 것처럼요.

첫 번째로, WebRTCConnection 클래스에서 사용할 모든 리소스를 멤버 변수로 관리합니다. eventListeners는 Map으로, intervals는 Set으로 저장하는데, 이렇게 하는 이유는 나중에 정리할 때 어떤 리스너를 제거해야 하고 어떤 타이머를 정리해야 하는지 명확히 알 수 있기 때문이에요.

만약 이렇게 하지 않고 그냥 addEventListener만 하면, 나중에 어떤 리스너를 붙였는지 기억하기 어렵고 제거할 수도 없죠. 두 번째로, cleanup 함수에서 순서대로 리소스를 정리합니다.

제일 먼저 localStream.getTracks().forEach(track => track.stop())을 호출하면 실제 하드웨어 장치(카메라/마이크)가 꺼지고 브라우저에서 "카메라 사용 중" 표시도 사라져요. 내부적으로 브라우저는 미디어 장치와의 연결을 끊고, 관련 버퍼와 인코더를 해제합니다.

그 다음 removeEventListener로 이벤트 리스너를 제거하여 메모리 참조를 끊습니다. 세 번째 단계로, peerConnection.close()를 호출하면 모든 네트워크 연결이 종료되고, ICE 에이전트, DTLS 연결, SRTP 세션 등이 정리됩니다.

마지막으로 clearInterval로 타이머를 정리하여 백그라운드에서 계속 실행되는 코드가 없도록 해요. 여러분이 이 코드를 사용하면 통화를 여러 번 시작하고 종료해도 메모리가 계속 증가하지 않습니다.

실무에서는 React의 useEffect cleanup, Vue의 onUnmounted, 또는 Angular의 ngOnDestroy 같은 생명주기 훅에서 이 cleanup 함수를 호출하면 돼요. 또한 Chrome DevTools의 Memory 탭에서 Heap Snapshot을 찍어서 cleanup 전후의 메모리 사용량을 비교하면 제대로 정리되고 있는지 확인할 수 있습니다.

예를 들어 통화 시작 시 100MB, 종료 후 105MB라면 거의 모든 리소스가 해제된 것이지만, 200MB까지 올라간다면 어딘가 누수가 있다는 신호죠.

실전 팁

💡 Chrome DevTools의 Performance Monitor를 사용하면 실시간으로 메모리 사용량을 모니터링할 수 있어요. 메모리 그래프가 계속 우상향하면 누수가 있다는 신호입니다

💡 WeakMap과 WeakSet을 활용하면 자동으로 가비지 컬렉션되는 참조를 만들 수 있어요. 하지만 명시적인 cleanup이 더 안전합니다

💡 React를 사용한다면 useRef로 PeerConnection을 저장하고 cleanup에서 .current를 정리하세요. 일반 변수로 저장하면 재렌더링 시 새로운 객체가 생성되어 누수가 발생할 수 있어요

💡 정기적으로 메모리 프로파일링을 하세요. 개발 중에는 괜찮다가 프로덕션에서 누수가 발견되면 대응이 어려워집니다

💡 globalThis나 window 객체에 직접 저장하는 것은 피하세요. 전역 객체에 저장된 참조는 가비지 컬렉션되지 않아 메모리 누수의 주범이 됩니다


4. 에러_처리_및_재연결

시작하며

여러분이 중요한 화상회의 중에 갑자기 네트워크가 끊겼다가 다시 연결됐는데, 통화가 완전히 종료되어 다시 처음부터 시작해야 하는 경험을 해본 적 있나요? 또는 상대방의 카메라에 문제가 생겼는데 아무런 안내 없이 검은 화면만 보이는 상황 말이죠.

이런 문제는 에러가 발생했을 때 적절하게 처리하지 않고, 자동으로 복구하는 메커니즘이 없어서 생깁니다. 마치 자동차가 작은 고장이 생겼을 때 바로 멈춰버리는 것과 같아요.

좋은 차는 경고등을 켜고 비상 모드로라도 계속 달릴 수 있죠. 바로 이럴 때 필요한 것이 견고한 에러 처리와 자동 재연결 시스템입니다.

WebRTC는 네트워크 불안정, 미디어 장치 오류, 시그널링 실패 등 다양한 에러 상황이 발생할 수 있기 때문에 이에 대비하는 것이 필수에요.

개요

간단히 말해서, 에러 처리는 문제가 발생했을 때 적절하게 대응하고 사용자에게 알려주는 것이고, 재연결은 일시적인 문제를 자동으로 복구하여 서비스 중단을 최소화하는 것입니다. WebRTC에서 처리해야 할 주요 에러는 네트워크 연결 실패(ICE connection failed), 미디어 장치 접근 거부(Permission denied), 시그널링 서버 연결 끊김(WebSocket closed), 코덱 불일치(SDP negotiation failed) 등이 있습니다.

예를 들어, 사용자가 지하철을 타고 이동하면서 Wi-Fi와 LTE를 오가면 ICE 연결이 끊겼다가 다시 연결되는데, 이때 자동으로 재협상(renegotiation)하지 않으면 통화가 끊어지게 돼요. 기존에는 에러가 발생하면 그냥 "연결 실패" 메시지만 보여주고 끝났다면, 이제는 "네트워크가 불안정합니다.

3초 후 자동으로 재연결을 시도합니다" 같은 구체적인 안내와 함께 자동 복구를 시도합니다. 에러 처리 및 재연결의 핵심 특징은 첫째, 에러 종류에 따라 다른 대응을 한다는 점, 둘째, 재시도 로직에 지수 백오프(exponential backoff)를 적용한다는 점, 셋째, 사용자에게 현재 상태를 명확히 전달한다는 점입니다.

이러한 특징들이 중요한 이유는 일시적인 문제로 인한 서비스 중단을 최소화하고 사용자 경험을 향상시키기 때문이에요.

코드 예제

// WebRTC 에러 처리 및 자동 재연결 시스템
class ResilientWebRTCConnection {
  constructor(signalingUrl, options = {}) {
    this.signalingUrl = signalingUrl;
    this.maxRetries = options.maxRetries || 5;
    this.retryDelay = options.retryDelay || 1000; // 1초
    this.retryCount = 0;
    this.peerConnection = null;
    this.signalingConnection = null;
    this.isReconnecting = false;
  }

  // 연결 시작
  async connect() {
    try {
      await this.setupSignaling();
      await this.setupPeerConnection();
      this.retryCount = 0; // 성공 시 재시도 카운트 초기화
      this.notifyUser('연결되었습니다', 'success');
    } catch (error) {
      this.handleConnectionError(error);
    }
  }

  // 시그널링 서버 연결
  setupSignaling() {
    return new Promise((resolve, reject) => {
      this.signalingConnection = new WebSocket(this.signalingUrl);

      this.signalingConnection.onopen = () => {
        console.log('✅ Signaling connected');
        resolve();
      };

      this.signalingConnection.onerror = (error) => {
        console.error('❌ Signaling error:', error);
        reject(new Error('Signaling connection failed'));
      };

      // 연결 끊김 감지 및 재연결
      this.signalingConnection.onclose = () => {
        console.warn('⚠️ Signaling disconnected');
        if (!this.isReconnecting) {
          this.reconnect('signaling');
        }
      };
    });
  }

  // PeerConnection 설정
  async setupPeerConnection() {
    this.peerConnection = new RTCPeerConnection({
      iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
    });

    // ICE 연결 상태 모니터링
    this.peerConnection.oniceconnectionstatechange = () => {
      const state = this.peerConnection.iceConnectionState;
      console.log('ICE Connection State:', state);

      switch (state) {
        case 'disconnected':
          this.notifyUser('연결이 불안정합니다', 'warning');
          // 잠시 기다렸다가 재연결 시도
          setTimeout(() => {
            if (this.peerConnection.iceConnectionState === 'disconnected') {
              this.reconnect('ice');
            }
          }, 3000);
          break;

        case 'failed':
          this.notifyUser('연결에 실패했습니다', 'error');
          this.reconnect('ice');
          break;

        case 'connected':
          this.notifyUser('연결이 복구되었습니다', 'success');
          this.retryCount = 0;
          break;
      }
    };

    // 연결 상태 모니터링 (최신 브라우저)
    this.peerConnection.onconnectionstatechange = () => {
      const state = this.peerConnection.connectionState;
      console.log('Connection State:', state);

      if (state === 'failed') {
        this.reconnect('peer');
      }
    };
  }

  // 자동 재연결 로직 (지수 백오프 적용)
  async reconnect(reason) {
    if (this.isReconnecting) return;
    if (this.retryCount >= this.maxRetries) {
      this.notifyUser('재연결 시도 횟수를 초과했습니다', 'error');
      return;
    }

    this.isReconnecting = true;
    this.retryCount++;

    // 지수 백오프: 1초, 2초, 4초, 8초, 16초...
    const delay = this.retryDelay * Math.pow(2, this.retryCount - 1);

    this.notifyUser(
      `재연결 중... (${this.retryCount}/${this.maxRetries})`,
      'info'
    );

    console.log(`🔄 Reconnecting due to ${reason} in ${delay}ms...`);

    await new Promise(resolve => setTimeout(resolve, delay));

    try {
      // 기존 연결 정리
      if (this.peerConnection) {
        this.peerConnection.close();
      }
      if (this.signalingConnection) {
        this.signalingConnection.close();
      }

      // 재연결 시도
      await this.connect();
      this.isReconnecting = false;
    } catch (error) {
      this.isReconnecting = false;
      console.error('Reconnection failed:', error);
      // 재귀적으로 다시 시도
      this.reconnect(reason);
    }
  }

  // 에러 처리
  handleConnectionError(error) {
    console.error('Connection error:', error);

    // 에러 종류에 따라 다른 처리
    if (error.name === 'NotAllowedError') {
      this.notifyUser('카메라/마이크 권한이 필요합니다', 'error');
      // 재연결 시도하지 않음 (사용자 액션 필요)
    } else if (error.name === 'NotFoundError') {
      this.notifyUser('카메라/마이크를 찾을 수 없습니다', 'error');
    } else {
      // 네트워크 에러 등은 재연결 시도
      this.reconnect('error');
    }
  }

  // 사용자에게 알림 (실제로는 UI 컴포넌트와 연결)
  notifyUser(message, type) {
    console.log(`[${type.toUpperCase()}] ${message}`);
    // 실제 구현: toast 알림, 상태 표시 등
  }
}

// 사용 예시
const connection = new ResilientWebRTCConnection('wss://signaling.example.com', {
  maxRetries: 5,
  retryDelay: 1000
});

await connection.connect();

설명

이것이 하는 일: 위 코드는 WebRTC 연결에서 발생할 수 있는 다양한 에러를 감지하고, 자동으로 복구를 시도하며, 사용자에게 상황을 알려주는 종합 시스템입니다. 마치 비행기의 자동 조종 장치가 난기류를 만나면 자동으로 고도를 조정하는 것과 같아요.

첫 번째로, setupSignaling과 setupPeerConnection에서 각각의 연결에 이벤트 리스너를 등록합니다. WebSocket의 onclose 이벤트는 시그널링 서버와의 연결이 끊겼을 때 발생하고, ICE의 oniceconnectionstatechange는 Peer 간 연결 상태가 변할 때 호출돼요.

이렇게 여러 레이어에서 상태를 모니터링하는 이유는 문제가 어디서 발생했는지 정확히 파악하기 위함입니다. 예를 들어 시그널링은 연결됐지만 ICE가 실패했다면 방화벽이나 NAT 문제일 가능성이 높죠.

두 번째로, reconnect 함수에서 지수 백오프를 구현했습니다. Math.pow(2, retryCount - 1)을 사용하면 재시도 간격이 1초, 2초, 4초, 8초...

식으로 늘어나요. 이렇게 하는 이유는 서버에 과부하를 주지 않으면서도 일시적인 문제는 빠르게 복구하기 위함입니다.

내부적으로 setTimeout으로 지연을 주고, 기존 연결을 close()로 정리한 뒤 새로 연결을 시도합니다. 세 번째 단계로, handleConnectionError에서 에러 종류를 구분하여 처리합니다.

NotAllowedError는 사용자가 권한을 거부한 것이므로 자동 재연결해봤자 소용없어요. 반면 네트워크 에러는 일시적일 수 있으니 재연결을 시도합니다.

마지막으로 notifyUser로 사용자에게 현재 상황을 알려줘서 "뭔가 문제가 있는데 뭔지 모르겠다"는 불안감을 줄입니다. 여러분이 이 코드를 사용하면 네트워크가 불안정한 환경에서도 통화가 자동으로 복구되어 사용자 경험이 크게 향상됩니다.

실무에서는 재연결 시도 횟수와 이유를 서버에 로깅하여 어떤 종류의 에러가 자주 발생하는지 분석할 수 있어요. 예를 들어 특정 ISP 사용자들이 ICE failed 에러를 자주 겪는다면 TURN 서버 추가를 고려해야 하고, 시그널링 disconnected가 많다면 서버 인프라를 점검해야 하죠.

또한 Sentry나 DataDog 같은 모니터링 도구와 연동하면 실시간으로 에러율을 추적하고 알림을 받을 수 있습니다.

실전 팁

💡 재연결 시도 중에는 사용자 인터페이스를 비활성화하거나 로딩 표시를 해서 중복 액션을 방지하세요

💡 ICE의 'disconnected' 상태는 일시적일 수 있으니 바로 재연결하지 말고 3-5초 정도 기다려보세요. 종종 자동으로 'connected'로 돌아갑니다

💡 재연결이 계속 실패하면 사용자에게 "네트워크 설정 확인" 또는 "페이지 새로고침" 같은 구체적인 해결 방법을 안내하세요

💡 에러 메시지에 에러 코드나 ID를 포함하면 사용자가 고객센터에 문의할 때 문제 파악이 쉬워집니다

💡 개발 환경에서는 Chrome DevTools의 Network 탭에서 "Offline"을 선택하여 연결 끊김을 시뮬레이션하고 재연결 로직을 테스트하세요


5. 크로스_브라우저_테스트

시작하며

여러분의 WebRTC 앱이 Chrome에서는 완벽하게 작동하는데, Safari에서는 화면이 안 보이고 Firefox에서는 음성이 끊기는 경험을 해본 적 있나요? 또는 최신 버전 브라우저에서는 잘 되는데, 1년 전 버전에서는 에러가 나는 상황 말이죠.

이런 문제는 브라우저마다 WebRTC API 구현 방식이 조금씩 다르고, 지원하는 기능과 코덱도 다르기 때문에 발생합니다. 마치 같은 한국어를 쓰지만 지역마다 사투리가 다른 것과 비슷해요.

"콜라"를 어떤 곳에서는 "사이다"라고 부르듯이, 브라우저마다 같은 기능을 다르게 구현할 수 있죠. 바로 이럴 때 필요한 것이 크로스 브라우저 테스트입니다.

주요 브라우저와 버전에서 앱이 정상 작동하는지 확인하고, 브라우저별 차이를 처리하는 polyfill이나 adapter를 적용해야 해요.

개요

간단히 말해서, 크로스 브라우저 테스트는 여러 브라우저와 버전에서 앱이 동일하게 작동하는지 확인하고, 차이점을 처리하는 것입니다. WebRTC에서 브라우저별 차이가 나는 부분은 getUserMedia의 제약 조건(constraints) 문법, VP8/VP9/H.264 코덱 지원 여부, Unified Plan vs Plan B SDP 형식, 권한 요청 UI, getDisplayMedia(화면 공유) 지원 등이 있습니다.

예를 들어, Safari는 오랫동안 VP9 코덱을 지원하지 않았고, 구형 Chrome은 Plan B를 사용했지만 최신 버전은 Unified Plan만 지원해요. 기존에는 "Chrome에서만 사용하세요"라고 안내했다면, 이제는 모든 주요 브라우저에서 작동하도록 만들어야 사용자 기반을 넓힐 수 있습니다.

크로스 브라우저 테스트의 핵심 특징은 첫째, 브라우저별 기능 지원 여부를 확인한다는 점, 둘째, adapter.js 같은 라이브러리로 API 차이를 보완한다는 점, 셋째, 실제 디바이스와 브라우저에서 직접 테스트한다는 점입니다. 이러한 특징들이 중요한 이유는 다양한 환경의 사용자들에게 일관된 경험을 제공할 수 있기 때문이에요.

코드 예제

// 크로스 브라우저 호환성을 위한 WebRTC 설정
import adapter from 'webrtc-adapter'; // 브라우저 차이 자동 보정

class CrossBrowserWebRTC {
  constructor() {
    this.browserInfo = this.detectBrowser();
    this.supportedFeatures = this.checkFeatureSupport();
  }

  // 브라우저 감지
  detectBrowser() {
    console.log('Browser:', adapter.browserDetails.browser);
    console.log('Version:', adapter.browserDetails.version);
    return adapter.browserDetails;
  }

  // 기능 지원 여부 확인
  checkFeatureSupport() {
    const features = {
      getUserMedia: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
      RTCPeerConnection: !!window.RTCPeerConnection,
      getDisplayMedia: !!(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia),
      insertableStreams: !!RTCRtpSender.prototype.createEncodedStreams,
      VP9: this.isCodecSupported('VP9'),
      H264: this.isCodecSupported('H264')
    };

    console.log('Supported features:', features);
    return features;
  }

  // 코덱 지원 확인
  async isCodecSupported(codecName) {
    if (!window.RTCRtpSender || !RTCRtpSender.getCapabilities) {
      return false;
    }

    const capabilities = RTCRtpSender.getCapabilities('video');
    return capabilities.codecs.some(codec =>
      codec.mimeType.includes(codecName)
    );
  }

  // 브라우저별 최적 설정 가져오기
  getOptimalConstraints() {
    const { browser, version } = this.browserInfo;

    // 기본 설정
    let constraints = {
      video: {
        width: { ideal: 1280 },
        height: { ideal: 720 },
        frameRate: { ideal: 30 }
      },
      audio: {
        echoCancellation: true,
        noiseSuppression: true,
        autoGainControl: true
      }
    };

    // Safari 특별 처리
    if (browser === 'safari') {
      // Safari는 ideal 대신 exact를 선호
      constraints.video = {
        width: 1280,
        height: 720,
        frameRate: 30
      };

      // 구형 Safari는 일부 오디오 제약 미지원
      if (version < 14) {
        delete constraints.audio.noiseSuppression;
      }
    }

    // Firefox 특별 처리
    if (browser === 'firefox') {
      // Firefox는 frameRate를 max로 지정
      constraints.video.frameRate = { max: 30 };
    }

    return constraints;
  }

  // 브라우저별 PeerConnection 설정
  createPeerConnection() {
    const config = {
      iceServers: [
        { urls: 'stun:stun.l.google.com:19302' }
      ],
      iceCandidatePoolSize: 10
    };

    // Unified Plan 강제 (최신 표준)
    if (this.browserInfo.browser === 'chrome' && this.browserInfo.version < 72) {
      // 구형 Chrome은 Plan B를 기본으로 사용
      config.sdpSemantics = 'unified-plan';
    }

    const pc = new RTCPeerConnection(config);

    // 브라우저별 이벤트 처리 차이
    this.setupBrowserSpecificHandlers(pc);

    return pc;
  }

  // 브라우저별 이벤트 핸들러
  setupBrowserSpecificHandlers(pc) {
    // Safari는 ontrack이 다르게 동작할 수 있음
    if (this.browserInfo.browser === 'safari') {
      pc.ontrack = (event) => {
        // Safari는 streams 배열이 비어있을 수 있음
        console.log('Track received:', event.track);
        if (event.streams && event.streams[0]) {
          console.log('Stream:', event.streams[0]);
        }
      };
    }
  }

  // 화면 공유 (브라우저별 처리)
  async shareScreen() {
    // 기능 지원 확인
    if (!this.supportedFeatures.getDisplayMedia) {
      throw new Error('화면 공유가 지원되지 않는 브라우저입니다');
    }

    try {
      // 최신 표준 방식
      const stream = await navigator.mediaDevices.getDisplayMedia({
        video: {
          cursor: 'always' // Safari는 미지원
        },
        audio: this.browserInfo.browser !== 'safari' // Safari는 화면 공유 시 오디오 미지원
      });

      return stream;
    } catch (error) {
      console.error('Screen share failed:', error);
      throw error;
    }
  }

  // 브라우저별 경고 표시
  showBrowserWarnings() {
    const { browser, version } = this.browserInfo;

    if (browser === 'safari' && !this.supportedFeatures.VP9) {
      console.warn('⚠️ Safari는 VP9 코덱을 지원하지 않습니다. H.264를 사용합니다.');
    }

    if (browser === 'firefox' && version < 60) {
      console.warn('⚠️ Firefox 60 미만 버전은 일부 기능이 제한됩니다.');
    }

    if (browser === 'chrome' && version < 74) {
      console.warn('⚠️ Chrome 74 미만 버전은 Unified Plan을 완전히 지원하지 않습니다.');
    }
  }
}

// 사용 예시
const webrtc = new CrossBrowserWebRTC();
webrtc.showBrowserWarnings();

const constraints = webrtc.getOptimalConstraints();
const stream = await navigator.mediaDevices.getUserMedia(constraints);

const pc = webrtc.createPeerConnection();

설명

이것이 하는 일: 위 코드는 다양한 브라우저에서 WebRTC가 일관되게 작동하도록 브라우저를 감지하고, 각 브라우저에 맞는 설정을 적용합니다. 마치 통역사가 여러 언어를 하나로 번역해주는 것과 같아요.

첫 번째로, webrtc-adapter 라이브러리를 import하면 자동으로 브라우저 간 API 차이를 보정해줍니다. 예를 들어 구형 브라우저에서는 navigator.getUserMedia였는데 최신 표준은 navigator.mediaDevices.getUserMedia인데, adapter가 자동으로 변환해줘요.

detectBrowser에서 adapter.browserDetails로 현재 브라우저 정보를 가져오고, checkFeatureSupport에서 각 기능이 지원되는지 확인합니다. 이렇게 하는 이유는 지원되지 않는 기능을 사용하려다가 에러가 나는 것을 방지하기 위함이에요.

두 번째로, getOptimalConstraints에서 브라우저별로 다른 설정을 반환합니다. Safari는 { ideal: 1280 } 같은 문법을 제대로 처리하지 못할 때가 있어서 그냥 1280으로 지정하고, Firefox는 frameRate를 { max: 30 }으로 표현하는 것을 선호해요.

내부적으로 각 브라우저는 이런 제약 조건을 파싱해서 최적의 미디어 설정을 찾는데, 문법이 조금씩 달라서 명시적으로 처리해줘야 합니다. 세 번째 단계로, createPeerConnection에서 Unified Plan을 강제로 설정합니다.

구형 Chrome은 Plan B라는 오래된 SDP 형식을 사용했는데, 이는 최신 브라우저와 호환되지 않아요. sdpSemantics를 'unified-plan'으로 설정하면 모든 브라우저가 같은 형식으로 통신할 수 있죠.

마지막으로 shareScreen에서 Safari는 화면 공유 시 오디오를 지원하지 않으므로 조건부로 audio 옵션을 제거합니다. 여러분이 이 코드를 사용하면 Chrome, Firefox, Safari, Edge 등 주요 브라우저에서 모두 일관된 동작을 보장할 수 있습니다.

실무에서는 BrowserStack이나 Sauce Labs 같은 클라우드 테스팅 서비스를 사용하면 수백 가지 브라우저/OS 조합에서 자동으로 테스트할 수 있어요. 또한 Can I Use(caniuse.com)에서 특정 WebRTC 기능의 브라우저 지원 현황을 확인하고, 사용자 통계를 분석하여 어떤 브라우저를 우선 지원할지 결정할 수 있습니다.

예를 들어 사용자의 90%가 Chrome을 쓴다면 Chrome 최적화에 집중하되, 나머지 10%를 위한 기본 지원도 제공하는 식이죠.

실전 팁

💡 webrtc-adapter는 프로젝트 최상단에서 import만 해도 자동으로 전역 API를 보정해주므로, 별도 설정 없이 바로 사용할 수 있어요

💡 BrowserStack이나 LambdaTest 같은 서비스를 CI/CD에 통합하면 코드 커밋 시 자동으로 여러 브라우저에서 테스트됩니다

💡 사용자 에이전트 문자열만으로 브라우저를 판단하지 말고, 기능 감지(feature detection)를 우선하세요. 브라우저는 속일 수 있지만 기능은 속일 수 없어요

💡 Safari는 iOS와 macOS에서 동작이 다를 수 있으니 모바일 Safari도 별도로 테스트하세요

💡 최신 브라우저만 지원하고 싶다면 "Chrome 90+, Firefox 88+, Safari 14+" 같은 최소 버전 요구사항을 명시하고, 구형 브라우저 사용자에게는 업그레이드 안내를 보여주세요


6. 실전_배포_체크리스트

시작하며

여러분이 몇 달 동안 열심히 개발한 WebRTC 앱을 드디어 배포하려는데, 뭔가 빠뜨린 게 없을까 불안한 경험을 해본 적 있나요? 또는 배포 후에 "아, 이것도 확인했어야 했는데"라고 후회하는 상황 말이죠.

이런 문제는 체계적인 배포 체크리스트 없이 그때그때 생각나는 것만 확인해서 발생합니다. 마치 여행을 떠나기 전에 챙겨야 할 것들을 메모하지 않고 가다가 여권을 깜빡하는 것과 같아요.

중요한 것일수록 놓치기 쉽죠. 바로 이럴 때 필요한 것이 실전 배포 체크리스트입니다.

개발, 테스트, 보안, 성능, 모니터링, 문서화 등 모든 영역에서 확인해야 할 항목을 정리해두면 안전하고 성공적인 배포를 할 수 있어요.

개요

간단히 말해서, 배포 체크리스트는 프로덕션 환경에 앱을 출시하기 전에 확인해야 할 모든 항목을 정리한 것입니다. WebRTC 앱 배포 시 확인해야 할 주요 영역은 코드 품질(린팅, 타입 체크), 보안(HTTPS, CORS, 권한), 성능(번들 크기, 최적화), 인프라(STUN/TURN 서버, 시그널링 서버), 모니터링(에러 추적, 분석), 사용자 경험(로딩 상태, 에러 메시지), 문서화(API 문서, 사용자 가이드) 등이 있습니다.

예를 들어, HTTPS가 아닌 HTTP에서는 getUserMedia가 작동하지 않기 때문에 SSL 인증서 설정이 필수에요. 기존에는 "일단 배포하고 문제 생기면 고치자"는 접근이었다면, 이제는 "문제가 생기기 전에 미리 방지하자"는 사전 예방적 접근이 필요합니다.

배포 체크리스트의 핵심 특징은 첫째, 빠뜨리기 쉬운 항목을 명시적으로 확인한다는 점, 둘째, 팀원 간 배포 프로세스를 표준화한다는 점, 셋째, 배포 후 문제 발생 시 롤백 계획을 가지고 있다는 점입니다. 이러한 특징들이 중요한 이유는 사용자에게 안정적인 서비스를 제공하고, 장애 시간을 최소화할 수 있기 때문이에요.

코드 예제

// 배포 전 자동 체크 스크립트 예제
const deploymentChecker = {
  // 환경 변수 확인
  checkEnvironment() {
    const required = [
      'SIGNALING_SERVER_URL',
      'TURN_SERVER_URL',
      'TURN_USERNAME',
      'TURN_PASSWORD',
      'API_KEY'
    ];

    const missing = required.filter(key => !process.env[key]);

    if (missing.length > 0) {
      throw new Error(`❌ 필수 환경 변수 누락: ${missing.join(', ')}`);
    }

    console.log('✅ 환경 변수 확인 완료');
  },

  // HTTPS 확인
  checkHTTPS() {
    if (process.env.NODE_ENV === 'production' &&
        !process.env.SIGNALING_SERVER_URL.startsWith('wss://')) {
      throw new Error('❌ 프로덕션에서는 WSS(Secure WebSocket) 사용 필수');
    }

    console.log('✅ HTTPS/WSS 확인 완료');
  },

  // 번들 크기 확인
  async checkBundleSize() {
    const fs = require('fs').promises;
    const stats = await fs.stat('./dist/bundle.js');
    const sizeMB = stats.size / 1024 / 1024;

    console.log(`📦 번들 크기: ${sizeMB.toFixed(2)}MB`);

    if (sizeMB > 5) {
      console.warn('⚠️ 번들 크기가 5MB를 초과합니다. 코드 스플리팅 고려하세요.');
    } else {
      console.log('✅ 번들 크기 적정');
    }
  },

  // TURN 서버 연결 테스트
  async checkTURNServer() {
    const pc = new RTCPeerConnection({
      iceServers: [{
        urls: process.env.TURN_SERVER_URL,
        username: process.env.TURN_USERNAME,
        credential: process.env.TURN_PASSWORD
      }]
    });

    return new Promise((resolve, reject) => {
      let hasRelay = false;

      pc.onicecandidate = (event) => {
        if (event.candidate && event.candidate.type === 'relay') {
          hasRelay = true;
          console.log('✅ TURN 서버 연결 성공');
          pc.close();
          resolve();
        }
      };

      pc.createDataChannel('test');
      pc.createOffer().then(offer => pc.setLocalDescription(offer));

      setTimeout(() => {
        pc.close();
        if (!hasRelay) {
          reject(new Error('❌ TURN 서버 연결 실패'));
        }
      }, 5000);
    });
  },

  // 코드 품질 확인
  async checkCodeQuality() {
    const { execSync } = require('child_process');

    try {
      // 린트 체크
      execSync('npm run lint', { stdio: 'inherit' });
      console.log('✅ 린트 검사 통과');

      // 타입 체크
      execSync('npm run type-check', { stdio: 'inherit' });
      console.log('✅ 타입 검사 통과');

      // 테스트
      execSync('npm run test', { stdio: 'inherit' });
      console.log('✅ 테스트 통과');

    } catch (error) {
      throw new Error('❌ 코드 품질 검사 실패');
    }
  },

  // 브라우저 지원 확인
  checkBrowserSupport() {
    const packageJson = require('./package.json');
    const browserslist = packageJson.browserslist;

    console.log('🌐 지원 브라우저:', browserslist);
    console.log('✅ 브라우저 설정 확인 완료');
  },

  // 에러 트래킹 설정 확인
  checkErrorTracking() {
    if (!process.env.SENTRY_DSN && process.env.NODE_ENV === 'production') {
      console.warn('⚠️ 에러 트래킹(Sentry 등)이 설정되지 않았습니다.');
    } else {
      console.log('✅ 에러 트래킹 설정 완료');
    }
  },

  // 분석 도구 확인
  checkAnalytics() {
    if (!process.env.GA_TRACKING_ID && process.env.NODE_ENV === 'production') {
      console.warn('⚠️ 분석 도구(Google Analytics 등)가 설정되지 않았습니다.');
    } else {
      console.log('✅ 분석 도구 설정 완료');
    }
  },

  // 전체 체크 실행
  async runAll() {
    console.log('🚀 배포 전 체크리스트 시작...\n');

    try {
      this.checkEnvironment();
      this.checkHTTPS();
      await this.checkBundleSize();
      await this.checkCodeQuality();
      this.checkBrowserSupport();
      await this.checkTURNServer();
      this.checkErrorTracking();
      this.checkAnalytics();

      console.log('\n🎉 모든 체크 완료! 배포 준비 완료!');
      return true;
    } catch (error) {
      console.error('\n💥 체크 실패:', error.message);
      console.log('\n배포를 중단하고 문제를 해결하세요.');
      return false;
    }
  }
};

// package.json에 스크립트 추가
// "scripts": {
//   "pre-deploy": "node deployment-checker.js",
//   "deploy": "npm run pre-deploy && npm run build && firebase deploy"
// }

// 사용 예시
if (require.main === module) {
  deploymentChecker.runAll()
    .then(success => process.exit(success ? 0 : 1));
}

module.exports = deploymentChecker;

설명

이것이 하는 일: 위 코드는 프로덕션 배포 전에 확인해야 할 모든 항목을 자동으로 체크하는 스크립트입니다. 마치 비행기 이륙 전 조종사가 체크리스트를 확인하는 것처럼, 하나씩 검증하면서 문제가 있으면 배포를 중단시켜요.

첫 번째로, checkEnvironment에서 필수 환경 변수가 모두 설정되어 있는지 확인합니다. 시그널링 서버 URL, TURN 서버 정보, API 키 등이 없으면 앱이 작동할 수 없으니 배포해봤자 무용지물이에요.

process.env에서 각 변수를 확인하고, 하나라도 없으면 에러를 throw하여 배포 프로세스를 즉시 중단합니다. 이렇게 하는 이유는 배포 후에 "아 환경 변수를 안 넣었네"라고 발견하는 것보다 배포 전에 미리 막는 것이 훨씬 효율적이기 때문이죠.

두 번째로, checkHTTPS에서 프로덕션 환경에서 WSS(Secure WebSocket)를 사용하는지 확인합니다. HTTP에서는 getUserMedia가 localhost를 제외하고 작동하지 않기 때문에 SSL 인증서가 필수에요.

checkBundleSize는 빌드된 JavaScript 파일 크기를 측정하여 너무 크면 경고를 보냅니다. 5MB가 넘으면 사용자의 초기 로딩 시간이 길어지므로 코드 스플리팅을 고려해야 하죠.

세 번째 단계로, checkTURNServer에서 실제로 TURN 서버에 연결을 시도합니다. 환경 변수에 URL만 설정했다고 끝이 아니라, 실제로 relay 타입의 ICE candidate가 생성되는지 확인하는 거예요.

내부적으로 임시 PeerConnection을 만들고 5초 안에 relay candidate가 수집되는지 검증합니다. checkCodeQuality에서는 execSync로 린트, 타입 체크, 테스트를 실행하여 코드에 문법 오류나 타입 에러가 없는지 확인해요.

마지막으로, runAll에서 모든 체크를 순차적으로 실행하고, 하나라도 실패하면 배포를 중단합니다. 성공하면 true를 반환하여 다음 단계(빌드, 실제 배포)로 진행할 수 있도록 하죠.

여러분이 이 코드를 사용하면 배포 실수로 인한 장애를 크게 줄일 수 있습니다. 실무에서는 이런 체크 스크립트를 CI/CD 파이프라인(GitHub Actions, GitLab CI, Jenkins 등)에 통합하면, 코드를 푸시할 때마다 자동으로 실행되어 문제를 조기에 발견할 수 있어요.

예를 들어 누군가 실수로 환경 변수를 지웠다면 PR이 머지되기 전에 자동으로 감지되죠. 또한 Slack이나 이메일로 체크 결과를 알림 받도록 설정하면, 배포 담당자가 직접 확인하지 않아도 자동으로 모니터링됩니다.

체크리스트를 문서화하여 팀 위키에 올리면 신입 개발자도 쉽게 배포 프로세스를 이해하고 따라할 수 있어요.

실전 팁

💡 체크리스트를 JSON이나 YAML로 정의하고 동적으로 로드하면, 프로젝트마다 다른 체크 항목을 쉽게 관리할 수 있어요

💡 배포 전에 스테이징 환경에서 먼저 테스트하세요. 프로덕션과 동일한 설정으로 만들어 실제 사용자 영향 없이 검증할 수 있습니다

💡 Blue-Green 배포나 Canary 배포를 사용하면 새 버전에 문제가 있을 때 빠르게 이전 버전으로 롤백할 수 있어요

💡 배포 후 최소 30분~1시간은 모니터링 대시보드를 주시하면서 에러율, 응답 시간, 사용자 행동을 확인하세요

💡 배포 체크리스트를 팀과 공유하고 정기적으로 업데이트하세요. 배포 중 발견한 새로운 이슈는 체크리스트에 추가하여 같은 실수를 반복하지 않도록 해요


#WebRTC#Testing#Optimization#Deployment#Performance#WebRTC,프로젝트

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

Gradient Descent 최적화 알고리즘 완벽 가이드

머신러닝의 핵심인 Gradient Descent 알고리즘을 초급자 눈높이에서 쉽게 설명합니다. 경사 하강법의 원리부터 실전 활용법까지, 실무에 바로 적용할 수 있는 내용을 담았습니다.

인덱스와 쿼리 성능 최적화 완벽 가이드

데이터베이스 성능의 핵심인 인덱스를 처음부터 끝까지 배워봅니다. B-Tree 구조부터 실행 계획 분석까지, 실무에서 바로 사용할 수 있는 인덱스 최적화 전략을 초급자도 이해할 수 있게 설명합니다.

SQL 서브쿼리 완벽 마스터 가이드

초급 개발자를 위한 SQL 서브쿼리 완벽 가이드입니다. WHERE, SELECT, FROM 절 서브쿼리부터 IN, EXISTS, 상관 서브쿼리까지 실무에서 바로 활용할 수 있는 예제와 함께 쉽게 설명합니다. 성능 최적화 팁까지 포함되어 있습니다.

Docker 배포와 CI/CD 완벽 가이드

Docker를 활용한 컨테이너 배포부터 GitHub Actions를 이용한 자동화 파이프라인까지, 초급 개발자도 쉽게 따라할 수 있는 실전 배포 가이드입니다. AWS EC2에 애플리케이션을 배포하고 SSL 인증서까지 적용하는 전 과정을 다룹니다.

보안 강화 및 테스트 완벽 가이드

웹 애플리케이션의 보안 취약점을 방어하고 안정적인 서비스를 제공하기 위한 실전 보안 기법과 테스트 전략을 다룹니다. XSS, CSRF부터 DDoS 방어, Rate Limiting까지 실무에서 바로 적용 가능한 보안 솔루션을 제공합니다.