이미지 로딩 중...

WebRTC 오디오/비디오 제어 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 20. · 7 Views

WebRTC 오디오/비디오 제어 완벽 가이드

실시간 화상회의나 영상통화 앱을 개발할 때 필수적인 WebRTC의 미디어 제어 기능들을 마스터해보세요. 음소거, 카메라 전환, 볼륨 조절 등 실무에서 바로 활용할 수 있는 모든 제어 방법을 상세한 예제와 함께 배워봅니다.


목차

  1. 음소거/음소거_해제
  2. 비디오_활성화/비활성화
  3. 카메라_전환_(전면/후면)
  4. 마이크/스피커_변경
  5. 볼륨_조절
  6. 미디어_트랙_교체

1. 음소거/음소거_해제

시작하며

여러분이 화상회의 앱을 개발하고 있다고 상상해보세요. 사용자가 회의 중에 잠깐 자리를 비우거나 주변이 시끄러울 때 음소거 버튼을 누르면 즉시 반응해야 합니다.

하지만 단순히 버튼만 만든다고 해서 끝이 아닙니다. 실제로 마이크 입력을 제어하고, UI를 업데이트하고, 상대방에게도 상태를 알려줘야 하죠.

이런 문제는 Zoom이나 Google Meet 같은 모든 실시간 통신 앱에서 필수적으로 해결해야 합니다. 사용자는 버튼 클릭 한 번에 즉각적인 반응을 기대하고, 조금이라도 지연되면 앱의 품질이 낮다고 느낍니다.

더 나아가 음소거가 제대로 작동하지 않으면 개인정보 유출이나 회의 방해 같은 심각한 문제로 이어질 수 있습니다. 바로 이럴 때 필요한 것이 MediaStreamTrack의 enabled 속성입니다.

이 간단한 속성 하나로 실시간 오디오 스트림을 즉시 제어할 수 있고, 사용자에게 완벽한 음소거 경험을 제공할 수 있습니다.

개요

간단히 말해서, 음소거는 마이크에서 들어오는 오디오 데이터를 실시간으로 차단하는 기능입니다. 이를 WebRTC에서는 MediaStreamTrack의 enabled 속성을 false로 설정하여 구현합니다.

왜 이 기능이 필요한지 실무 관점에서 설명하면, 사용자가 회의 중에 잠깐 말을 하지 않거나 주변 소음을 차단하고 싶을 때 필수적입니다. 예를 들어, 재택근무 중 강아지가 짖거나 택배가 도착할 때 빠르게 음소거할 수 있어야 합니다.

또한 네트워크 대역폭을 절약하고 상대방의 청취 경험을 개선하는 데도 도움이 됩니다. 전통적인 방법으로는 오디오 엘리먼트의 volume을 0으로 설정하거나 오디오 컨텍스트를 조작했습니다.

하지만 이제는 MediaStreamTrack의 enabled 속성만 토글하면 됩니다. 이 방법은 네트워크 전송도 중단시키므로 더 효율적입니다.

이 개념의 핵심 특징은 첫째, 즉각적인 반응 속도입니다. enabled를 false로 설정하는 순간 오디오 데이터 전송이 중단됩니다.

둘째, 네트워크 효율성입니다. 단순히 볼륨을 0으로 만드는 게 아니라 데이터 전송 자체를 중단합니다.

셋째, 간단한 구현입니다. 복잡한 오디오 처리 로직 없이 단 한 줄의 코드로 제어할 수 있습니다.

이러한 특징들이 중요한 이유는 실시간 통신 앱에서 사용자 경험과 성능이 직결되기 때문입니다. 빠른 반응 속도는 사용자 만족도를 높이고, 네트워크 효율성은 더 안정적인 통화 품질을 보장합니다.

코드 예제

// MediaStream에서 오디오 트랙 가져오기
const localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
const audioTrack = localStream.getAudioTracks()[0];

// 음소거 상태를 토글하는 함수
function toggleMute() {
  // enabled 속성을 반전시켜서 음소거/해제 전환
  audioTrack.enabled = !audioTrack.enabled;

  // UI 업데이트 (버튼 텍스트나 아이콘 변경)
  const muteButton = document.getElementById('muteBtn');
  muteButton.textContent = audioTrack.enabled ? '🎤 음소거' : '🔇 음소거 해제';

  // 콘솔에 현재 상태 출력 (디버깅용)
  console.log(`마이크 상태: ${audioTrack.enabled ? '활성' : '음소거'}`);
}

// 버튼 클릭 이벤트 연결
document.getElementById('muteBtn').addEventListener('click', toggleMute);

설명

이것이 하는 일: 사용자의 마이크에서 들어오는 오디오 데이터를 실시간으로 차단하거나 다시 활성화합니다. enabled 속성을 조작하여 오디오 트랙의 전송 여부를 즉시 제어합니다.

첫 번째로, getUserMedia로 획득한 MediaStream에서 getAudioTracks() 메서드로 오디오 트랙을 추출합니다. 이 트랙 객체가 바로 마이크 입력을 제어하는 핵심입니다.

MediaStream은 여러 개의 트랙을 담고 있는 컨테이너라고 생각하면 되고, 우리는 그 중에서 오디오 트랙만 꺼내서 제어하는 것입니다. 그 다음으로, toggleMute 함수 내에서 audioTrack.enabled의 값을 반전시킵니다.

enabled가 true면 false로, false면 true로 바꾸는 것이죠. 이 순간 WebRTC 엔진이 자동으로 오디오 데이터의 전송을 중단하거나 재개합니다.

브라우저 내부에서는 마이크 입력 자체는 계속 받지만, 네트워크로 전송하지 않도록 처리합니다. 세 번째 단계에서는 UI를 업데이트합니다.

사용자에게 현재 음소거 상태를 명확히 보여주는 것이 중요합니다. 버튼의 텍스트나 아이콘을 바꿔서 시각적 피드백을 제공합니다.

