이미지 로딩 중...

WebRTC 화면 공유 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 20. · 3 Views

WebRTC 화면 공유 완벽 가이드

WebRTC의 화면 공유 기능을 처음부터 끝까지 배워봅니다. getDisplayMedia API 사용법부터 화면과 카메라 동시 표시, 공유 중단 감지, 해상도 설정까지 실무에서 바로 활용할 수 있는 모든 것을 다룹니다.


목차

  1. getDisplayMedia API
  2. 화면 공유 시작/중지
  3. 화면 트랙 교체
  4. 화면과 카메라 동시 표시
  5. 공유 중단 감지
  6. 해상도 및 프레임레이트 설정

1. getDisplayMedia API

시작하며

여러분이 화상 회의 앱을 만들거나 원격 협업 도구를 개발할 때 "사용자의 화면을 공유하는 기능을 어떻게 구현하지?"라고 고민해본 적 있나요? 줌(Zoom)이나 구글 미트(Google Meet)처럼 자신의 화면을 다른 사람에게 보여주는 기능 말이죠.

과거에는 이런 기능을 만들려면 복잡한 플러그인을 설치하거나 특별한 소프트웨어를 사용해야 했습니다. 하지만 지금은 웹 브라우저 자체에서 제공하는 간단한 API만으로도 화면 공유 기능을 쉽게 구현할 수 있습니다.

바로 이럴 때 필요한 것이 getDisplayMedia API입니다. 이 API를 사용하면 단 몇 줄의 코드만으로 사용자의 화면, 특정 창, 또는 브라우저 탭을 캡처해서 다른 사용자와 공유할 수 있습니다.

개요

간단히 말해서, getDisplayMedia API는 사용자의 화면을 캡처해서 비디오 스트림으로 제공하는 웹 브라우저의 기능입니다. 마치 카메라로 영상을 찍듯이, 화면을 "촬영"해서 실시간으로 전송할 수 있게 해주는 거죠.

왜 이 API가 필요할까요? 온라인 교육 플랫폼에서 강사가 자신의 화면을 보여주거나, 원격 근무 중에 동료에게 작업 화면을 공유하거나, 고객 지원 시 문제 상황을 함께 확인할 때 매우 유용합니다.

예를 들어, 코드 리뷰를 할 때 에디터 화면을 공유하면서 실시간으로 설명할 수 있습니다. 기존에는 화면 공유를 위해 별도의 소프트웨어를 설치하거나 복잡한 설정이 필요했다면, 이제는 웹 페이지에서 버튼 하나만 클릭하면 바로 화면 공유가 시작됩니다.

사용자는 어떤 화면을 공유할지 선택만 하면 됩니다. 이 API의 핵심 특징은 세 가지입니다.

첫째, 사용자가 직접 공유할 화면을 선택하므로 보안이 철저합니다. 둘째, 별도의 플러그인 설치 없이 브라우저만으로 작동합니다.

셋째, getUserMedia(카메라/마이크)와 동일한 방식으로 사용할 수 있어 배우기 쉽습니다. 이러한 특징들이 개발자에게는 구현의 편리함을, 사용자에게는 안전하고 간편한 경험을 제공합니다.

코드 예제

// 화면 공유 스트림 가져오기
async function startScreenShare() {
  try {
    // 사용자에게 화면 선택 다이얼로그 표시
    const screenStream = await navigator.mediaDevices.getDisplayMedia({
      video: true,  // 화면 비디오 캡처 활성화
      audio: true   // 시스템 오디오도 함께 캡처 (선택사항)
    });

    // 비디오 요소에 스트림 연결
    const videoElement = document.getElementById('screenVideo');
    videoElement.srcObject = screenStream;

    console.log('화면 공유 시작:', screenStream.id);
    return screenStream;
  } catch (error) {
    console.error('화면 공유 실패:', error);
  }
}

설명

이것이 하는 일: getDisplayMedia API는 사용자의 컴퓨터 화면을 실시간으로 캡처해서 MediaStream 객체로 반환합니다. 이 스트림은 비디오 요소에 표시하거나, WebRTC를 통해 다른 사용자에게 전송할 수 있습니다.

첫 번째로, navigator.mediaDevices.getDisplayMedia()를 호출하면 브라우저가 자동으로 화면 선택 다이얼로그를 띄웁니다. 사용자는 이 다이얼로그에서 전체 화면, 특정 창, 또는 브라우저 탭 중 무엇을 공유할지 직접 선택합니다.

이렇게 함으로써 개발자가 임의로 화면을 캡처할 수 없게 만들어 사용자의 프라이버시를 보호합니다. 그 다음으로, 사용자가 화면을 선택하고 "공유" 버튼을 클릭하면 Promise가 resolve되면서 MediaStream 객체를 반환합니다.

이 스트림에는 화면의 비디오 트랙과 (설정에 따라) 오디오 트랙이 포함되어 있습니다. video: true 옵션은 화면 캡처를 활성화하고, audio: true는 시스템 사운드(예: YouTube 재생 중인 소리)도 함께 캡처하도록 합니다.

마지막으로, 반환된 스트림을 videoElement.srcObject에 할당하면 해당 비디오 요소에 화면 공유 영상이 실시간으로 표시됩니다. 이 스트림은 WebRTC의 RTCPeerConnection을 통해 다른 사용자에게도 전송할 수 있습니다.

여러분이 이 코드를 사용하면 화상 회의, 온라인 교육, 원격 지원 등 다양한 실시간 협업 기능을 웹 앱에 추가할 수 있습니다. 별도의 서버나 복잡한 인프라 없이도 브라우저만으로 화면 공유가 가능하며, 사용자 경험도 매우 직관적입니다.

또한 동일한 MediaStream API를 사용하므로 카메라 스트림과 동일한 방식으로 처리할 수 있어 코드 재사용성도 높습니다.

실전 팁

💡 audio: true를 사용할 때는 브라우저와 운영체제에 따라 시스템 오디오 캡처가 지원되지 않을 수 있습니다. 사용자에게 "시스템 오디오를 공유하려면 체크박스를 선택하세요"라는 안내를 제공하세요.

💡 getDisplayMedia()는 반드시 사용자 제스처(버튼 클릭 등)에 의해 호출되어야 합니다. 페이지 로드 시 자동으로 실행하면 브라우저가 차단합니다. 항상 버튼 이벤트 핸들러 안에서 호출하세요.

