WebRTC 박살내기 두번째 시리즈 입니다. WebRTC의 MediaStream과 MediaStreamTrack을 깊이 이해하고, getUserMedia부터 트랙 제어, 품질 관리까지 실전 예제와 함께 알아봅니다.
WebRTC 박살내기 마지막 시리즈 입니다. WebRTC의 RTCDataChannel을 이해하고, 채팅·파일 전송·게임 동기화까지 실시간 데이터 전송의 모든 것을 알아봅니다.
WebRTC 박살내기 세번째 시리즈 입니다. WebRTC의 핵심 RTCPeerConnection을 완벽하게 이해하고, 연결 생성부터 이벤트 처리, 품질 관리, 연결 복구까지 실전 예제와 함께 알아봅니다.
WebRTC 박살내기 첫번째 시리즈 입니다. WebRTC 기본 개념부터 시그널링, Offer/Answer(SDP), Trickle ICE, STUN/TURN, NAT, 그리고 Mesh·SFU·MCU 아키텍처까지 한 번에 정리합니다.
안녕하세요! 지난 글에서는 WebRTC의 기본 개념과 연결 구조에 대해 알아보았습니다.
시그널링, SDP, NAT 문제 해결, 그리고 다양한 아키텍처 패턴까지 연결(Connection) 자체를 이해하는 데 집중했는데요.
그렇다면 연결이 된 후, 실제로 주고받는 데이터는 무엇일까요?
바로 미디어 스트림(MediaStream)과 미디어 트랙(MediaStreamTrack)입니다.
처음 WebRTC로 화상 통화를 구현할 때 getUserMedia()를 사용해 카메라를 켜고, 트랙을 다루는 코드를 접해보셨을 겁니다.
하지만 "스트림과 트랙은 무엇이 다를까? 언제 어떤 걸 써야 할까?"라는 의문이 생기기 쉽습니다.
이번 글에서는 MediaStream과 MediaStreamTrack의 개념을 정리하고, 실제로 미디어를 캡처하고 제어하는 방법을 단계별로 살펴보겠습니다.
MediaStream은 택배 상자라고 생각하면 쉽습니다.
MediaStreamTrack은 상자 안에 담긴 개별 물건입니다.
📦 MediaStream (택배 상자)
├── 🎥 비디오 트랙 (카메라 영상)
└── 🎤 오디오 트랙 (마이크 소리)
하나의 상자(스트림)에 여러 물건(트랙)을 담을 수 있고,
필요하면 물건만 꺼내서 다른 상자에 옮길 수도 있습니다.

Media Capture and Streams API(줄여서 MediaStream API)는 WebRTC와 함께 사용되는 핵심 API 입니다.
브라우저에서 카메라, 마이크, 화면을 가져오는 기능을 제공합니다.
💡 스트림(Stream)이란?
물이 흐르듯이 데이터가 연속적으로 전달되는 것입니다.
파일을 한 번에 다운로드하는 게 아니라, 조금씩 계속 받아서 재생하는 방식이죠.
MediaStream은 0개 이상의 트랙을 담는 컨테이너입니다.
📦 빈 상자 (트랙 없음)
📦 오디오만
└── 🎤 마이크📦 비디오만
└── 🎥 카메라📦 둘 다
├── 🎥 카메라
└── 🎤 마이크📦 여러 개
├── 🎥 카메라
├── 🖥️ 화면 공유
└── 🎤 마이크