이렇게 하면 사용자가 현재 자신의 마이크가 켜져 있는지 꺼져 있는지 한눈에 알 수 있습니다. 여러분이 이 코드를 사용하면 즉각적으로 반응하는 음소거 기능을 구현할 수 있습니다.

네트워크 대역폭도 절약되고, 사용자 경험도 향상됩니다. 또한 코드가 매우 간결해서 유지보수하기도 쉽고, 버그가 발생할 가능성도 낮습니다.

실무에서는 이 기능에 추가로 원격 참가자들에게 음소거 상태를 알리는 시그널링 로직을 추가하면 됩니다. 예를 들어 WebSocket이나 Socket.io를 통해 "사용자 A가 음소거했음"이라는 메시지를 보내서 다른 참가자의 UI도 업데이트할 수 있습니다.

실전 팁

💡 음소거 상태를 서버나 다른 참가자에게 알리는 시그널링을 반드시 추가하세요. 그래야 상대방 화면에도 "음소거됨" 표시가 나타납니다.

💡 enabled를 false로 설정해도 마이크 권한은 유지됩니다. 따라서 다시 true로 설정할 때 추가 권한 요청 없이 즉시 활성화됩니다.

💡 음소거 단축키(예: Spacebar)를 제공하면 사용자 편의성이 크게 향상됩니다. 키보드 이벤트 리스너를 추가해보세요.

💡 모바일 브라우저에서는 오디오 권한 정책이 까다로우니 반드시 사용자 제스처(터치, 클릭) 후에 getUserMedia를 호출하세요.

💡 음소거 상태를 localStorage에 저장하면 페이지 새로고침 후에도 사용자의 선호도를 유지할 수 있습니다.


2. 비디오_활성화/비활성화

시작하며

여러분이 화상회의 중에 잠깐 자리를 정돈하거나 옷차림을 고쳐야 할 때가 있죠? 또는 배터리나 네트워크 대역폭을 절약하고 싶을 때가 있습니다.

이럴 때 사용자는 카메라를 끄는 버튼을 클릭하고, 앱은 즉시 비디오 전송을 중단해야 합니다. 단순히 화면을 검은색으로 만드는 게 아니라 실제로 비디오 데이터 전송을 중단해야 합니다.

이 기능은 프라이버시 보호와 직결됩니다. 사용자가 카메라를 끄면 정말로 꺼졌는지 확신할 수 있어야 하고, 상대방도 "비디오 꺼짐" 상태를 명확히 인식해야 합니다.

또한 비디오는 오디오보다 훨씬 많은 네트워크 대역폭을 사용하므로, 비활성화하면 통화 품질이 크게 개선될 수 있습니다. 바로 이럴 때 필요한 것이 비디오 트랙의 enabled 속성 제어입니다.

오디오 음소거와 동일한 원리로, 비디오 트랙의 enabled를 조작하여 카메라 전송을 실시간으로 제어할 수 있습니다.

개요

간단히 말해서, 비디오 활성화/비활성화는 카메라에서 들어오는 영상 데이터를 실시간으로 차단하거나 재개하는 기능입니다. MediaStreamTrack의 enabled 속성을 사용하여 비디오 트랙을 제어합니다.

왜 이 기능이 필요한지 실무 관점에서 설명하면, 프라이버시 보호가 가장 중요합니다. 사용자가 카메라를 끄면 상대방에게 영상이 전송되지 않는다는 확신을 줘야 합니다.

예를 들어, 화상면접 중에 잠깐 자료를 찾거나 메모를 할 때 카메라를 끌 수 있어야 합니다. 또한 저사양 기기나 느린 네트워크 환경에서 비디오를 끄면 오디오 품질이 크게 향상됩니다.

전통적인 방법으로는 video 엘리먼트를 숨기거나 CSS로 가렸습니다. 하지만 이 방법은 실제로 비디오 데이터가 계속 전송되므로 네트워크 대역폭을 낭비합니다.

이제는 비디오 트랙의 enabled 속성을 false로 설정하면 데이터 전송 자체가 중단됩니다. 이 개념의 핵심 특징은 첫째, 실제 데이터 전송 중단입니다.

단순히 화면을 가리는 게 아니라 네트워크 전송을 멈춥니다. 둘째, 카메라 리소스 관리입니다.

트랙을 비활성화해도 카메라 권한은 유지되므로 다시 활성화할 때 빠릅니다. 셋째, 배터리 절약입니다.

특히 모바일 기기에서 비디오 인코딩과 전송을 중단하면 배터리 소모가 크게 줄어듭니다. 이러한 특징들이 중요한 이유는 사용자의 프라이버시를 보호하고, 제한된 리소스를 효율적으로 사용하며, 전체적인 통화 품질을 개선할 수 있기 때문입니다.

코드 예제

// MediaStream에서 비디오 트랙 가져오기
const localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
const videoTrack = localStream.getVideoTracks()[0];

// 비디오 활성화/비활성화를 토글하는 함수
function toggleVideo() {
  // enabled 속성을 반전시켜서 비디오 켜기/끄기 전환
  videoTrack.enabled = !videoTrack.enabled;

  // UI 업데이트 (버튼과 비디오 프리뷰)
  const videoButton = document.getElementById('videoBtn');
  const videoElement = document.getElementById('localVideo');

  videoButton.textContent = videoTrack.enabled ? '📹 비디오 끄기' : '📷 비디오 켜기';

  // 비디오가 꺼지면 placeholder 이미지 표시 (선택사항)
  videoElement.classList.toggle('video-off', !videoTrack.enabled);

  console.log(`카메라 상태: ${videoTrack.enabled ? '활성' : '비활성'}`);
}

// 버튼 클릭 이벤트 연결
document.getElementById('videoBtn').addEventListener('click', toggleVideo);

설명

이것이 하는 일: 사용자의 카메라에서 들어오는 비디오 데이터를 실시간으로 차단하거나 다시 활성화합니다. enabled 속성을 조작하여 비디오 트랙의 전송 여부를 즉시 제어합니다.