💡 사용자가 화면 공유를 거부하면 NotAllowedError가 발생합니다. try-catch로 에러를 처리하고, 사용자에게 친절한 메시지를 보여주세요. "화면 공유가 필요한 기능입니다. 다시 시도해주세요."

💡 모바일 브라우저에서는 getDisplayMedia 지원이 제한적입니다. 기능을 제공하기 전에 if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia)로 지원 여부를 확인하세요.

💡 화면 공유 스트림은 일반 카메라 스트림보다 해상도가 높아 네트워크 대역폭을 많이 사용합니다. 필요하다면 constraints에서 해상도를 제한하거나, 네트워크 상태에 따라 품질을 동적으로 조정하세요.


2. 화면 공유 시작/중지

시작하며

여러분이 화면 공유 기능을 만들었는데, 사용자가 "이제 그만 공유하고 싶어요"라고 할 때 어떻게 처리해야 할까요? 단순히 스트림을 얻는 것만큼이나 중요한 것이 바로 스트림을 깔끔하게 종료하는 것입니다.

실제 서비스를 운영하다 보면 사용자가 화면 공유를 시작했다가 중지하는 상황이 빈번하게 발생합니다. 예를 들어, 회의 중 특정 자료만 보여주고 다시 카메라로 돌아가거나, 개인정보가 포함된 화면이 나올 때 재빨리 공유를 중단해야 하는 경우가 있죠.

바로 이럴 때 필요한 것이 화면 공유의 시작과 중지를 제어하는 메커니즘입니다. 스트림의 모든 트랙을 올바르게 정리하고, 관련 리소스를 해제하여 메모리 누수를 방지하는 방법을 배워봅시다.

개요

간단히 말해서, 화면 공유 시작은 getDisplayMedia()로 스트림을 얻는 것이고, 중지는 해당 스트림의 모든 트랙을 stop() 메서드로 종료하는 것입니다. 마치 수도꼭지를 열고 닫는 것처럼 명확한 시작과 끝이 있어야 합니다.

왜 제대로 된 중지 처리가 필요할까요? 스트림을 중지하지 않으면 사용자의 화면이 계속 캡처되고, 브라우저 탭에 "공유 중" 표시가 사라지지 않으며, 메모리도 계속 사용됩니다.

예를 들어, 사용자가 회의를 종료했는데도 화면이 계속 캡처되고 있다면 심각한 프라이버시 문제가 발생할 수 있습니다. 기존에는 단순히 비디오 요소의 srcObject를 null로 설정하는 것으로 충분하다고 생각할 수 있지만, 이것만으로는 실제 미디어 트랙이 종료되지 않습니다.

제대로 하려면 스트림의 각 트랙을 명시적으로 stop()해야 하고, 이벤트 리스너도 정리해야 합니다. 이 패턴의 핵심 특징은 세 가지입니다.

첫째, 스트림 상태를 명확히 관리하여 언제든지 시작/중지할 수 있습니다. 둘째, 모든 트랙을 개별적으로 중지하여 완전한 정리를 보장합니다.

셋째, UI 상태와 실제 스트림 상태를 동기화하여 사용자에게 명확한 피드백을 제공합니다. 이러한 특징들이 안정적이고 사용자 친화적인 화면 공유 경험을 만들어냅니다.

코드 예제

// 화면 공유 시작/중지 관리
let screenStream = null;
const startButton = document.getElementById('startShare');
const stopButton = document.getElementById('stopShare');

async function startScreenShare() {
  try {
    // 화면 공유 시작
    screenStream = await navigator.mediaDevices.getDisplayMedia({
      video: { mediaSource: 'screen' }
    });

    // 비디오 요소에 연결
    document.getElementById('screenVideo').srcObject = screenStream;

    // UI 업데이트
    startButton.disabled = true;
    stopButton.disabled = false;

  } catch (error) {
    console.error('화면 공유 시작 실패:', error);
  }
}

function stopScreenShare() {
  if (screenStream) {
    // 모든 트랙 중지 (비디오, 오디오 포함)
    screenStream.getTracks().forEach(track => track.stop());

    // 비디오 요소 정리
    document.getElementById('screenVideo').srcObject = null;
    screenStream = null;

    // UI 업데이트
    startButton.disabled = false;
    stopButton.disabled = true;

    console.log('화면 공유 중지됨');
  }
}

// 버튼 이벤트 연결
startButton.addEventListener('click', startScreenShare);
stopButton.addEventListener('click', stopScreenShare);

설명

이것이 하는 일: 이 코드는 사용자가 버튼을 클릭해 화면 공유를 시작하고 중지할 수 있는 완전한 제어 시스템을 제공합니다. 스트림의 생명주기를 처음부터 끝까지 관리하며, UI도 함께 동기화합니다.

첫 번째로, startScreenShare() 함수는 getDisplayMedia()를 호출하여 화면 스트림을 얻고, 이를 전역 변수 screenStream에 저장합니다. 전역 변수로 저장하는 이유는 나중에 stopScreenShare()에서 이 스트림에 접근해서 중지해야 하기 때문입니다.

스트림을 얻은 후에는 비디오 요소에 연결하고, 버튼 상태를 업데이트하여 "시작" 버튼은 비활성화하고 "중지" 버튼은 활성화합니다. 그 다음으로, stopScreenShare() 함수가 핵심입니다.

getTracks() 메서드로 스트림의 모든 트랙(비디오, 오디오)을 배열로 가져온 다음, forEach로 각 트랙의 stop()을 호출합니다. 이렇게 하면 실제로 화면 캡처가 중단되고, 브라우저 탭의 "공유 중" 표시도 사라집니다.

단순히 srcObject를 null로 설정하는 것만으로는 트랙이 계속 실행되므로 반드시 stop()을 호출해야 합니다. 마지막으로, 비디오 요소의 srcObject를 null로 설정하고 screenStream 변수도 null로 초기화하여 메모리를 정리합니다.

그리고 UI 버튼 상태를 다시 반대로 전환하여 사용자가 언제든지 다시 화면 공유를 시작할 수 있도록 준비합니다. 여러분이 이 코드를 사용하면 사용자가 직관적으로 화면 공유를 제어할 수 있습니다.