<video> 요소)const videoElement = document.querySelector("video");
videoElement.srcObject = stream; const recorder = new MediaRecorder(stream);
recorder.start(); // 모든 트랙 확인
const allTracks = stream.getTracks();
console.log(`총 ${allTracks.length}개 트랙`);
// 오디오만
const audioTracks = stream.getAudioTracks();
// 비디오만
const videoTracks = stream.getVideoTracks();
// 특정 ID로
const track = stream.getTrackById("some-id"); // 다른 스트림의 트랙 가져와서 추가
const screenTrack = screenStream.getVideoTracks()[0];
myStream.addTrack(screenTrack);
// 필요 없는 트랙 제거
const videoTrack = myStream.getVideoTracks()[0];
myStream.removeTrack(videoTrack); // 같은 트랙을 참조하지만 다른 ID
const clonedStream = stream.clone();
// 용도: 같은 영상을 여러 곳에 사용
videoElement1.srcObject = stream;
videoElement2.srcObject = clonedStream; // 트랙이 추가되면
stream.addEventListener("addtrack", (event) => {
console.log("새 트랙:", event.track.kind);
// "audio" 또는 "video"
});
// 트랙이 제거되면
stream.addEventListener("removetrack", (event) => {
console.log("트랙 제거됨");
});
// 스트림이 활성화되면
stream.addEventListener("active", () => {
console.log("스트림 사용 가능");
});
// 모든 트랙이 종료되면
stream.addEventListener("inactive", () => {
console.log("스트림 종료됨");
}); 실제 미디어 데이터를 담는 개별 단위입니다.
const track = stream.getVideoTracks()[0];
console.log(track.kind); // "video" 또는 "audio"
console.log(track.label); // "FaceTime HD Camera"
console.log(track.id); // "unique-id"
console.log(track.readyState); // "live" 또는 "ended"
📹 카메라 켜짐
⬇
🟢 트랙 생성 (readyState = "live")
⬇
📡 데이터 전송 중
트랙이 종료되는 경우🛑 트랙 종료 (readyState = "ended")
⬇
📹 카메라 꺼짐
⬇
❌ 다시 켤 수 없음 (새로 만들어야 함)
track.stop() 호출이 둘은 완전히 다릅니다!
// 카메라 끄기 (검은 화면 전송)
videoTrack.enabled = false;
// 카메라 켜기
videoTrack.enabled = true;
// 마이크 음소거
audioTrack.enabled = false; // 읽기만 가능 (변경 불가)
console.log(track.muted); // true 또는 false
// 이벤트로 감지
track.addEventListener("mute", () => {
console.log("트랙이 음소거되었습니다");
});
track.addEventListener("unmute", () => {
console.log("트랙이 다시 활성화되었습니다");
}); 💡 언제 뭘 쓸까?
- enabled: "화면에서 카메라를 꺼줘!" → UI 버튼으로 제어
- muted: "어? 마이크가 안 되네?" → 사용자에게 알림 표시
const videoTrack = stream.getVideoTracks()[0];
// 방법 1: enabled 사용 (임시로 끄기)
videoTrack.enabled = false; // 카메라는 켜져 있지만 검은 화면
videoTrack.enabled = true; // 다시 켜기 가능
// 방법 2: stop 사용 (완전히 종료)
videoTrack.stop(); // 카메라 LED 꺼짐, 다시 켤 수 없음
const originalTrack = stream.getVideoTracks()[0];
const clonedTrack = originalTrack.clone();
// 같은 소스, 다른 ID
console.log(originalTrack.id !== clonedTrack.id); // true
// 둘 다 켜기
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
// 화면에 보여주기
document.querySelector("#myVideo").srcObject = stream; ⚠️ 권한이 필요합니다!
사용자가 "차단"을 누르면 에러가 발생합니다.
브라우저 설정에서 직접 권한을 변경해야 합니다.
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 }, // 가능하면 1280px
height: { ideal: 720 } // 가능하면 720px
}
}); | 키워드 | 의미 | 동작 |
|---|---|---|
ideal | 이상적인 값 | 최대한 맞추되, 안 되면 비슷한 값 |
exact | 정확히 이 값 | 불가능하면 에러 발생 |
min | 최소값 | 이 값 이상 |
max | 최대값 | 이 값 이하 |
// 고화질 화상 회의
video: {
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 30 }
}
// 모바일 데이터 절약
video: {
width: { max: 640 },
height: { max: 480 },
frameRate: { max: 15 }
}
// 전면 카메라 사용 (모바일)
video: {
facingMode: 'user' // 전면 카메라
// facingMode: 'environment' // 후면 카메라
}
// 오디오 품질 개선
audio: {
echoCancellation: true, // 메아리 제거
noiseSuppression: true, // 배경 소음 제거
autoGainControl: true, // 자동 볼륨 조절
sampleRate: { ideal: 48000 } // 고음질
} 💡 화상 통화 필수 설정
echoCancellation과noiseSuppression은 반드시 켜세요!
스피커에서 나오는 소리가 다시 마이크로 들어가는 메아리 현상을 막아줍니다.
// 1. 사용 가능한 장치 목록
const devices = await navigator.mediaDevices.enumerateDevices();
devices.forEach((device) => {
console.log(device.kind); // "videoinput", "audioinput"
console.log(device.label); // "FaceTime HD Camera"
console.log(device.deviceId); // 고유 ID
});
// 2. 특정 장치 선택
const constraints = {
video: {
deviceId: { exact: "specific-camera-id" }
}
};
const stream = await navigator.mediaDevices.getUserMedia(constraints); const screenStream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: true // 시스템 오디오
}); ⚠️ 시스템 오디오는 제한적
Chrome: Windows/Mac 지원
Firefox: Windows만 지원
Safari: 미지원
const screenStream = await navigator.mediaDevices.getDisplayMedia({
video: true
});
const screenTrack = screenStream.getVideoTracks()[0];
// 사용자가 공유 중지 버튼을 누르면
screenTrack.onended = () => {
console.log("화면 공유 종료됨");
// UI 업데이트 또는 카메라로 복귀
}; // 카메라 + 화면 공유
const camera = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
const screen = await navigator.mediaDevices.getDisplayMedia({
video: true
});
// 카메라 오디오 + 화면 비디오
const combined = new MediaStream([...camera.getAudioTracks(), ...screen.getVideoTracks()]); 여러 비디오를 하나로 합치기 (PIP, 격자 레이아웃 등)
const canvas = document.createElement("canvas");
canvas.width = 1280;
canvas.height = 720;
const ctx = canvas.getContext("2d");
// 두 비디오를 나란히 배치
function drawFrame() {
// 왼쪽에 카메라
ctx.drawImage(cameraVideo, 0, 0, 640, 720);
// 오른쪽에 화면 공유
ctx.drawImage(screenVideo, 640, 0, 640, 720);
requestAnimationFrame(drawFrame);
}
drawFrame();
// Canvas를 스트림으로 변환
const compositeStream = canvas.captureStream(30); // 30fps
// 이렇게 하면 카메라가 계속 켜져 있음
const stream = await getUserMedia({ video: true });
// ... 사용 후
// 정리 안 함!
const stream = await getUserMedia({ video: true });
// 사용 완료 후
function cleanup() {
stream.getTracks().forEach((track) => {
track.stop(); // 카메라 LED 꺼짐
});
}
// React라면 이렇게하면 되겠죠?
useEffect(() => {
return () => {
cleanup(); // cleanup 함수
};
}, [stream]); try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true
});
} catch (error) {
if (error instanceof DOMException) {
switch (error.name) {
case "NotAllowedError":
alert("카메라 권한을 허용해주세요");
break;
case "NotFoundError":
alert("카메라를 찾을 수 없습니다");
break;
case "NotReadableError":
alert("카메라가 이미 다른 앱에서 사용 중입니다");
break;
case "OverconstrainedError":
alert("요청한 설정을 지원하지 않습니다");
break;
default:
alert("미디어 장치 접근 중 오류가 발생했습니다");
break;
}
}
} getUserMedia()는 보안상의 이유로 HTTPS에서만 작동합니다.
http://localhost:3000 (예외적으로 허용)
http://127.0.0.1:3000 (예외적으로 허용)
http://192.168.0.10:3000 (안 됨!)
https://your-domain.com (필수!)
// 음소거 상태여야 자동재생 가능
videoElement.muted = true;
await videoElement.play(); // Wake Lock API (실험적)
let wakeLock = null;
async function preventSleep() {
try {
wakeLock = await navigator.wakeLock.request("screen");
} catch (err) {
console.log("Wake Lock 실패:", err);
}
} // 백그라운드에서는 품질 낮추기
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
// 탭이 백그라운드로 갔을 때
// 해상도 낮추기 또는 비디오 끄기
} else {
// 다시 활성화됐을 때
// 원래 품질로 복구
}
}); 이번 글에서는 WebRTC의 MediaStream과 MediaStreamTrack에 대해 알아보았습니다.
id로 식별getTracks(), addTrack(), removeTrack() 제공kind: "audio" 또는 "video"readyState: "live" 또는 "ended"enabled: 출력 제어 (내가 조절)muted: 시스템 상태 (읽기 전용)getUserMedia(): 카메라/마이크getDisplayMedia(): 화면 공유track.stop())다음 글에서는 RTCPeerConnection의 이벤트 흐름을 다루겠습니다.
addTrack)replaceTrack, removeTrack)끝까지 읽어주셔서 감사합니다!