첫 번째로, getUserMedia로 획득한 MediaStream에서 getVideoTracks() 메서드로 비디오 트랙을 추출합니다. 이 트랙 객체가 카메라 입력을 제어하는 핵심입니다.

오디오와 마찬가지로 MediaStream은 여러 트랙을 담고 있고, 우리는 그 중 비디오 트랙만 제어합니다. 그 다음으로, toggleVideo 함수에서 videoTrack.enabled 값을 반전시킵니다.

이 순간 WebRTC 엔진이 비디오 인코딩과 네트워크 전송을 중단합니다. 카메라 자체는 여전히 열려 있지만, 실제 데이터는 전송되지 않습니다.

이렇게 하면 나중에 다시 켤 때 카메라 초기화 시간 없이 즉시 활성화할 수 있습니다. 세 번째 단계에서는 UI를 업데이트합니다.

버튼 텍스트를 바꾸고, 로컬 비디오 프리뷰에 "비디오 꺼짐" 표시를 추가합니다. 많은 앱들이 비디오가 꺼지면 사용자의 프로필 이미지나 이니셜을 표시합니다.

이런 시각적 피드백이 사용자 경험을 크게 향상시킵니다. 여러분이 이 코드를 사용하면 프라이버시를 보호하면서도 빠르게 비디오를 제어할 수 있습니다.

네트워크 사용량이 줄어들어 오디오 품질이 개선되고, 배터리 소모도 감소합니다. 특히 모바일 환경에서 이 효과가 두드러집니다.

실무에서는 비디오를 끌 때 상대방에게 시그널링으로 알려서 상대방 화면에도 "비디오 꺼짐" 상태를 표시해야 합니다. 또한 비디오가 꺼져 있는 동안에는 화면 공유나 가상 배경 같은 비디오 관련 기능들을 비활성화하는 것이 좋습니다.

마지막으로, 일부 브라우저에서는 비디오 트랙을 오래 비활성화하면 카메라 LED가 꺼질 수 있습니다. 이것은 좋은 사용자 경험이지만, 다시 켤 때 약간의 지연이 발생할 수 있으니 이 점을 고려해서 UX를 설계하세요.

실전 팁

💡 비디오를 끈 상태에서는 사용자의 이니셜이나 프로필 이미지를 표시하면 UX가 훨씬 좋아집니다.

💡 enabled를 false로 설정하면 네트워크 전송뿐만 아니라 비디오 인코딩도 중단되어 CPU 사용량이 크게 줄어듭니다.

💡 화상회의 시작 시 기본적으로 비디오를 꺼놓고 시작하는 옵션을 제공하면 사용자가 준비할 시간을 가질 수 있습니다.

💡 모바일에서는 비디오가 배터리를 가장 많이 소모하므로, 저전력 모드에서 자동으로 비디오를 끄는 기능을 고려해보세요.

💡 비디오 트랙의 muted 이벤트를 리스닝하면 브라우저나 OS 레벨에서 카메라가 차단되는 상황을 감지할 수 있습니다.


3. 카메라_전환_(전면/후면)

시작하며

여러분이 모바일로 화상통화를 하다가 뒤쪽에 있는 무언가를 보여주고 싶을 때가 있죠? 또는 전면 카메라에서 후면 카메라로 바꿔서 더 좋은 화질로 촬영하고 싶을 때가 있습니다.

Instagram이나 Snapchat 같은 앱에서 카메라 전환 버튼을 누르면 부드럽게 전환되는 것을 경험해보셨을 겁니다. 이 기능은 모바일 화상회의 앱에서 필수입니다.

단순히 카메라를 전환하는 것을 넘어서, 기존 연결을 유지하면서 부드럽게 전환해야 합니다. 만약 전환 과정에서 통화가 끊기거나 몇 초 동안 화면이 멈추면 사용자는 큰 불편을 느낍니다.

바로 이럴 때 필요한 것이 MediaStreamTrack.applyConstraints()와 replaceTrack() 메서드입니다. 이 두 방법으로 기존 연결을 유지하면서 카메라만 부드럽게 전환할 수 있습니다.

개요

간단히 말해서, 카메라 전환은 현재 사용 중인 비디오 트랙을 다른 카메라의 트랙으로 교체하는 기능입니다. facingMode constraint를 사용하여 전면('user')과 후면('environment') 카메라를 지정합니다.

왜 이 기능이 필요한지 실무 관점에서 설명하면, 모바일 환경에서 매우 중요합니다. 예를 들어, 화상 상담 중에 제품을 보여주거나, 원격 기술 지원 중에 문제가 있는 기기를 보여줄 때 필수적입니다.

또한 후면 카메라는 일반적으로 전면 카메라보다 해상도가 높아서 더 선명한 영상을 제공할 수 있습니다. 전통적인 방법으로는 기존 MediaStream을 완전히 중단하고 새로 getUserMedia를 호출했습니다.

하지만 이 방법은 PeerConnection을 재설정해야 하고, 화면이 잠깐 끊기는 문제가 있었습니다. 이제는 RTCRtpSender.replaceTrack()을 사용하면 연결을 유지한 채로 트랙만 교체할 수 있습니다.

이 개념의 핵심 특징은 첫째, 끊김 없는 전환입니다. PeerConnection을 유지한 채로 비디오 트랙만 교체하므로 통화가 끊기지 않습니다.

둘째, facingMode constraint로 카메라를 명시적으로 지정할 수 있습니다. 셋째, 기존 설정(해상도, 프레임레이트 등)을 유지하거나 변경할 수 있습니다.

이러한 특징들이 중요한 이유는 모바일 사용자에게 매끄러운 경험을 제공하고, 다양한 상황에서 유연하게 대응할 수 있기 때문입니다. 카메라 전환이 부드럽지 않으면 앱의 품질이 낮다고 느껴집니다.

코드 예제

let currentFacingMode = 'user'; // 'user' = 전면, 'environment' = 후면