회의 중 필요할 때만 화면을 공유하고, 개인정보가 나올 때는 즉시 중지할 수 있어 프라이버시도 보호됩니다. 또한 메모리 누수나 리소스 낭비 없이 깔끔한 정리가 이루어져 앱의 성능과 안정성도 향상됩니다.

버튼 상태 관리로 인해 사용자가 현재 공유 중인지 아닌지도 명확하게 알 수 있습니다.

실전 팁

💡 화면 공유 중지 시 반드시 getTracks().forEach(track => track.stop())을 사용하세요. 단순히 srcObject = null만 하면 트랙이 백그라운드에서 계속 실행되어 "공유 중" 표시가 사라지지 않습니다.

💡 페이지를 벗어나거나 새로고침할 때도 스트림을 정리해야 합니다. window.addEventListener('beforeunload', stopScreenShare)로 페이지 종료 시 자동 정리를 구현하세요.

💡 사용자가 브라우저의 "공유 중지" 버튼을 클릭할 수도 있습니다. track.onended 이벤트를 리스닝하여 외부에서 공유가 중단되었을 때 UI를 동기화하세요. 그렇지 않으면 버튼 상태가 실제 상태와 맞지 않게 됩니다.

💡 여러 탭에서 동시에 화면 공유를 할 수도 있지만, 일반적으로 한 번에 하나만 공유하는 것이 좋습니다. 새로운 공유를 시작하기 전에 기존 스트림을 자동으로 중지하는 로직을 추가하세요.

💡 디버깅 시 console.log(screenStream.getTracks())로 트랙 상태를 확인하세요. readyState가 'ended'면 정상적으로 중지된 것이고, 'live'면 여전히 실행 중입니다. 이를 통해 정리가 제대로 되었는지 검증할 수 있습니다.


3. 화면 트랙 교체

시작하며

여러분이 화상 회의 중에 카메라를 보여주다가 갑자기 화면 공유로 전환하고 싶을 때가 있죠? 또는 반대로 화면 공유를 하다가 다시 카메라로 돌아가고 싶을 때 말입니다.

이럴 때마다 연결을 끊었다가 다시 연결하면 상대방 화면이 끊기고 재연결되면서 불편한 경험을 주게 됩니다. 실제 프로덕션 환경에서는 이런 전환이 매우 자주 발생합니다.

프레젠테이션 중에 슬라이드를 보여주다가 본인 얼굴을 보여주거나, 코드 리뷰 중에 화면과 카메라를 번갈아가며 보여주는 경우가 대표적입니다. 매번 연결을 재설정하면 네트워크 오버헤드도 크고 사용자 경험도 나빠집니다.

바로 이럴 때 필요한 것이 트랙 교체(Track Replacement) 기능입니다. WebRTC의 RTCRtpSender.replaceTrack() 메서드를 사용하면 연결을 유지한 채로 실시간으로 비디오 트랙만 교체할 수 있습니다.

개요

간단히 말해서, 트랙 교체는 기존 WebRTC 연결을 유지한 상태에서 전송 중인 비디오 트랙만 다른 트랙으로 바꾸는 기술입니다. 마치 달리는 기차에서 화물만 교체하는 것처럼, 연결은 그대로 두고 내용만 바꾸는 거죠.

왜 이 기능이 필요할까요? WebRTC로 실시간 통신을 할 때, 새로운 연결을 맺는 과정(시그널링, ICE 협상 등)은 시간이 걸리고 복잡합니다.

예를 들어, 온라인 수업에서 강사가 화면과 카메라를 수시로 전환한다면, 매번 재연결하는 것은 비효율적입니다. 트랙 교체를 사용하면 즉시 전환이 가능합니다.

기존에는 스트림 전체를 교체하거나 PeerConnection을 재설정해야 했다면, 이제는 replaceTrack() 한 줄로 비디오 소스만 바꿀 수 있습니다. 오디오 트랙은 그대로 유지되고, 네트워크 연결도 끊기지 않으며, 상대방은 잠깐의 프레임 전환만 경험합니다.

이 기능의 핵심 특징은 세 가지입니다. 첫째, 재협상(renegotiation) 없이 즉시 트랙을 교체할 수 있어 빠릅니다.

둘째, 기존 연결의 모든 설정(코덱, 대역폭 등)이 유지됩니다. 셋째, 사용자는 거의 끊김 없는 매끄러운 전환을 경험합니다.

이러한 특징들이 고품질의 실시간 영상 전환을 가능하게 만듭니다.

코드 예제

// RTCPeerConnection이 이미 설정되어 있다고 가정
let peerConnection;
let currentStream;

// 카메라에서 화면 공유로 전환
async function switchToScreenShare() {
  // 화면 공유 스트림 획득
  const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
  const screenTrack = screenStream.getVideoTracks()[0];

  // 현재 비디오 트랙을 전송하는 sender 찾기
  const videoSender = peerConnection.getSenders()
    .find(sender => sender.track && sender.track.kind === 'video');

  if (videoSender) {
    // 트랙 교체 (재협상 없이 즉시 적용)
    await videoSender.replaceTrack(screenTrack);

    // 기존 트랙 중지
    if (currentStream) {
      currentStream.getVideoTracks().forEach(track => track.stop());
    }

    currentStream = screenStream;
    console.log('화면 공유로 전환 완료');
  }
}

// 화면 공유에서 카메라로 전환
async function switchToCamera() {
  // 카메라 스트림 획득
  const cameraStream = await navigator.mediaDevices.getUserMedia({ video: true });
  const cameraTrack = cameraStream.getVideoTracks()[0];

  // 비디오 sender 찾아서 트랙 교체
  const videoSender = peerConnection.getSenders()
    .find(sender => sender.track && sender.track.kind === 'video');

  if (videoSender) {
    await videoSender.replaceTrack(cameraTrack);

    if (currentStream) {
      currentStream.getVideoTracks().forEach(track => track.stop());
    }

    currentStream = cameraStream;
    console.log('카메라로 전환 완료');
  }
}

설명

이것이 하는 일: 이 코드는 WebRTC 통신 중에 카메라 영상과 화면 공유를 끊김 없이 전환하는 기능을 제공합니다. PeerConnection을 재설정하지 않고도 전송 중인 비디오 소스를 실시간으로 바꿀 수 있습니다.

첫 번째로, switchToScreenShare() 함수는 먼저 getDisplayMedia()로 화면 공유 스트림을 얻고, 그 중 비디오 트랙을 추출합니다. getVideoTracks()[0]는 스트림에서 첫 번째 비디오 트랙을 가져오는데, 일반적으로 화면 공유 스트림에는 비디오 트랙이 하나만 있습니다.