// 카메라를 전환하는 함수
async function switchCamera() {
  // 현재와 반대 방향으로 전환
  currentFacingMode = currentFacingMode === 'user' ? 'environment' : 'user';

  try {
    // 새로운 카메라로 MediaStream 획득
    const newStream = await navigator.mediaDevices.getUserMedia({
      video: { facingMode: currentFacingMode },
      audio: true
    });

    const newVideoTrack = newStream.getVideoTracks()[0];

    // 로컬 비디오 프리뷰 업데이트
    const localVideo = document.getElementById('localVideo');
    localVideo.srcObject = newStream;

    // PeerConnection의 sender 찾아서 트랙 교체 (끊김 없이 전환)
    const sender = peerConnection.getSenders().find(s => s.track?.kind === 'video');
    if (sender) {
      await sender.replaceTrack(newVideoTrack);
    }

    // 기존 트랙 정리
    oldVideoTrack?.stop();

    console.log(`카메라 전환 완료: ${currentFacingMode === 'user' ? '전면' : '후면'}`);
  } catch (error) {
    console.error('카메라 전환 실패:', error);
    // 에러 발생 시 이전 방향으로 되돌리기
    currentFacingMode = currentFacingMode === 'user' ? 'environment' : 'user';
  }
}

설명

이것이 하는 일: 현재 사용 중인 카메라를 전면과 후면 사이에서 전환하면서도 WebRTC 연결을 끊지 않고 유지합니다. facingMode를 변경하고 새 트랙으로 교체합니다.

첫 번째로, currentFacingMode 변수를 토글하여 전환할 방향을 결정합니다. 'user'는 전면 카메라를 의미하고, 'environment'는 후면 카메라를 의미합니다.

이 이름들은 WebRTC 표준에서 정의된 것으로, 모든 브라우저에서 동일하게 작동합니다. 그 다음으로, 새로운 facingMode로 getUserMedia를 다시 호출합니다.

이때 브라우저는 지정된 방향의 카메라를 찾아서 MediaStream을 생성합니다. 만약 해당 방향의 카메라가 없으면 (예: 데스크톱) 에러가 발생하므로 try-catch로 처리해야 합니다.

세 번째 단계에서는 로컬 비디오 프리뷰를 업데이트합니다. 사용자가 자신의 화면에서 카메라가 전환된 것을 즉시 볼 수 있어야 합니다.

srcObject를 새 스트림으로 설정하면 비디오 엘리먼트가 자동으로 새 스트림을 재생합니다. 가장 중요한 네 번째 단계는 PeerConnection의 sender를 찾아서 replaceTrack()을 호출하는 것입니다.

이 메서드는 RTP 연결을 유지한 채로 비디오 트랙만 교체합니다. 상대방은 잠깐의 끊김도 없이 새 카메라의 영상을 받게 됩니다.

이것이 기존 방식(전체 재연결)과의 가장 큰 차이점입니다. 마지막으로 기존 비디오 트랙을 stop()으로 정리합니다.

이렇게 하지 않으면 이전 카메라가 계속 활성화된 채로 남아서 리소스를 낭비합니다. 또한 일부 기기에서는 두 카메라를 동시에 사용할 수 없어서 에러가 발생할 수 있습니다.

여러분이 이 코드를 사용하면 Instagram처럼 부드러운 카메라 전환 기능을 구현할 수 있습니다. 사용자는 통화가 끊기지 않으면서도 자유롭게 카메라를 바꿀 수 있고, 이는 모바일 화상회의 앱의 필수 기능입니다.

실전 팁

💡 데스크톱에서는 여러 웹캠이 연결되어 있을 수 있으니 enumerateDevices()로 사용 가능한 카메라 목록을 먼저 확인하세요.

💡 카메라 전환 버튼은 모바일에서만 보이도록 하고, 데스크톱에서는 드롭다운으로 카메라를 선택하게 하는 것이 좋습니다.

💡 replaceTrack()은 해상도나 프레임레이트가 달라도 작동하지만, 상대방의 디코더가 적응하는 데 1-2초 걸릴 수 있습니다.

💡 iOS Safari에서는 facingMode: 'exact' 대신 ideal을 사용하는 것이 더 안정적입니다.

💡 카메라 전환 중에는 버튼을 비활성화하고 로딩 표시를 보여주면 사용자가 연속으로 클릭하는 것을 방지할 수 있습니다.


4. 마이크/스피커_변경

시작하며

여러분이 화상회의를 하다가 에어팟에서 블루투스 헤드셋으로 바꾸고 싶거나, 노트북 내장 마이크 대신 외부 USB 마이크를 사용하고 싶을 때가 있습니다. 또는 회의실에서 여러 명이 함께 참여할 때 스피커를 큰 외부 스피커로 전환하고 싶을 수 있죠.

이 기능은 특히 전문적인 환경에서 중요합니다. 팟캐스트 녹음, 온라인 강의, 원격 면접 등에서 오디오 품질은 매우 중요하고, 적절한 오디오 장치를 선택할 수 있어야 합니다.

기본 장치만 사용하도록 강제하면 사용자는 불편함을 느끼고, 앱의 전문성에 의문을 가지게 됩니다. 바로 이럴 때 필요한 것이 enumerateDevices()와 deviceId constraint입니다.

사용 가능한 오디오 장치 목록을 가져오고, 특정 장치를 선택하여 사용할 수 있습니다.

개요

간단히 말해서, 마이크/스피커 변경은 현재 사용 중인 오디오 입출력 장치를 다른 장치로 전환하는 기능입니다. MediaDevices.enumerateDevices()로 장치 목록을 가져오고, deviceId constraint로 특정 장치를 지정합니다.

왜 이 기능이 필요한지 실무 관점에서 설명하면, 오디오 품질 향상이 가장 큰 이유입니다. 예를 들어, 전문 마이크는 노트북 내장 마이크보다 훨씬 선명한 음질을 제공합니다.

또한 블루투스 이어폰이 연결되거나 해제될 때 자동으로 또는 수동으로 오디오 장치를 전환할 수 있어야 합니다. 회의실에서 여러 명이 참여할 때는 고품질 스피커폰을 선택하는 것이 필수입니다.

전통적인 방법으로는 브라우저가 기본 장치를 자동으로 선택했고, 사용자가 변경할 수 없었습니다. 운영체제 설정에서 기본 장치를 바꿔야 했죠.

하지만 이제는 웹 앱 내에서 직접 장치를 선택할 수 있습니다. 이 개념의 핵심 특징은 첫째, 동적 장치 감지입니다.

devicechange 이벤트로 장치 연결/해제를 실시간으로 감지할 수 있습니다. 둘째, 세밀한 제어입니다.

마이크와 스피커를 각각 독립적으로 선택할 수 있습니다. 셋째, 사용자 경험 향상입니다.

Zoom이나 Teams처럼 설정 메뉴에서 장치를 테스트하고 선택할 수 있습니다. 이러한 특징들이 중요한 이유는 전문적인 오디오 품질을 제공하고, 다양한 하드웨어 환경에 유연하게 대응하며, 사용자에게 완전한 제어권을 주기 때문입니다.

코드 예제

// 사용 가능한 오디오 장치 목록 가져오기
async function getAudioDevices() {
  const devices = await navigator.mediaDevices.enumerateDevices();

  // audioinput(마이크), audiooutput(스피커)로 필터링
  const microphones = devices.filter(d => d.kind === 'audioinput');
  const speakers = devices.filter(d => d.kind === 'audiooutput');

  return { microphones, speakers };
}

// 특정 마이크로 전환하는 함수
async function changeMicrophone(deviceId) {
  try {
    // 선택한 마이크로 새 스트림 획득
    const newStream = await navigator.mediaDevices.getUserMedia({
      audio: { deviceId: { exact: deviceId } },
      video: true
    });

    const newAudioTrack = newStream.getAudioTracks()[0];

    // PeerConnection의 오디오 sender 찾아서 트랙 교체
    const sender = peerConnection.getSenders().find(s => s.track?.kind === 'audio');
    if (sender) {
      await sender.replaceTrack(newAudioTrack);
    }

    // 기존 오디오 트랙 정리
    oldAudioTrack?.stop();

    console.log(`마이크 변경 완료: ${newAudioTrack.label}`);
  } catch (error) {
    console.error('마이크 변경 실패:', error);
  }
}

// 스피커 변경 (Chrome/Edge만 지원)
async function changeSpeaker(deviceId) {
  const audioElement = document.getElementById('remoteAudio');

  // setSinkId는 Chrome/Edge에서만 사용 가능
  if (audioElement.setSinkId) {
    await audioElement.setSinkId(deviceId);
    console.log(`스피커 변경 완료: ${deviceId}`);
  } else {
    console.warn('이 브라우저는 스피커 선택을 지원하지 않습니다');
  }
}

설명

이것이 하는 일: 컴퓨터에 연결된 모든 오디오 입출력 장치를 감지하고, 사용자가 선택한 장치로 실시간 전환합니다. 마이크는 getUserMedia의 deviceId constraint로, 스피커는 HTMLMediaElement.setSinkId()로 제어합니다.

첫 번째로, enumerateDevices() 메서드를 호출하여 모든 미디어 장치 목록을 가져옵니다. 이 목록에는 마이크(audioinput), 스피커(audiooutput), 카메라(videoinput)가 모두 포함됩니다.

각 장치는 고유한 deviceId와 사람이 읽을 수 있는 label을 가지고 있습니다. label을 보려면 먼저 getUserMedia로 권한을 얻어야 합니다.

그 다음으로, 마이크를 전환할 때는 deviceId를 exact constraint로 지정하여 getUserMedia를 다시 호출합니다. 'exact'를 사용하면 정확히 그 장치를 사용하거나 실패합니다.

'ideal'을 사용하면 해당 장치를 우선하되, 없으면 다른 장치를 사용합니다. 대부분의 경우 exact가 더 예측 가능하므로 권장됩니다.

세 번째 단계에서는 새로 얻은 오디오 트랙으로 PeerConnection의 sender를 교체합니다. 카메라 전환과 동일한 방식으로 replaceTrack()을 사용하여 끊김 없이 마이크를 전환할 수 있습니다.

기존 오디오 트랙은 반드시 stop()으로 정리해야 이전 마이크의 리소스가 해제됩니다. 스피커 변경은 조금 다릅니다.

HTMLMediaElement(audio 또는 video 태그)의 setSinkId() 메서드를 사용합니다. 이 메서드는 Chrome과 Edge에서만 지원되고, Firefox와 Safari에서는 아직 지원되지 않습니다.

따라서 기능 감지(feature detection)를 반드시 해야 합니다. 여러분이 이 코드를 사용하면 Zoom이나 Teams처럼 전문적인 오디오 장치 선택 기능을 구현할 수 있습니다.

사용자는 고품질 마이크로 전환하여 더 선명한 음질로 대화할 수 있고, 상황에 맞는 스피커를 선택하여 최적의 청취 환경을 만들 수 있습니다. 실무에서는 devicechange 이벤트 리스너를 추가하여 이어폰이 연결되거나 해제될 때 UI를 업데이트하고, 선택적으로 자동 전환 기능을 제공할 수 있습니다.

또한 설정 페이지에서 각 장치를 테스트할 수 있는 기능(마이크 레벨 미터, 스피커 테스트 사운드)을 추가하면 사용자 경험이 크게 향상됩니다.

실전 팁

💡 enumerateDevices()를 호출하기 전에 반드시 getUserMedia로 권한을 먼저 얻어야 장치의 label이 표시됩니다.

💡 devicechange 이벤트를 리스닝하면 블루투스 이어폰 연결/해제를 실시간으로 감지하여 UI를 업데이트할 수 있습니다.

💡 setSinkId()는 Chrome/Edge만 지원하므로, 다른 브라우저에서는 "시스템 기본 스피커 사용" 메시지를 표시하세요.