이 트랙이 실제로 상대방에게 전송될 화면 영상입니다. 그 다음으로, peerConnection.getSenders()로 현재 전송 중인 모든 미디어 sender를 가져옵니다.

이 중에서 video 타입인 sender를 find()로 찾습니다. RTCRtpSender는 로컬 미디어를 원격 피어에게 전송하는 역할을 하는 객체인데, 우리는 비디오를 전송하는 sender가 필요합니다.

찾았다면 videoSender.replaceTrack(screenTrack)로 기존 카메라 트랙을 화면 공유 트랙으로 교체합니다. 이 과정은 비동기로 처리되지만 매우 빠르게 완료됩니다.

마지막으로, 교체가 완료되면 기존 스트림의 비디오 트랙을 stop()으로 중지합니다. 이렇게 하지 않으면 카메라가 계속 켜져 있어 리소스가 낭비됩니다.

currentStream을 새로운 화면 공유 스트림으로 업데이트하여 다음 전환 시에도 올바르게 정리할 수 있도록 합니다. switchToCamera() 함수도 동일한 패턴으로 작동하며, getUserMedia()로 카메라 스트림을 얻는다는 점만 다릅니다.

여러분이 이 코드를 사용하면 화상 회의나 라이브 스트리밍 앱에서 전문가 수준의 영상 전환 기능을 구현할 수 있습니다. 사용자는 버튼 하나로 카메라와 화면 공유를 즉시 전환할 수 있고, 상대방은 연결 끊김 없이 매끄러운 전환을 경험합니다.

네트워크 재협상이 없으므로 전환 시간이 거의 0에 가깝고, ICE 연결도 유지되어 안정적입니다. 이는 사용자 만족도를 크게 향상시키는 중요한 기능입니다.

실전 팁

💡 replaceTrack()는 동일한 종류의 트랙만 교체할 수 있습니다. video 트랙은 video로, audio는 audio로만 교체 가능합니다. kind가 다르면 에러가 발생하므로 주의하세요.

💡 트랙 교체 후 반드시 기존 트랙을 stop()하세요. 그렇지 않으면 카메라나 화면 캡처가 백그라운드에서 계속 실행되어 배터리와 CPU를 낭비합니다. 사용자는 불필요하게 "카메라 사용 중" 표시를 계속 보게 됩니다.

💡 로컬 비디오 미리보기도 업데이트하세요. localVideoElement.srcObject = currentStream을 사용해 사용자가 자신의 화면도 확인할 수 있게 하면 UX가 향상됩니다.

💡 null을 replaceTrack()에 전달하면 비디오 전송을 일시 중단할 수 있습니다. 예를 들어, 잠시 아무것도 전송하지 않고 싶을 때 await videoSender.replaceTrack(null)을 사용하세요. 나중에 다시 트랙을 교체하면 재개됩니다.

💡 트랙 교체는 거의 즉시 일어나지만, 네트워크 상태에 따라 1-2프레임 정도 지연될 수 있습니다. UI에 "전환 중..." 같은 짧은 피드백을 보여주면 사용자 경험이 더 좋습니다.


4. 화면과 카메라 동시 표시

시작하며

여러분이 온라인 강의를 듣거나 유튜브 튜토리얼을 볼 때, 화면 한쪽에는 강사의 얼굴이 작게 보이고 나머지는 작업 화면이 보이는 경우를 본 적 있나요? 이런 "화면 속 화면(Picture-in-Picture)" 스타일은 시청자에게 훨씬 더 몰입감 있는 경험을 제공합니다.

이 기능은 단순히 보기 좋은 것을 넘어서 실용적인 가치가 큽니다. 원격 교육에서 강사의 표정과 제스처를 보면서 동시에 설명하는 내용을 화면으로 확인할 수 있고, 웨비나에서 발표자의 슬라이드와 얼굴을 함께 볼 수 있으며, 게임 스트리밍에서 플레이 화면과 스트리머의 리액션을 동시에 즐길 수 있습니다.

바로 이럴 때 필요한 것이 화면 공유와 카메라 스트림을 동시에 처리하는 기술입니다. 두 개의 독립적인 스트림을 각각 관리하고, WebRTC로 모두 전송하거나, 로컬에서 합성하여 표시하는 방법을 배워봅시다.

개요

간단히 말해서, 화면과 카메라를 동시에 표시하는 것은 두 개의 독립적인 미디어 스트림을 병렬로 획득하고 관리하는 작업입니다. 마치 두 대의 카메라로 동시에 촬영하는 것처럼, 화면과 웹캠을 모두 캡처합니다.

왜 이 기능이 필요할까요? 단일 소스만 보여주면 정보 전달이 제한적입니다.

예를 들어, 코딩 튜토리얼에서 코드만 보여주면 강사의 열정이나 강조하는 부분을 느끼기 어렵고, 얼굴만 보여주면 실제 작업 과정을 알 수 없습니다. 두 가지를 함께 보여주면 훨씬 풍부한 커뮤니케이션이 가능합니다.

기존에는 OBS 같은 외부 소프트웨어를 사용해야 화면과 카메라를 합성할 수 있었다면, 이제는 웹 브라우저만으로도 가능합니다. getUserMedia()로 카메라를 얻고, getDisplayMedia()로 화면을 얻은 다음, 각각을 비디오 요소에 표시하거나, Canvas API로 합성하거나, WebRTC의 다중 트랙 기능으로 모두 전송할 수 있습니다.

이 패턴의 핵심 특징은 세 가지입니다. 첫째, 두 스트림을 독립적으로 관리하여 각각 시작/중지할 수 있습니다.

둘째, 필요에 따라 로컬에서 합성하거나 별도로 전송할 수 있어 유연합니다. 셋째, CSS나 Canvas로 레이아웃을 자유롭게 구성할 수 있습니다.

이러한 특징들이 방송 품질의 화면 공유 경험을 웹에서도 가능하게 만듭니다.

코드 예제

// 화면과 카메라를 동시에 표시
let screenStream = null;
let cameraStream = null;