💡 마이크 선택 드롭다운에 "시스템 기본값" 옵션을 추가하고, deviceId 없이 getUserMedia를 호출하면 기본 장치를 사용합니다.

💡 설정 페이지에서 마이크 레벨 미터를 보여주면 사용자가 각 마이크의 감도를 비교하며 선택할 수 있습니다.


5. 볼륨_조절

시작하며

여러분이 화상회의 중에 상대방의 목소리가 너무 작거나 너무 클 때가 있습니다. 또는 여러 명이 참여하는 회의에서 특정 사람의 볼륨만 조절하고 싶을 때가 있죠.

시스템 볼륨을 조절할 수도 있지만, 그러면 다른 앱의 소리까지 영향을 받습니다. 이 기능은 사용자 맞춤 경험에 필수적입니다.

누군가는 마이크를 입에 가까이 대고 말하고, 누군가는 멀리 떨어져서 말합니다. 각 참가자의 오디오 레벨이 다르므로, 개별적으로 볼륨을 조절할 수 있어야 쾌적한 청취 환경을 만들 수 있습니다.

Discord나 Slack Huddle 같은 앱들이 이 기능을 제공하는 이유입니다. 바로 이럴 때 필요한 것이 HTMLMediaElement의 volume 속성과 Web Audio API의 GainNode입니다.

간단한 조절은 volume 속성으로, 고급 기능은 GainNode로 구현할 수 있습니다.

개요

간단히 말해서, 볼륨 조절은 원격 참가자의 오디오 출력 레벨을 실시간으로 높이거나 낮추는 기능입니다. HTMLMediaElement의 volume 속성(0.0~1.0)이나 Web Audio API의 GainNode를 사용합니다.

왜 이 기능이 필요한지 실무 관점에서 설명하면, 오디오 밸런스 조정이 핵심입니다. 예를 들어, 다자간 회의에서 한 사람은 조용하고 다른 사람은 시끄러울 때 각각의 볼륨을 조절하여 균형을 맞출 수 있습니다.

또한 배경 음악이나 효과음이 있는 스트리밍에서 음성과 음악의 볼륨을 따로 조절할 수 있습니다. 청각 장애가 있는 사용자를 위해 볼륨을 크게 키울 수도 있습니다.

전통적인 방법으로는 시스템 볼륨을 조절하거나 브라우저 탭 전체의 볼륨을 조절했습니다. 하지만 이 방법은 다른 소리까지 영향을 받고, 개별 참가자를 제어할 수 없었습니다.

이제는 각 오디오 스트림마다 독립적으로 볼륨을 조절할 수 있습니다. 이 개념의 핵심 특징은 첫째, 참가자별 개별 제어입니다.

각 원격 참가자의 볼륨을 독립적으로 조절할 수 있습니다. 둘째, 실시간 조정입니다.

슬라이더를 움직이는 즉시 볼륨이 변경됩니다. 셋째, 범위 확장입니다.

Web Audio API를 사용하면 1.0 이상으로 볼륨을 증폭할 수도 있습니다. 이러한 특징들이 중요한 이유는 다양한 오디오 환경에 유연하게 대응하고, 사용자가 자신에게 맞는 최적의 청취 환경을 만들 수 있기 때문입니다.

코드 예제

// 방법 1: 간단한 볼륨 조절 (HTMLMediaElement 사용)
function setSimpleVolume(audioElement, volume) {
  // volume은 0.0(무음)에서 1.0(최대) 사이의 값
  audioElement.volume = Math.max(0, Math.min(1, volume));
  console.log(`볼륨 설정: ${audioElement.volume * 100}%`);
}

// 방법 2: 고급 볼륨 조절 (Web Audio API 사용)
function setupAdvancedVolumeControl(mediaStream) {
  // Web Audio API 컨텍스트 생성
  const audioContext = new AudioContext();

  // MediaStream을 Web Audio 노드로 변환
  const source = audioContext.createMediaStreamSource(mediaStream);

  // GainNode 생성 (볼륨 조절용)
  const gainNode = audioContext.createGain();

  // 노드 연결: source -> gainNode -> destination(스피커)
  source.connect(gainNode);
  gainNode.connect(audioContext.destination);

  // 볼륨 조절 함수 반환 (0.0 이상도 가능, 증폭 효과)
  return (volume) => {
    gainNode.gain.value = volume;
    console.log(`고급 볼륨 설정: ${volume * 100}%`);
  };
}

// 사용 예시
const remoteAudio = document.getElementById('remoteAudio');
const volumeSlider = document.getElementById('volumeSlider');

volumeSlider.addEventListener('input', (e) => {
  const volume = e.target.value / 100; // 0-100 범위를 0.0-1.0으로 변환
  setSimpleVolume(remoteAudio, volume);
});

설명

이것이 하는 일: 원격 참가자의 오디오 출력 레벨을 동적으로 조절합니다. 간단한 방법은 audio 엘리먼트의 volume 속성을, 고급 방법은 Web Audio API의 GainNode를 사용합니다.

첫 번째 방법은 HTMLMediaElement의 volume 속성을 직접 설정하는 것입니다. 이 값은 0.0(완전 무음)부터 1.0(최대 볼륨) 사이여야 합니다.

매우 간단하고 직관적이며, 대부분의 경우 이 방법으로 충분합니다. 슬라이더의 값을 0-100 범위로 받아서 100으로 나누면 0.0-1.0 범위로 변환됩니다.

두 번째 방법은 Web Audio API를 사용하는 것입니다. 먼저 AudioContext를 생성하고, MediaStream을 createMediaStreamSource()로 오디오 소스 노드로 변환합니다.

이 소스 노드는 실시간 오디오 데이터의 시작점입니다. Web Audio API는 노드 기반 아키텍처를 사용하므로, 여러 노드를 연결하여 복잡한 오디오 처리를 할 수 있습니다.

그 다음으로 GainNode를 생성합니다. GainNode는 오디오 신호의 크기를 조절하는 역할을 합니다.