async function startBothStreams() {
  try {
    // 카메라 스트림 획득
    cameraStream = await navigator.mediaDevices.getUserMedia({
      video: { width: 320, height: 240 },  // 작은 크기로 설정
      audio: true  // 마이크 오디오 사용
    });

    // 화면 공유 스트림 획득
    screenStream = await navigator.mediaDevices.getDisplayMedia({
      video: { width: 1920, height: 1080 },  // 큰 크기로 설정
      audio: false  // 화면 오디오는 제외
    });

    // 각 스트림을 별도 비디오 요소에 연결
    document.getElementById('cameraVideo').srcObject = cameraStream;
    document.getElementById('screenVideo').srcObject = screenStream;

    // WebRTC로 전송하는 경우
    if (peerConnection) {
      // 카메라와 화면 트랙을 모두 추가
      cameraStream.getTracks().forEach(track => {
        peerConnection.addTrack(track, cameraStream);
      });
      screenStream.getVideoTracks().forEach(track => {
        peerConnection.addTrack(track, screenStream);
      });
    }

    console.log('화면과 카메라 모두 활성화됨');
  } catch (error) {
    console.error('스트림 획득 실패:', error);
  }
}

function stopBothStreams() {
  // 모든 스트림 정리
  [cameraStream, screenStream].forEach(stream => {
    if (stream) {
      stream.getTracks().forEach(track => track.stop());
    }
  });

  document.getElementById('cameraVideo').srcObject = null;
  document.getElementById('screenVideo').srcObject = null;

  cameraStream = null;
  screenStream = null;
}

설명

이것이 하는 일: 이 코드는 카메라와 화면 공유를 동시에 시작하고, 각각을 독립적으로 관리하며, 필요하다면 WebRTC로 모두 전송하는 완전한 시스템을 제공합니다. 마치 방송국의 멀티 카메라 시스템처럼 여러 소스를 동시에 처리합니다.

첫 번째로, startBothStreams() 함수는 순차적으로 두 개의 스트림을 획득합니다. 먼저 getUserMedia()로 카메라를 얻는데, 해상도를 320x240으로 작게 설정한 이유는 일반적으로 카메라는 화면 구석에 작게 표시되기 때문입니다.

오디오도 true로 설정하여 마이크 입력을 받습니다. 그 다음 getDisplayMedia()로 화면을 얻는데, 이쪽은 1920x1080의 고해상도로 설정하여 선명한 화면 공유를 제공합니다.

화면의 오디오는 false로 설정하여 중복을 피합니다. 그 다음으로, 두 스트림을 각각 다른 비디오 요소에 연결합니다.

cameraVideo는 작은 PIP 창으로, screenVideo는 메인 화면으로 표시할 수 있습니다. CSS를 사용하면 카메라 영상을 화면 우하단에 오버레이하는 등 다양한 레이아웃을 구현할 수 있습니다.

두 스트림이 완전히 독립적이므로 각각의 볼륨, 크기, 위치를 자유롭게 조정할 수 있습니다. 마지막으로, WebRTC로 원격 전송이 필요한 경우 peerConnection.addTrack()으로 모든 트랙을 추가합니다.

cameraStream의 getTracks()는 비디오와 오디오 트랙을 모두 포함하고, screenStream의 getVideoTracks()는 비디오만 포함합니다. 이렇게 하면 상대방은 여러분의 카메라 영상, 마이크 소리, 그리고 화면 공유를 모두 받을 수 있습니다.

상대방 측에서는 여러 개의 미디어 스트림을 받아서 원하는 대로 레이아웃할 수 있습니다. 여러분이 이 코드를 사용하면 전문적인 온라인 강의 플랫폼, 웨비나 서비스, 라이브 스트리밍 앱을 만들 수 있습니다.

사용자는 자신의 얼굴과 작업 화면을 동시에 공유하여 더 효과적으로 소통할 수 있고, 시청자는 풍부한 시각 정보를 받아 이해도가 높아집니다. 각 스트림을 독립적으로 제어할 수 있어 필요에 따라 카메라만 끄거나 화면만 바꾸는 등 유연한 조작도 가능합니다.

실전 팁

💡 카메라 해상도를 낮게 설정하면 CPU와 네트워크 대역폭을 절약할 수 있습니다. 작은 PIP 창에는 320x240이나 640x480이면 충분하며, 화면 공유는 텍스트가 선명해야 하므로 1080p 이상을 권장합니다.

💡 CSS로 카메라 비디오를 position: absolute와 z-index로 화면 위에 오버레이하면 "화면 속 화면" 효과를 쉽게 만들 수 있습니다. 우하단에 배치하는 것이 일반적입니다.

💡 두 스트림의 오디오가 겹치지 않도록 주의하세요. 일반적으로 마이크 오디오만 사용하고 화면 오디오는 비활성화하거나, 필요하다면 Web Audio API로 믹싱하세요. 두 오디오가 모두 활성화되면 에코나 중복 소리가 발생할 수 있습니다.

💡 Canvas API를 사용하면 두 스트림을 하나로 합성할 수도 있습니다. 캔버스에 화면을 그리고 그 위에 카메라 영상을 작게 오버레이한 후, canvas.captureStream()으로 합성된 단일 스트림을 만들 수 있습니다. 이렇게 하면 수신 측에서 레이아웃을 신경 쓸 필요가 없습니다.

💡 모바일 환경에서는 두 스트림을 동시에 처리하기 부담스러울 수 있습니다. 디바이스 성능을 감지하여 모바일에서는 화면 공유만, 데스크톱에서는 둘 다 제공하는 등 적응형 전략을 사용하세요.


5. 공유 중단 감지

시작하며

여러분이 화면 공유 기능을 구현했는데, 사용자가 브라우저의 "공유 중지" 버튼을 클릭했을 때 앱이 그 사실을 모르고 있다면 어떻게 될까요? UI는 여전히 "공유 중"이라고 표시하고, 실제로는 아무것도 전송되지 않는 혼란스러운 상황이 발생합니다.

이런 문제는 실제 서비스에서 빈번하게 발생합니다. 사용자는 앱 내부의 버튼 대신 브라우저의 네이티브 UI를 사용할 수도 있고, 시스템 권한 설정에서 공유를 차단할 수도 있으며, 공유 중인 창을 닫을 수도 있습니다.

이런 모든 경우에 앱은 공유가 중단되었다는 것을 알아야 합니다. 바로 이럴 때 필요한 것이 트랙의 ended 이벤트를 리스닝하는 것입니다.

화면 공유가 외부 요인으로 중단되면 비디오 트랙이 자동으로 'ended' 상태가 되는데, 이를 감지하여 앱의 상태를 동기화할 수 있습니다.

개요

간단히 말해서, 공유 중단 감지는 MediaStreamTrack의 onended 이벤트 핸들러를 등록하여 트랙이 종료되는 순간을 포착하는 것입니다. 마치 전화가 끊겼을 때 신호음이 들리는 것처럼, 화면 공유가 중단되면 이벤트가 발생합니다.

왜 이 기능이 필요할까요? 사용자는 여러 방법으로 화면 공유를 중단할 수 있습니다.

브라우저 주소창 옆의 "공유 중지" 버튼, 시스템 트레이의 "공유 중단", 또는 공유 중인 탭이나 창을 닫는 것 등이 있습니다. 예를 들어, 사용자가 실수로 공유 중인 탭을 닫았는데 앱이 여전히 "공유 중"이라고 표시한다면, 재시작하려고 할 때 혼란스럽고 버튼도 제대로 작동하지 않습니다.

기존에는 주기적으로 트랙의 readyState를 확인하는 폴링 방식을 사용했다면, 이제는 이벤트 기반 방식으로 즉시 반응할 수 있습니다. onended 핸들러를 등록하면 트랙이 종료되는 순간 자동으로 콜백이 실행되어 UI를 업데이트하고, 리소스를 정리하고, 사용자에게 알림을 줄 수 있습니다.

이 패턴의 핵심 특징은 세 가지입니다. 첫째, 중단 원인과 관계없이 모든 경우를 캐치할 수 있습니다.

둘째, 실시간으로 반응하여 앱 상태와 실제 상태의 불일치를 최소화합니다. 셋째, 사용자에게 명확한 피드백을 제공하여 다음 행동을 안내할 수 있습니다.

이러한 특징들이 견고하고 사용자 친화적인 화면 공유 경험을 만듭니다.

코드 예제

// 화면 공유 트랙의 종료 감지
async function startScreenShareWithEndDetection() {
  try {
    const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
    const videoTrack = screenStream.getVideoTracks()[0];

    // 트랙 종료 이벤트 리스너 등록
    videoTrack.onended = () => {
      console.log('화면 공유가 중단되었습니다');

      // UI 업데이트
      updateUIForStopped();

      // 스트림 정리
      cleanupStream(screenStream);

      // 사용자에게 알림
      showNotification('화면 공유가 종료되었습니다');

      // WebRTC 연결이 있다면 트랙 제거
      if (peerConnection) {
        const sender = peerConnection.getSenders()
          .find(s => s.track === videoTrack);
        if (sender) {
          peerConnection.removeTrack(sender);
        }
      }
    };

    // 비디오 요소에 연결
    document.getElementById('screenVideo').srcObject = screenStream;
    updateUIForSharing();

    return screenStream;
  } catch (error) {
    console.error('화면 공유 시작 실패:', error);
  }
}

function updateUIForSharing() {
  document.getElementById('startButton').disabled = true;
  document.getElementById('stopButton').disabled = false;
  document.getElementById('status').textContent = '공유 중';
}

function updateUIForStopped() {
  document.getElementById('startButton').disabled = false;
  document.getElementById('stopButton').disabled = true;
  document.getElementById('status').textContent = '공유 안 함';
}

function cleanupStream(stream) {
  if (stream) {
    stream.getTracks().forEach(track => track.stop());
  }
  document.getElementById('screenVideo').srcObject = null;
}

function showNotification(message) {
  // 사용자에게 알림 표시 (Toast, Alert 등)
  alert(message);  // 실제로는 더 나은 UI 사용
}

설명

이것이 하는 일: 이 코드는 화면 공유 스트림을 시작할 때 비디오 트랙에 종료 이벤트 리스너를 등록하여, 어떤 방식으로든 공유가 중단되면 즉시 감지하고 적절히 대응하는 시스템을 제공합니다. 첫 번째로, getDisplayMedia()로 화면 스트림을 얻은 후 getVideoTracks()[0]로 비디오 트랙을 추출합니다.

이 트랙 객체가 화면 캡처의 실제 생명주기를 관리하는 핵심입니다. 트랙에 onended 이벤트 핸들러를 등록하면, 사용자가 브라우저의 "공유 중지" 버튼을 클릭하거나, 공유 중인 탭/창을 닫거나, 시스템 권한을 변경하는 등 어떤 이유로든 트랙이 종료될 때 이 콜백이 자동으로 실행됩니다.

그 다음으로, onended 콜백 안에서 여러 정리 작업을 수행합니다. 먼저 updateUIForStopped()로 버튼 상태를 원래대로 되돌립니다.

사용자가 "시작" 버튼을 다시 클릭할 수 있도록 활성화하고, "중지" 버튼은 비활성화하며, 상태 텍스트도 "공유 안 함"으로 바꿉니다. cleanupStream()으로 스트림의 모든 트랙을 stop()하고 비디오 요소를 null로 정리하여 메모리를 해제합니다.

비록 트랙이 이미 종료되었지만, 명시적으로 stop()을 호출하는 것이 좋은 습관입니다. 마지막으로, WebRTC로 원격 전송 중이었다면 peerConnection에서 해당 트랙을 제거합니다.

getSenders()로 모든 sender를 찾아서 종료된 videoTrack과 일치하는 sender를 찾고, removeTrack()으로 제거합니다. 이렇게 하면 상대방도 더 이상 빈 화면을 받지 않습니다.

showNotification()으로 사용자에게 "화면 공유가 종료되었습니다"라는 알림을 보여주면 무슨 일이 일어났는지 명확하게 알 수 있습니다. 여러분이 이 코드를 사용하면 사용자가 어떤 방식으로 화면 공유를 중단하든 앱이 항상 정확한 상태를 유지합니다.

브라우저 UI를 사용하든, 앱 버튼을 사용하든, 창을 닫든 관계없이 즉시 감지하고 적절히 반응합니다. 이는 버그와 혼란을 크게 줄이고, 사용자가 언제든지 화면 공유를 재시작할 수 있게 만들어 전체적인 안정성과 사용성을 향상시킵니다.

실전 팁

💡 onended 핸들러를 등록하지 않으면 브라우저 버튼으로 공유를 중지했을 때 앱이 모르고 있어 "공유 중" 상태가 계속 유지됩니다. 이는 매우 혼란스러운 UX를 만들므로 반드시 등록하세요.