gain.value 속성을 변경하여 실시간으로 볼륨을 조절할 수 있습니다. HTMLMediaElement.volume과 달리 1.0 이상의 값도 설정할 수 있어서 오디오를 증폭할 수 있습니다.

다만 너무 높은 값은 오디오 왜곡(clipping)을 유발할 수 있으니 주의해야 합니다. 노드를 연결하는 단계가 중요합니다.

source -> gainNode -> destination 순서로 connect()를 호출합니다. destination은 기본적으로 사용자의 스피커를 의미합니다.

이렇게 연결하면 오디오 데이터가 소스에서 출발하여 GainNode를 거쳐 증폭되거나 감소된 후 스피커로 출력됩니다. 여러분이 첫 번째 방법을 사용하면 간단하고 빠르게 볼륨 조절 기능을 추가할 수 있습니다.

대부분의 화상회의 앱에서는 이 정도로 충분합니다. 두 번째 방법은 더 복잡하지만, 볼륨 증폭, 이퀄라이저, 노이즈 감소 같은 고급 오디오 처리를 추가할 수 있는 확장성을 제공합니다.

실무에서는 사용자별 볼륨 설정을 localStorage에 저장하여 다음 세션에서도 유지하는 것이 좋습니다. 또한 "모두 음소거" 버튼을 제공하면 긴급 상황에서 유용합니다.

실전 팁

💡 volume 속성은 0.0-1.0 범위를 넘어서 설정할 수 없으므로, 증폭이 필요하면 Web Audio API를 사용하세요.

💡 슬라이더는 로그 스케일로 만들면 인간의 청각 특성과 더 잘 맞아서 자연스러운 조절이 가능합니다.

💡 각 참가자의 볼륨 설정을 localStorage에 저장하면 다음 회의에서도 같은 설정이 유지됩니다.

💡 Web Audio API를 사용할 때는 AudioContext를 재사용하세요. 매번 새로 만들면 메모리 낭비가 심합니다.

💡 볼륨 슬라이더 옆에 현재 오디오 레벨을 시각적으로 표시하면 사용자가 적절한 볼륨을 찾기 쉽습니다.


6. 미디어_트랙_교체

시작하며

여러분이 화상회의 중에 화면 공유로 전환하거나, 카메라 영상에서 미리 녹화된 비디오로 바꾸고 싶을 때가 있습니다. 또는 실시간 카메라 대신 가상 배경이 적용된 캔버스 스트림을 보내고 싶을 수도 있죠.

이런 경우 기존 WebRTC 연결을 유지하면서 미디어 트랙만 교체해야 합니다. 이 기능은 현대적인 화상회의 앱의 핵심입니다.

Zoom의 화면 공유, OBS의 씬 전환, Snapchat의 필터 적용 등이 모두 이 기술을 사용합니다. 만약 트랙을 교체할 때마다 전체 연결을 재시작한다면 화면이 끊기고, 재연결 시간이 필요하며, 사용자 경험이 크게 저하됩니다.

바로 이럴 때 필요한 것이 RTCRtpSender.replaceTrack() 메서드입니다. 이 메서드 하나로 PeerConnection을 유지한 채로 미디어 트랙만 부드럽게 교체할 수 있습니다.

개요

간단히 말해서, 미디어 트랙 교체는 현재 전송 중인 오디오 또는 비디오 트랙을 다른 트랙으로 바꾸는 기능입니다. RTCRtpSender.replaceTrack()을 사용하여 재협상(renegotiation) 없이 즉시 교체합니다.

왜 이 기능이 필요한지 실무 관점에서 설명하면, 끊김 없는 전환이 가장 중요합니다. 예를 들어, 프레젠테이션 중에 화면 공유로 전환할 때 통화가 끊기면 안 됩니다.

또는 가상 배경을 적용하거나 필터를 추가할 때 원본 카메라 트랙을 가공된 캔버스 트랙으로 교체해야 합니다. 라이브 스트리밍에서 여러 카메라 각도를 전환하거나, 영상과 슬라이드를 번갈아 보여줄 때도 필수적입니다.

전통적인 방법으로는 removeTrack()으로 기존 트랙을 제거하고 addTrack()으로 새 트랙을 추가했습니다. 하지만 이 방법은 SDP 재협상이 필요하고, 몇 초간 화면이 끊기며, 복잡한 시그널링이 필요했습니다.

이제는 replaceTrack() 한 번 호출로 즉시 전환됩니다. 이 개념의 핵심 특징은 첫째, 재협상 불필요입니다.

SDP offer/answer 교환 없이 즉시 트랙을 교체합니다. 둘째, 끊김 없는 전환입니다.

상대방은 잠깐의 끊김도 없이 새 트랙을 받습니다. 셋째, 코덱 호환성입니다.

새 트랙이 기존 코덱과 호환되는 한 문제없이 작동합니다. 이러한 특징들이 중요한 이유는 전문적이고 부드러운 사용자 경험을 제공하고, 복잡한 실시간 미디어 애플리케이션을 간단하게 구현할 수 있기 때문입니다.

코드 예제

// 화면 공유로 전환하는 예제
async function startScreenShare() {
  try {
    // 화면 공유 스트림 획득
    const screenStream = await navigator.mediaDevices.getDisplayMedia({
      video: { cursor: 'always' },
      audio: false
    });

    const screenTrack = screenStream.getVideoTracks()[0];

    // 현재 비디오를 전송하는 sender 찾기
    const sender = peerConnection.getSenders().find(s => s.track?.kind === 'video');

    if (sender) {
      // 재협상 없이 트랙만 교체
      await sender.replaceTrack(screenTrack);
      console.log('화면 공유로 전환 완료');

      // 로컬 프리뷰 업데이트
      document.getElementById('localVideo').srcObject = screenStream;

      // 화면 공유 중단 시 원래 카메라로 복구
      screenTrack.onended = async () => {
        await sender.replaceTrack(originalVideoTrack);
        document.getElementById('localVideo').srcObject = originalStream;
        console.log('카메라로 복구 완료');
      };
    }
  } catch (error) {
    console.error('화면 공유 시작 실패:', error);
  }
}