💡 오디오 트랙도 별도로 ended 이벤트를 발생시킬 수 있습니다. 화면 공유 스트림에 오디오가 포함되어 있다면 getAudioTracks()[0]에도 onended를 등록하여 오디오만 중단되는 경우를 처리하세요.

💡 onended 대신 addEventListener('ended', callback)를 사용할 수도 있습니다. 이 방식은 여러 리스너를 등록할 수 있어 더 유연하지만, 대부분의 경우 onended만으로 충분합니다.

💡 사용자에게 알림을 보낼 때는 너무 공격적이지 않게 하세요. alert() 대신 토스트 메시지나 상태 표시줄을 사용하면 더 부드러운 경험을 제공합니다. 사용자는 의도적으로 중단한 것이므로 방해하지 않는 것이 좋습니다.

💡 트랙의 readyState를 확인하면 현재 상태를 알 수 있습니다. 'live'면 활성화, 'ended'면 종료된 것입니다. 디버깅 시 console.log(videoTrack.readyState)로 상태를 확인하면 도움이 됩니다.


6. 해상도 및 프레임레이트 설정

시작하며

여러분이 화면 공유를 할 때, 텍스트가 흐릿하게 보이거나 마우스 움직임이 끊기는 경험을 해본 적 있나요? 또는 반대로 고화질 화면 공유 때문에 네트워크가 느려지고 영상이 버퍼링되는 경우도 있을 겁니다.

이런 문제들은 모두 해상도와 프레임레이트 설정과 관련이 있습니다. 실제 서비스를 운영할 때는 다양한 사용 사례를 고려해야 합니다.

코드 리뷰나 문서 공유처럼 텍스트가 중요한 경우에는 높은 해상도가 필요하지만, 동영상을 함께 보거나 게임 화면을 공유할 때는 높은 프레임레이트가 더 중요합니다. 또한 사용자의 네트워크 상황에 따라 적절한 품질을 선택해야 합니다.

바로 이럴 때 필요한 것이 getDisplayMedia의 constraints 옵션을 활용한 해상도와 프레임레이트 설정입니다. 적절한 값을 설정하면 선명도와 부드러움 사이의 균형을 맞추고, 네트워크 효율성도 최적화할 수 있습니다.

개요

간단히 말해서, 해상도와 프레임레이트 설정은 getDisplayMedia()의 video constraints를 통해 화면 캡처의 품질과 성능을 제어하는 것입니다. 마치 카메라의 화질 설정을 조정하듯이, 화면 공유의 선명도와 프레임 수를 원하는 대로 설정할 수 있습니다.

왜 이 설정이 필요할까요? 기본 설정은 모든 상황에 맞지 않습니다.

예를 들어, 4K 모니터에서 기본 설정으로 공유하면 너무 큰 해상도로 인해 수신자의 브라우저가 느려지거나, 네트워크 대역폭이 부족해 끊기는 문제가 발생합니다. 반대로 너무 낮은 해상도로 설정하면 코드나 작은 글씨가 읽기 어려워집니다.

기존에는 브라우저가 자동으로 해상도를 결정했다면, 이제는 개발자가 width, height, frameRate 같은 constraints를 명시적으로 지정할 수 있습니다. ideal 값으로 권장 설정을 제공하거나, max/min으로 범위를 제한할 수 있으며, 사용자의 네트워크 상태나 사용 사례에 따라 동적으로 조정할 수도 있습니다.

이 기능의 핵심 특징은 세 가지입니다. 첫째, 해상도와 프레임레이트를 독립적으로 제어하여 최적의 품질/성능 균형을 찾을 수 있습니다.

둘째, ideal, max, min 키워드로 유연한 설정이 가능하여 다양한 디바이스에 대응할 수 있습니다. 셋째, 실시간으로 constraints를 변경하여 네트워크 상황에 적응할 수 있습니다.

이러한 특징들이 전문적인 화면 공유 서비스를 만들 수 있게 해줍니다.

코드 예제

// 다양한 화질 프리셋 정의
const QUALITY_PRESETS = {
  // 고화질: 텍스트 작업, 코딩에 적합
  high: {
    video: {
      width: { ideal: 1920 },
      height: { ideal: 1080 },
      frameRate: { ideal: 30, max: 30 }
    }
  },
  // 중화질: 일반 공유에 적합
  medium: {
    video: {
      width: { ideal: 1280 },
      height: { ideal: 720 },
      frameRate: { ideal: 24, max: 30 }
    }
  },
  // 저화질: 느린 네트워크에 적합
  low: {
    video: {
      width: { ideal: 854 },
      height: { ideal: 480 },
      frameRate: { ideal: 15, max: 20 }
    }
  },
  // 고프레임레이트: 동영상, 게임에 적합
  smooth: {
    video: {
      width: { ideal: 1280 },
      height: { ideal: 720 },
      frameRate: { ideal: 60, max: 60 }
    }
  }
};

// 사용자 선택에 따라 화면 공유 시작
async function startScreenShareWithQuality(qualityPreset = 'medium') {
  try {
    const constraints = QUALITY_PRESETS[qualityPreset];

    const screenStream = await navigator.mediaDevices.getDisplayMedia(constraints);
    const videoTrack = screenStream.getVideoTracks()[0];

    // 실제 적용된 설정 확인
    const settings = videoTrack.getSettings();
    console.log('적용된 설정:', {
      width: settings.width,
      height: settings.height,
      frameRate: settings.frameRate
    });

    document.getElementById('screenVideo').srcObject = screenStream;
    return screenStream;
  } catch (error) {
    console.error('화면 공유 실패:', error);
  }
}

// 네트워크 상태에 따라 자동 조정
async function startAdaptiveScreenShare() {
  // 네트워크 속도 추정 (예시)
  const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
  const effectiveType = connection?.effectiveType;

  let quality = 'medium';
  if (effectiveType === '4g') quality = 'high';
  else if (effectiveType === '3g') quality = 'medium';
  else if (effectiveType === '2g' || effectiveType === 'slow-2g') quality = 'low';

  console.log(`네트워크: ${effectiveType}, 품질: ${quality}`);
  return startScreenShareWithQuality(quality);
}

설명