// 가상 배경 적용 예제 (canvas 트랙으로 교체)
async function applyVirtualBackground() {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');

  // canvas를 MediaStream으로 변환
  const canvasStream = canvas.captureStream(30); // 30 FPS
  const canvasTrack = canvasStream.getVideoTracks()[0];

  // 트랙 교체
  const sender = peerConnection.getSenders().find(s => s.track?.kind === 'video');
  await sender.replaceTrack(canvasTrack);

  console.log('가상 배경 적용 완료');
}

설명

이것이 하는 일: 현재 PeerConnection을 통해 전송 중인 미디어 트랙을 다른 트랙으로 교체합니다. SDP 재협상 없이 즉시 전환되므로 통화가 끊기지 않습니다.

첫 번째로, 교체할 새로운 MediaStreamTrack을 준비합니다. 예제에서는 getDisplayMedia()로 화면 공유 트랙을 얻었지만, 어떤 소스든 가능합니다.

getUserMedia()로 다른 카메라 트랙을, canvas.captureStream()으로 캔버스 트랙을, 또는 video 엘리먼트에서 captureStream()으로 비디오 파일의 트랙을 얻을 수도 있습니다. 그 다음으로, PeerConnection의 getSenders() 메서드로 현재 전송 중인 모든 RTCRtpSender 배열을 가져옵니다.

각 sender는 하나의 미디어 트랙을 담당합니다. 우리는 그 중에서 비디오 트랙을 전송하는 sender를 찾아야 합니다.

find() 메서드로 track.kind가 'video'인 sender를 찾습니다. 세 번째로, sender.replaceTrack(newTrack)을 호출합니다.

이 메서드는 비동기이므로 await로 완료를 기다려야 합니다. 호출하는 순간 WebRTC 엔진이 내부적으로 기존 트랙 전송을 중단하고 새 트랙 전송을 시작합니다.

중요한 점은 SDP 협상이 다시 일어나지 않는다는 것입니다. 기존 PeerConnection의 설정(코덱, 대역폭 등)을 그대로 사용합니다.

네 번째로, 로컬 비디오 프리뷰를 업데이트합니다. 사용자가 자신의 화면에서 변경 사항을 즉시 볼 수 있어야 합니다.

srcObject를 새 스트림으로 설정하면 됩니다. 또한 예제에서는 screenTrack.onended 이벤트 리스너를 추가하여 사용자가 화면 공유를 중단하면 자동으로 원래 카메라로 복구되도록 했습니다.

여러분이 이 코드를 사용하면 Zoom처럼 부드러운 화면 공유 기능을 구현할 수 있습니다. 사용자는 버튼 클릭 한 번에 카메라에서 화면 공유로 전환하고, 다시 카메라로 돌아올 수 있습니다.

통화가 끊기지 않고, 재연결 시간도 없으며, 상대방은 즉시 새 영상을 받습니다. 실무에서는 가상 배경, 필터, 뷰티 모드 등을 구현할 때 원본 카메라 트랙을 canvas로 가공한 후 canvas 트랙으로 교체하는 패턴을 자주 사용합니다.

또한 라이브 스트리밍에서 여러 카메라 각도를 전환하거나, 비디오 플레이어와 카메라를 번갈아 보여줄 때도 이 방법을 사용합니다. 한 가지 주의할 점은 replaceTrack()이 null을 받을 수도 있다는 것입니다.

sender.replaceTrack(null)을 호출하면 아무것도 전송하지 않습니다. 이것은 비디오를 완전히 끄되 sender 자체는 유지하고 싶을 때 유용합니다.

실전 팁

💡 replaceTrack()은 코덱이 호환되는 트랙만 교체할 수 있습니다. 예를 들어 VP8 코덱으로 연결되었다면 새 트랙도 VP8을 지원해야 합니다.

💡 화면 공유 시작 시 screenTrack.onended 리스너를 꼭 추가하세요. 사용자가 브라우저 UI로 공유를 중단하면 자동으로 카메라로 복구됩니다.

💡 canvas.captureStream()으로 가상 배경이나 필터를 적용할 때는 프레임레이트(FPS)를 명시하면 성능을 제어할 수 있습니다.

💡 replaceTrack(null)로 트랙을 제거하면 대역폭을 절약할 수 있지만, sender는 유지되므로 나중에 다시 트랙을 추가할 수 있습니다.

💡 모바일에서 화면 공유는 지원이 제한적이니 getDisplayMedia() 호출 전에 기능 감지를 해야 합니다.


#WebRTC#MediaStream#MediaStreamTrack#getUserMedia#RealTimeControl#WebRTC,미디어제어

댓글 (0)

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

함께 보면 좋은 카드 뉴스

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

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

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

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

Redis 캐싱과 Socket.io 클러스터링 완벽 가이드

실시간 채팅 서비스의 성능을 획기적으로 향상시키는 Redis 캐싱 전략과 Socket.io 클러스터링 방법을 배워봅니다. 다중 서버 환경에서도 안정적으로 작동하는 실시간 애플리케이션을 구축하는 방법을 단계별로 알아봅니다.

반응형 디자인 및 UX 최적화 완벽 가이드

모바일부터 데스크톱까지 완벽하게 대응하는 반응형 웹 디자인과 사용자 경험을 개선하는 실전 기법을 학습합니다. Tailwind CSS를 활용한 빠른 개발부터 다크모드, 무한 스크롤, 스켈레톤 로딩까지 최신 UX 패턴을 실무에 바로 적용할 수 있습니다.

React 채팅 UI 구현 완벽 가이드

실시간 채팅 애플리케이션의 UI를 React로 구현하는 방법을 다룹니다. Socket.io 연동부터 컴포넌트 설계, 상태 관리까지 실무에 바로 적용할 수 있는 내용을 담았습니다.