이것이 하는 일: 이 코드는 화면 공유의 해상도와 프레임레이트를 상황에 맞게 설정할 수 있는 프리셋 시스템과, 네트워크 상태에 따라 자동으로 품질을 조정하는 적응형 시스템을 제공합니다. 첫 번째로, QUALITY_PRESETS 객체는 네 가지 사용 사례에 맞는 설정을 미리 정의합니다.

'high'는 1920x1080에 30fps로 코드 리뷰나 디자인 공유처럼 텍스트 선명도가 중요한 경우에 적합합니다. 'medium'은 1280x720에 24fps로 일반적인 화면 공유에 좋은 균형을 제공합니다.

'low'는 854x480에 15fps로 네트워크가 느린 상황에서도 끊김 없이 공유할 수 있습니다. 'smooth'는 720p에 60fps로 동영상 재생이나 게임 화면처럼 움직임이 많은 경우에 부드러운 경험을 제공합니다.

그 다음으로, startScreenShareWithQuality() 함수는 선택된 프리셋의 constraints를 getDisplayMedia()에 전달합니다. ideal 키워드는 브라우저에게 "가능하면 이 값을 사용하세요"라고 권장하는 것이고, max는 "절대 이 값을 넘지 마세요"라고 제한하는 것입니다.

예를 들어, frameRate: { ideal: 30, max: 30 }은 정확히 30fps를 요청합니다. 실제로 적용된 설정은 videoTrack.getSettings()로 확인할 수 있는데, 이는 브라우저나 시스템 제약으로 인해 요청한 값과 다를 수 있기 때문입니다.

마지막으로, startAdaptiveScreenShare() 함수는 사용자의 네트워크 상태를 자동으로 감지하여 최적의 프리셋을 선택합니다. navigator.connection API를 사용하면 현재 네트워크 타입(4g, 3g, 2g 등)을 알 수 있습니다.

4G 네트워크에서는 고화질을, 3G에서는 중화질을, 2G에서는 저화질을 자동으로 선택하여 사용자 경험을 최적화합니다. 이렇게 하면 빠른 네트워크에서는 선명한 화면을, 느린 네트워크에서는 끊김 없는 공유를 제공할 수 있습니다.

여러분이 이 코드를 사용하면 다양한 사용자 환경에 대응하는 견고한 화면 공유 서비스를 만들 수 있습니다. 사용자는 직접 품질을 선택하거나, 시스템이 자동으로 최적화할 수 있습니다.

코드 리뷰 시에는 고화질로 작은 글씨도 선명하게 보이고, 네트워크가 느린 카페에서도 저화질로 끊김 없이 공유할 수 있으며, 게임 스트리밍 시에는 고프레임레이트로 부드러운 움직임을 제공합니다. 이는 사용자 만족도를 크게 향상시키고, 다양한 상황에서도 안정적인 서비스를 보장합니다.

실전 팁

💡 ideal 값은 권장사항이므로 브라우저가 다른 값을 선택할 수 있습니다. 정확한 값이 필요하면 exact 키워드를 사용하되, 이 경우 지원하지 않는 환경에서는 실패할 수 있으니 주의하세요. 일반적으로 ideal이 더 안전합니다.

💡 높은 해상도와 프레임레이트는 CPU 사용량과 네트워크 대역폭을 크게 증가시킵니다. 1080p@60fps는 720p@30fps보다 약 4배의 데이터를 생성하므로, 필요한 경우에만 사용하세요. 대부분의 화면 공유에는 720p@24fps면 충분합니다.

💡 사용자에게 품질 선택 옵션을 제공하세요. "고화질(텍스트 작업)", "중화질(일반)", "저화질(느린 네트워크)", "부드러움(동영상/게임)" 같은 설명과 함께 제공하면 사용자가 자신의 상황에 맞게 선택할 수 있습니다.

💡 WebRTC로 전송 시 getStats() API를 사용해 실시간 대역폭 사용량을 모니터링하세요. 네트워크가 혼잡하면 자동으로 해상도나 프레임레이트를 낮추는 적응형 비트레이트(ABR) 로직을 구현할 수 있습니다.

💡 모바일 디바이스에서는 더 낮은 설정을 사용하세요. 모바일은 CPU와 배터리가 제한적이므로 데스크톱보다 한 단계 낮은 프리셋을 기본값으로 설정하는 것이 좋습니다. 예를 들어, 데스크톱에서 'high'를 사용한다면 모바일에서는 'medium'을 사용하세요.


#WebRTC#getDisplayMedia#화면공유#MediaStream#PeerConnection#WebRTC,화면공유

댓글 (0)

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

함께 보면 좋은 카드 뉴스

WebRTC 실시간 채팅 구현 완벽 가이드

WebRTC의 RTCDataChannel을 활용하여 서버 없이 브라우저 간 실시간 채팅을 구현하는 방법을 배웁니다. 메시지 전송부터 파일 공유, 읽음 표시까지 실무에서 바로 사용할 수 있는 완벽한 채팅 시스템을 만들어봅니다.

WebRTC Mesh 방식 다자간 화상회의 완벽 가이드

다자간 화상회의를 구현하는 Mesh, SFU, MCU 방식의 차이부터 실제 구현까지. PeerConnection 관리, 참여자 추가/제거, 대역폭 최적화까지 실무에 바로 적용할 수 있는 완벽한 가이드입니다.

1:1 화상 통화 구현 완벽 가이드

WebRTC를 활용한 1:1 화상 통화 시스템을 처음부터 끝까지 구현하는 방법을 배워봅니다. 통화 시작부터 종료까지, 음소거와 카메라 제어, 연결 품질 모니터링까지 실무에 바로 적용할 수 있는 모든 과정을 다룹니다.

ICE Candidate 처리 완벽 가이드

WebRTC 연결을 위한 ICE Candidate 처리 과정을 초급 개발자도 쉽게 이해할 수 있도록 설명합니다. onicecandidate 이벤트부터 addIceCandidate 메서드, Trickle ICE 구현, NAT 트래버설까지 실무에서 바로 적용 가능한 예제와 함께 안내합니다.

WebRTC RTCPeerConnection 생성과 관리 완벽 가이드

WebRTC의 핵심인 RTCPeerConnection을 생성하고 관리하는 방법을 배웁니다. ICE 서버 설정부터 미디어 트랙 추가, 이벤트 관리, 연결 상태 모니터링까지 실무에서 바로 사용할 수 있는 완전한 가이드입니다.