이미지 로딩 중...
AI Generated
2025. 11. 20. · 3 Views
1:1 화상 통화 구현 완벽 가이드
WebRTC를 활용한 1:1 화상 통화 시스템을 처음부터 끝까지 구현하는 방법을 배워봅니다. 통화 시작부터 종료까지, 음소거와 카메라 제어, 연결 품질 모니터링까지 실무에 바로 적용할 수 있는 모든 과정을 다룹니다.
목차
1. 통화_시작_UI
시작하며
여러분이 화상 통화 앱을 만들 때 가장 먼저 마주치는 고민이 무엇일까요? 바로 "사용자가 어떻게 통화를 시작하고, 어떻게 상대방의 화면을 볼 수 있을까?"입니다.
화상 통화는 단순히 버튼 하나만으로는 작동하지 않습니다. 실제로 화상 통화를 구현하려면 사용자의 카메라와 마이크에 접근해야 하고, 이를 화면에 보여줘야 하며, 상대방과 연결할 준비를 해야 합니다.
많은 초보 개발자들이 이 첫 단계에서 막히곤 합니다. 바로 이럴 때 필요한 것이 통화 시작 UI입니다.
사용자가 자신의 카메라 화면을 미리 보고, 통화 버튼을 누를 수 있는 직관적인 인터페이스를 만들어야 합니다. 이것이 성공적인 화상 통화의 첫걸음입니다.
개요
간단히 말해서, 통화 시작 UI는 사용자가 화상 통화를 시작하기 위한 모든 준비를 하는 화면입니다. 여기서는 카메라와 마이크를 켜고, 자신의 모습을 확인하며, 통화를 시작할 수 있습니다.
왜 이것이 필요할까요? 사용자는 통화를 시작하기 전에 자신의 모습을 확인하고 싶어합니다.
머리가 괜찮은지, 조명이 적절한지, 배경이 깔끔한지 미리 체크하고 싶은 것이죠. 예를 들어, 중요한 화상 면접이나 비즈니스 미팅 전에는 이런 확인 과정이 매우 중요합니다.
기존에는 복잡한 플러그인이나 네이티브 앱을 설치해야 했다면, 이제는 웹 브라우저만으로도 getUserMedia API를 통해 쉽게 카메라에 접근할 수 있습니다. 이 UI의 핵심 특징은 세 가지입니다: 첫째, 로컬 비디오 미리보기 기능, 둘째, 카메라/마이크 권한 요청 처리, 셋째, 직관적인 통화 시작 버튼입니다.
이러한 특징들이 사용자에게 안정감을 주고, 통화 시작의 심리적 장벽을 낮춰줍니다.
코드 예제
// HTML 구조를 먼저 설정합니다
const videoContainer = document.getElementById('video-container');
const localVideo = document.getElementById('local-video');
const startCallButton = document.getElementById('start-call');
const statusText = document.getElementById('status');
// 로컬 비디오 스트림을 가져오는 함수
async function setupLocalVideo() {
try {
// 카메라와 마이크 권한을 요청합니다
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 1280, height: 720 },
audio: true
});
// 가져온 스트림을 비디오 엘리먼트에 연결합니다
localVideo.srcObject = stream;
localVideo.muted = true; // 자신의 소리는 들리지 않게 합니다
statusText.textContent = '카메라 준비 완료! 통화를 시작하세요.';
startCallButton.disabled = false;
return stream;
} catch (error) {
statusText.textContent = '카메라 접근 실패: ' + error.message;
console.error('미디어 접근 오류:', error);
}
}
// 페이지 로드 시 자동으로 카메라를 켭니다
setupLocalVideo();
설명
이것이 하는 일: 이 코드는 웹 브라우저를 통해 사용자의 카메라와 마이크에 접근하고, 가져온 비디오 스트림을 화면에 보여주는 역할을 합니다. 마치 거울을 보듯이 자신의 모습을 실시간으로 확인할 수 있게 해줍니다.
첫 번째로, navigator.mediaDevices.getUserMedia() 메서드가 실행됩니다. 이것은 브라우저에게 "카메라와 마이크를 사용하고 싶어요"라고 요청하는 것입니다.
이때 사용자에게 권한 요청 팝업이 뜨고, 사용자가 허용하면 비디오와 오디오 스트림을 받아옵니다. 왜 이렇게 하냐고요?
보안과 프라이버시를 위해서입니다. 아무나 마음대로 카메라를 켤 수 없도록 말이죠.
그 다음으로, 받아온 stream 객체를 localVideo.srcObject에 할당합니다. 이것은 마치 TV에 안테나를 연결하는 것과 같습니다.
비디오 엘리먼트에 카메라 신호를 연결해주는 거죠. 그리고 muted = true로 설정하여 자신의 목소리가 스피커로 나오지 않게 합니다.
만약 이걸 안 하면 하울링(삐- 하는 소리)이 발생할 수 있습니다. 마지막으로, 상태 메시지를 업데이트하고 통화 시작 버튼을 활성화합니다.
사용자에게 "준비됐어요!"라고 알려주는 것입니다. 에러가 발생하면 catch 블록에서 친절한 에러 메시지를 보여줍니다.
여러분이 이 코드를 사용하면 사용자 친화적인 화상 통화 시작 경험을 제공할 수 있습니다. 사용자는 자신의 모습을 미리 확인하고 안심한 상태로 통화를 시작할 수 있으며, 카메라가 제대로 작동하는지 사전에 테스트할 수 있습니다.
또한 권한 거부나 기기 오류 같은 문제를 통화 시작 전에 미리 발견하고 해결할 수 있습니다.
실전 팁
💡 카메라 해상도는 너무 높게 설정하지 마세요. 1280x720이면 충분하며, 모바일에서는 640x480도 괜찮습니다. 너무 높은 해상도는 네트워크 대역폭을 많이 소모합니다.
💡 getUserMedia는 HTTPS 환경에서만 작동합니다. 로컬 개발 시에는 localhost는 예외적으로 허용되지만, 배포 시에는 반드시 SSL 인증서를 설정하세요.
💡 사용자가 권한을 거부했을 때를 대비한 UI를 꼭 준비하세요. "카메라 권한이 필요합니다. 브라우저 설정에서 허용해주세요"와 같은 안내 메시지와 함께 설정 가이드를 제공하면 좋습니다.
💡 모바일 기기에서는 전면/후면 카메라를 선택할 수 있는 옵션을 제공하세요. facingMode: "user" (전면) 또는 "environment" (후면)을 설정할 수 있습니다.
💡 페이지를 떠날 때는 반드시 스트림을 정리하세요. stream.getTracks().forEach(track => track.stop())을 호출하여 카메라 LED가 계속 켜져있는 문제를 방지할 수 있습니다.
2. 발신_수신_로직
시작하며
여러분이 전화기로 친구에게 전화를 걸 때를 생각해보세요. 여러분이 번호를 누르면, 친구의 전화기가 울리고, 친구가 받으면 연결됩니다.
화상 통화도 똑같은 원리입니다. 하지만 웹에서는 이 과정이 어떻게 작동할까요?
실제 개발 현장에서는 "시그널링(Signaling)"이라는 개념 때문에 많은 개발자들이 혼란스러워합니다. WebRTC는 P2P(Peer-to-Peer) 연결을 만들지만, 처음 연결을 시작하려면 서버를 통해 상대방에게 "나 통화하고 싶어!"라는 신호를 보내야 합니다.
바로 이럴 때 필요한 것이 발신/수신 로직입니다. 한쪽은 "offer"를 보내고, 다른 쪽은 "answer"를 보내는 이 프로세스를 이해하면 화상 통화의 핵심을 마스터한 것입니다.
개요
간단히 말해서, 발신/수신 로직은 두 브라우저가 서로를 찾아 연결하는 과정입니다. WebRTC의 RTCPeerConnection을 사용하여 한쪽은 연결 제안(offer)을, 다른 쪽은 수락(answer)을 교환합니다.
왜 이 과정이 필요할까요? 인터넷에서 두 컴퓨터가 직접 연결되려면 서로의 IP 주소와 포트, 그리고 네트워크 정보를 알아야 합니다.
하지만 대부분의 사용자는 공유기(NAT) 뒤에 있어서 직접 연결이 어렵습니다. 예를 들어, 집에서 와이파이를 사용하는 경우, 여러분의 실제 인터넷 주소는 공유기가 숨기고 있습니다.
기존에는 복잡한 NAT 통과 기술을 직접 구현해야 했다면, 이제는 WebRTC가 이 모든 것을 자동으로 처리해줍니다. 우리는 단지 offer와 answer를 교환하기만 하면 됩니다.
이 로직의 핵심 특징은: 첫째, SDP(Session Description Protocol)를 통한 미디어 정보 교환, 둘째, ICE Candidate를 통한 네트워크 경로 찾기, 셋째, WebSocket이나 Socket.io 같은 시그널링 서버를 통한 메시지 전달입니다. 이러한 특징들이 안정적인 P2P 연결을 가능하게 합니다.
코드 예제
// WebSocket으로 시그널링 서버에 연결합니다
const signalingServer = new WebSocket('wss://your-signaling-server.com');
const peerConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
// 발신: Offer를 생성하고 보내는 함수
async function makeCall(roomId) {
// 로컬 스트림을 PeerConnection에 추가합니다
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream);
});
// Offer SDP를 생성합니다
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
// 시그널링 서버를 통해 상대방에게 Offer를 전송합니다
signalingServer.send(JSON.stringify({
type: 'offer',
offer: offer,
roomId: roomId
}));
}
// 수신: Offer를 받고 Answer로 응답하는 함수
async function handleOffer(offer) {
// 받은 Offer를 원격 설명으로 설정합니다
await peerConnection.setRemoteDescription(offer);
// Answer SDP를 생성합니다
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
// Answer를 발신자에게 보냅니다
signalingServer.send(JSON.stringify({
type: 'answer',
answer: answer
}));
}
// ICE Candidate 이벤트 처리 - 네트워크 경로 정보를 교환합니다
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
signalingServer.send(JSON.stringify({
type: 'ice-candidate',
candidate: event.candidate
}));
}
};
설명
이것이 하는 일: 이 코드는 두 브라우저가 서로를 찾아 직접 연결하는 과정을 관리합니다. 마치 두 사람이 중간에 전령(시그널링 서버)을 통해 만날 장소와 시간을 약속하는 것과 같습니다.
첫 번째로, RTCPeerConnection 객체를 생성합니다. 이것은 실제 비디오와 오디오 데이터가 오가는 통로입니다.
iceServers는 Google의 공개 STUN 서버를 사용하여 여러분의 공인 IP 주소를 찾아줍니다. 왜 이게 필요하냐고요?
집 안에서 친구에게 "우리 집으로 와"라고 하면, 친구는 정확한 주소를 알아야 하잖아요. STUN 서버가 바로 그 주소를 찾아주는 역할입니다.
그 다음으로, 발신자는 createOffer()를 호출합니다. 이것은 "나는 이런 비디오 코덱을 지원하고, 이런 오디오 포맷을 사용할 수 있어"라는 정보를 담은 SDP 문서를 만듭니다.
그리고 setLocalDescription()으로 자신에게 이 설정을 적용하고, 시그널링 서버를 통해 상대방에게 전송합니다. 이것은 마치 "나는 한국어와 영어로 대화할 수 있어"라고 알려주는 것과 같습니다.
수신자는 setRemoteDescription()으로 받은 Offer를 적용하고, createAnswer()로 "좋아, 나도 한국어 할 수 있으니 한국어로 하자"라는 응답을 만듭니다. 이렇게 서로의 능력을 확인하고 합의하는 과정입니다.
마지막으로, onicecandidate 이벤트가 발생할 때마다 네트워크 경로 정보를 교환합니다. 이것은 "나한테 오려면 이 길로 와", "아니면 저 길도 있어"라고 여러 경로를 알려주는 것입니다.
WebRTC는 이 중에서 가장 빠른 경로를 자동으로 선택합니다. 여러분이 이 코드를 사용하면 방화벽이나 공유기 뒤에 있는 사용자들끼리도 안정적으로 연결할 수 있습니다.
네트워크 환경에 따라 최적의 경로를 자동으로 찾아주며, 연결이 끊어지면 재연결도 자동으로 시도합니다. 또한 여러 네트워크 인터페이스(와이파이, 유선, 모바일 데이터)가 있을 때 가장 좋은 것을 선택해줍니다.
실전 팁
💡 STUN 서버만으로 연결이 안 되는 경우를 대비해 TURN 서버도 설정하세요. 약 10-15%의 사용자는 엄격한 방화벽 때문에 TURN이 필요합니다. Coturn이나 Twilio의 TURN 서비스를 활용할 수 있습니다.
💡 offer와 answer는 순서가 매우 중요합니다. 반드시 offer를 보낸 쪽이 먼저 setLocalDescription을 하고, 받는 쪽이 setRemoteDescription을 해야 합니다. 순서가 바뀌면 연결이 실패합니다.
💡 ICE Candidate는 여러 개가 발생합니다. 모든 candidate를 상대방에게 전송해야 최적의 연결 경로를 찾을 수 있습니다. "trickle ICE"라고 하는 이 방식이 연결 속도를 크게 향상시킵니다.
💡 시그널링 서버는 연결이 수립된 후에는 필요 없습니다. Offer/Answer와 ICE Candidate 교환이 완료되면, 실제 미디어 데이터는 P2P로 직접 전송됩니다. 서버 부하를 크게 줄일 수 있습니다.
💡 연결 상태를 모니터링하세요. peerConnection.onconnectionstatechange 이벤트로 'connected', 'disconnected', 'failed' 상태를 추적하여 사용자에게 알려주거나 재연결을 시도할 수 있습니다.
3. 로컬_리모트_비디오_표시
시작하며
여러분이 화상 통화를 할 때, 화면에는 두 개의 영상이 보입니다. 하나는 내 모습(로컬 비디오), 다른 하나는 상대방 모습(리모트 비디오)입니다.
간단해 보이지만, 이 두 영상을 제대로 배치하고 관리하는 것이 생각보다 까다롭습니다. 실제로 개발하다 보면 이런 문제가 발생합니다: 상대방 영상이 안 나온다, 내 영상이 거울처럼 좌우반전되어야 하는데 안 된다, 화면 크기가 이상하게 나온다, 모바일에서 세로/가로 전환 시 비율이 깨진다 등등.
이런 문제들은 사용자 경험을 크게 해칩니다. 바로 이럴 때 필요한 것이 올바른 비디오 표시 로직입니다.
각 비디오 스트림을 적절한 HTML 요소에 연결하고, CSS로 보기 좋게 배치하며, 다양한 화면 크기에 대응하는 방법을 알아야 합니다.
개요
간단히 말해서, 로컬/리모트 비디오 표시는 자신의 카메라 영상과 상대방의 영상을 화면에 동시에 보여주는 기능입니다. 로컬 비디오는 이미 getUserMedia로 받았고, 리모트 비디오는 PeerConnection을 통해 받아옵니다.
왜 이것이 중요할까요? 화상 통화에서 가장 중요한 것은 "상대방을 보는 것"입니다.
아무리 연결이 잘 되어도 화면에 영상이 안 나오면 의미가 없죠. 예를 들어, 화상 회의 중에 발표자의 얼굴이 안 보이면 회의 진행이 불가능합니다.
또한 자신의 모습을 작은 화면(PIP, Picture-in-Picture)으로 보면서 표정이나 배경을 확인할 수 있어야 합니다. 기존에는 복잡한 플러그인이나 플래시를 사용해야 했다면, 이제는 HTML5의 <video> 태그만으로 깔끔하게 구현할 수 있습니다.
이 기능의 핵심 특징은: 첫째, 로컬 비디오의 좌우반전(mirror 효과)으로 자연스러운 느낌 제공, 둘째, 리모트 비디오의 자동 재생(autoplay) 설정, 셋째, 반응형 레이아웃으로 다양한 화면 크기 대응입니다. 이러한 특징들이 전문적인 화상 통화 UI를 만들어줍니다.
코드 예제
// 로컬 비디오 엘리먼트 (내 모습)
const localVideo = document.getElementById('local-video');
// 리모트 비디오 엘리먼트 (상대방 모습)
const remoteVideo = document.getElementById('remote-video');
// 로컬 스트림을 로컬 비디오에 연결
async function displayLocalVideo() {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
localVideo.srcObject = stream;
localVideo.muted = true; // 에코 방지
// CSS로 좌우반전 효과 (거울 모드)
localVideo.style.transform = 'scaleX(-1)';
return stream;
}
// PeerConnection에서 리모트 스트림 받기
peerConnection.ontrack = (event) => {
// 상대방의 비디오/오디오 트랙이 도착했을 때 실행됩니다
console.log('리모트 트랙 수신:', event.track.kind);
if (!remoteVideo.srcObject) {
// 처음 받은 스트림을 리모트 비디오에 연결
remoteVideo.srcObject = event.streams[0];
}
};
// 비디오 재생 오류 처리
remoteVideo.onloadedmetadata = () => {
// 비디오 메타데이터 로드 완료 시 자동 재생
remoteVideo.play().catch(error => {
console.error('자동 재생 실패:', error);
// 사용자 상호작용 필요 시 버튼 표시
showPlayButton();
});
};
// 화면 크기 조정 함수
function adjustVideoLayout() {
const container = document.getElementById('video-container');
const isPortrait = window.innerHeight > window.innerWidth;
if (isPortrait) {
// 세로 모드: 위아래 배치
container.style.flexDirection = 'column';
} else {
// 가로 모드: 좌우 배치
container.style.flexDirection = 'row';
}
}
// 화면 회전 감지
window.addEventListener('resize', adjustVideoLayout);
설명
이것이 하는 일: 이 코드는 두 개의 비디오 스트림을 화면에 표시하고, 다양한 상황에 맞게 레이아웃을 조정합니다. 마치 액자에 사진을 넣듯이, 비디오 데이터를 HTML 요소에 담아 보여주는 것입니다.
첫 번째로, displayLocalVideo() 함수가 자신의 카메라 영상을 가져와 localVideo 엘리먼트에 연결합니다. 여기서 중요한 점은 muted = true 설정입니다.
만약 이걸 안 하면, 내 목소리가 스피커로 나오고 다시 마이크로 들어가면서 엄청난 에코(메아리)가 발생합니다. 그리고 transform: scaleX(-1)로 좌우반전을 해주는데, 이는 거울을 보는 것처럼 자연스러운 느낌을 주기 위함입니다.
실제로 사람들은 거울에 비친 자신의 모습에 익숙하기 때문입니다. 그 다음으로, ontrack 이벤트 핸들러가 상대방의 비디오/오디오 트랙을 받습니다.
이 이벤트는 트랙마다 한 번씩 발생하므로, 비디오 트랙과 오디오 트랙으로 총 두 번 호출됩니다. event.streams[0]은 이 트랙들을 묶은 MediaStream 객체인데, 이것을 remoteVideo.srcObject에 할당하면 자동으로 비디오와 오디오가 모두 재생됩니다.
세 번째로, onloadedmetadata 이벤트에서 자동 재생을 시도합니다. 하지만 최근 브라우저들은 자동 재생을 차단하는 정책이 있어서, 사용자가 페이지와 상호작용(클릭, 터치 등)하기 전에는 재생이 안 될 수 있습니다.
이런 경우를 대비해 catch 블록에서 "재생 버튼"을 표시하여 사용자가 직접 재생할 수 있게 합니다. 마지막으로, adjustVideoLayout() 함수가 화면 방향에 따라 레이아웃을 변경합니다.
모바일에서 세로로 들고 있으면 위아래로, 가로로 돌리면 좌우로 배치하는 식입니다. resize 이벤트를 감지하여 실시간으로 조정됩니다.
여러분이 이 코드를 사용하면 전문적인 화상 통화 앱과 같은 수준의 비디오 표시를 구현할 수 있습니다. 사용자는 자신의 모습을 자연스럽게 확인하면서 상대방과 대화할 수 있으며, 어떤 기기와 화면 크기에서든 최적의 레이아웃으로 볼 수 있습니다.
또한 브라우저 정책에도 유연하게 대응하여 재생 오류를 최소화할 수 있습니다.
실전 팁
💡 리모트 비디오는 좌우반전하지 마세요. 로컬 비디오만 반전해야 합니다. 상대방 영상까지 반전하면 상대방이 이상하게 보입니다. 예를 들어, 상대방이 오른손을 들면 여러분 화면에서도 오른쪽에 보여야 자연스럽습니다.
💡 비디오 엘리먼트에 playsinline 속성을 추가하세요. 특히 iOS Safari에서는 이 속성이 없으면 비디오가 전체화면으로 재생되어 레이아웃이 깨집니다.
💡 object-fit CSS 속성을 활용하세요. object-fit: cover는 화면을 꽉 채우지만 잘릴 수 있고, object-fit: contain은 전체가 보이지만 여백이 생깁니다. 용도에 맞게 선택하세요.
💡 리모트 비디오가 안 나올 때를 대비한 placeholder 이미지나 로딩 애니메이션을 준비하세요. "상대방 연결 중..."과 같은 메시지로 사용자의 불안을 줄일 수 있습니다.
💡 Picture-in-Picture API를 활용하면 다른 탭을 보면서도 화상 통화를 계속할 수 있습니다. videoElement.requestPictureInPicture()로 쉽게 구현 가능합니다.
4. 통화_종료_처리
시작하며
여러분이 전화 통화를 끝낼 때 어떻게 하나요? 빨간 버튼을 누르면 깔끔하게 끊기고, 통화 시간이 표시되며, 다시 대기 화면으로 돌아갑니다.
화상 통화도 마찬가지로 깔끔한 종료 처리가 필요합니다. 실제 개발 현장에서는 통화 종료가 생각보다 복잡합니다.
카메라 LED가 계속 켜져 있거나, 메모리 누수가 발생하거나, 다음 통화를 시작할 수 없는 문제가 자주 발생합니다. 심지어 사용자가 브라우저 탭을 닫았을 때도 제대로 정리해야 합니다.
바로 이럴 때 필요한 것이 올바른 통화 종료 처리입니다. 모든 리소스를 해제하고, 연결을 끊고, UI를 초기화하는 과정을 빠짐없이 수행해야 합니다.
이것이 안정적인 화상 통화 앱의 필수 조건입니다.
개요
간단히 말해서, 통화 종료 처리는 화상 통화에 사용된 모든 리소스를 정리하고 초기 상태로 되돌리는 과정입니다. PeerConnection을 닫고, 미디어 스트림을 중지하고, 이벤트 리스너를 제거해야 합니다.
왜 이것이 중요할까요? 웹 브라우저의 리소스는 유한합니다.
카메라와 마이크는 한 번에 하나의 앱만 사용할 수 있고, 메모리도 계속 쌓이면 브라우저가 느려지거나 다운됩니다. 예를 들어, 하루 종일 여러 번 화상 회의를 하는 직장인의 경우, 매번 제대로 정리하지 않으면 오후쯤에는 브라우저가 버벅거리기 시작합니다.
기존에는 개발자가 일일이 모든 리소스를 추적하고 해제해야 했다면, 이제는 체계적인 cleanup 패턴을 사용하여 안전하게 관리할 수 있습니다. 이 처리의 핵심 특징은: 첫째, 모든 미디어 트랙의 중지로 카메라/마이크 LED 끄기, 둘째, PeerConnection의 완전한 종료로 네트워크 리소스 해제, 셋째, UI 상태 초기화로 다음 통화 준비입니다.
이러한 특징들이 매끄러운 사용자 경험을 만들어줍니다.
코드 예제
// 통화 종료 버튼 이벤트
const hangupButton = document.getElementById('hangup-btn');
hangupButton.addEventListener('click', endCall);
async function endCall() {
console.log('통화 종료 시작...');
// 1. 로컬 스트림의 모든 트랙 중지 (카메라/마이크 끄기)
if (localStream) {
localStream.getTracks().forEach(track => {
track.stop(); // LED가 꺼집니다
console.log(`${track.kind} 트랙 중지됨`);
});
}
// 2. 비디오 엘리먼트에서 스트림 제거
if (localVideo.srcObject) {
localVideo.srcObject = null;
}
if (remoteVideo.srcObject) {
remoteVideo.srcObject = null;
}
// 3. PeerConnection 종료
if (peerConnection) {
// 이벤트 리스너 제거 (메모리 누수 방지)
peerConnection.onicecandidate = null;
peerConnection.ontrack = null;
peerConnection.onconnectionstatechange = null;
// 연결 닫기
peerConnection.close();
peerConnection = null;
}
// 4. 시그널링 서버에 종료 알림
if (signalingServer && signalingServer.readyState === WebSocket.OPEN) {
signalingServer.send(JSON.stringify({
type: 'hangup',
roomId: currentRoomId
}));
}
// 5. UI 상태 초기화
document.getElementById('call-controls').style.display = 'none';
document.getElementById('start-call-section').style.display = 'block';
document.getElementById('call-duration').textContent = '00:00';
// 6. 상태 메시지 업데이트
showNotification('통화가 종료되었습니다');
console.log('통화 종료 완료');
}
// 페이지를 떠날 때도 정리
window.addEventListener('beforeunload', () => {
endCall();
});
// 상대방이 먼저 종료했을 때 처리
signalingServer.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'hangup') {
showNotification('상대방이 통화를 종료했습니다');
endCall();
}
};
설명
이것이 하는 일: 이 코드는 화상 통화에 사용된 모든 것을 깔끔하게 정리하고 원래 상태로 되돌립니다. 마치 호텔 방을 체크아웃할 때 모든 짐을 챙기고 열쇠를 반납하는 것처럼, 빌려 쓴 리소스를 모두 반환합니다.
첫 번째로, localStream.getTracks().forEach(track => track.stop())이 실행됩니다. 이것은 카메라와 마이크를 완전히 끄는 명령입니다.
track.stop()을 호출하면 웹캠의 LED가 꺼지고, 운영체제에게 "이제 카메라 안 쓸게요"라고 알려줍니다. 왜 중요하냐고요?
이걸 안 하면 사용자가 통화를 끝냈는데도 카메라 불빛이 계속 켜져 있어서 불안해합니다. 프라이버시 관점에서 매우 중요한 부분입니다.
그 다음으로, 비디오 엘리먼트의 srcObject를 null로 설정합니다. 이것은 비디오 태그와 스트림의 연결을 끊는 것입니다.
그냥 화면만 검게 만드는 게 아니라, 브라우저의 메모리에서 스트림 객체를 해제할 수 있게 해줍니다. 가비지 컬렉션(쓰레기 수거)을 가능하게 하는 것이죠.
세 번째로, PeerConnection의 모든 이벤트 리스너를 null로 설정하고 close()를 호출합니다. 이벤트 리스너를 제거하지 않으면 "메모리 누수"가 발생합니다.
함수가 계속 메모리에 남아있어서, 통화를 10번, 20번 하다 보면 브라우저가 느려지기 시작합니다. close()는 실제 네트워크 연결을 끊고, STUN/TURN 서버와의 통신도 모두 종료합니다.
네 번째로, 시그널링 서버에 'hangup' 메시지를 보냅니다. 이것은 상대방에게 "나 통화 끊었어"라고 알려주는 것입니다.
상대방도 이 메시지를 받으면 자동으로 endCall()을 실행하여 함께 정리합니다. 또한 beforeunload 이벤트로 사용자가 브라우저 탭을 닫거나 새로고침할 때도 자동으로 정리됩니다.
여러분이 이 코드를 사용하면 사용자가 여러 번 통화를 반복해도 브라우저가 느려지거나 멈추지 않습니다. 카메라와 마이크가 제대로 해제되어 다른 앱에서도 사용할 수 있으며, 메모리 누수 없이 장시간 안정적으로 작동합니다.
또한 예기치 않은 종료 상황(탭 닫기, 새로고침 등)에서도 안전하게 리소스를 정리할 수 있습니다.
실전 팁
💡 track.stop()과 peerConnection.close()의 순서가 중요합니다. 먼저 트랙을 멈추고, 그 다음 연결을 닫아야 합니다. 순서를 바꾸면 일부 브라우저에서 제대로 정리되지 않을 수 있습니다.
💡 signalingServer.send() 전에 readyState를 확인하세요. 이미 연결이 끊어진 상태에서 send()를 호출하면 에러가 발생합니다. WebSocket.OPEN 상태일 때만 보내야 합니다.
💡 endCall 함수는 여러 곳에서 호출될 수 있으므로 멱등성(idempotent)을 보장하세요. 즉, 여러 번 호출해도 안전해야 합니다. null 체크를 철저히 하고, 이미 null이면 스킵하도록 만드세요.
💡 통화 시간을 기록하고 싶다면, setInterval로 타이머를 돌렸을 경우 endCall에서 clearInterval을 꼭 해야 합니다. 안 그러면 백그라운드에서 타이머가 계속 돌아갑니다.
💡 개발자 도구의 콘솔을 열어두고 통화 종료 시 에러가 없는지 확인하세요. "Cannot read property of null" 같은 에러가 있다면 정리 순서를 조정해야 합니다.
5. 음소거_카메라_끄기
시작하며
여러분이 화상 회의 중에 갑자기 택배가 왔거나, 애완동물이 소리를 낸다면 어떻게 하나요? 당황하지 말고 음소거 버튼을 누르면 됩니다.
또는 잠시 자리를 비울 때 카메라를 끄고 싶을 수도 있습니다. 이런 기능은 화상 통화의 필수 요소입니다.
실제 개발에서는 단순히 트랙을 멈추는 것이 아니라, "일시적으로 비활성화"하는 것이 중요합니다. 트랙을 완전히 중지(stop)하면 다시 시작할 수 없지만, 비활성화(enabled = false)하면 언제든 다시 켤 수 있습니다.
많은 초보 개발자들이 이 차이를 몰라서 헤맵니다. 바로 이럴 때 필요한 것이 음소거와 카메라 끄기 기능입니다.
사용자가 자신의 오디오와 비디오를 실시간으로 제어할 수 있게 해주며, 프라이버시와 편의성을 모두 보장합니다.
개요
간단히 말해서, 음소거와 카메라 끄기는 미디어 트랙의 enabled 속성을 토글하여 일시적으로 전송을 멈추는 기능입니다. 연결은 유지되지만 데이터 전송만 중단됩니다.
왜 이것이 필요할까요? 화상 회의 중에는 예상치 못한 상황이 많이 발생합니다.
아이가 울거나, 공사 소음이 들리거나, 잠시 문서를 찾아야 하거나, 다른 사람과 잠깐 대화해야 할 때가 있습니다. 예를 들어, 재택근무 중에 배우자가 말을 걸면 빠르게 음소거하고 대답한 후 다시 켜야 합니다.
매번 통화를 끊고 다시 연결하는 것은 비효율적이죠. 기존에는 트랙을 중지했다가 새로 getUserMedia를 호출해야 했다면, 이제는 enabled 속성 하나로 간단하게 제어할 수 있습니다.
이 기능의 핵심 특징은: 첫째, enabled 속성으로 즉각적인 토글, 둘째, 상대방에게 음소거/비디오 꺼짐 상태 알림, 셋째, UI 버튼의 시각적 피드백(아이콘 변경, 색상 변경)입니다. 이러한 특징들이 직관적이고 사용하기 쉬운 인터페이스를 만들어줍니다.
코드 예제
// 음소거/카메라 버튼 요소
const muteButton = document.getElementById('mute-btn');
const videoToggleButton = document.getElementById('video-toggle-btn');
let isMuted = false;
let isVideoOff = false;
// 음소거 토글 함수
function toggleMute() {
if (!localStream) return;
// 오디오 트랙 찾기
const audioTrack = localStream.getAudioTracks()[0];
if (audioTrack) {
// enabled를 반대로 설정 (true ↔ false)
audioTrack.enabled = !audioTrack.enabled;
isMuted = !audioTrack.enabled;
// UI 업데이트
muteButton.textContent = isMuted ? '🔇 음소거 중' : '🔊 음소거';
muteButton.classList.toggle('active', isMuted);
// 상대방에게 상태 알림
sendSignal({
type: 'audio-status',
muted: isMuted
});
console.log(`오디오 ${isMuted ? '끔' : '켬'}`);
}
}
// 비디오 토글 함수
function toggleVideo() {
if (!localStream) return;
// 비디오 트랙 찾기
const videoTrack = localStream.getVideoTracks()[0];
if (videoTrack) {
// enabled를 반대로 설정
videoTrack.enabled = !videoTrack.enabled;
isVideoOff = !videoTrack.enabled;
// UI 업데이트
videoToggleButton.textContent = isVideoOff ? '📹 비디오 끔' : '📹 비디오 켬';
videoToggleButton.classList.toggle('active', isVideoOff);
// 로컬 비디오 화면 처리 (검은 화면 또는 아바타 표시)
localVideo.style.display = isVideoOff ? 'none' : 'block';
document.getElementById('video-placeholder').style.display = isVideoOff ? 'block' : 'none';
// 상대방에게 상태 알림
sendSignal({
type: 'video-status',
videoOff: isVideoOff
});
console.log(`비디오 ${isVideoOff ? '끔' : '켬'}`);
}
}
// 버튼 이벤트 연결
muteButton.addEventListener('click', toggleMute);
videoToggleButton.addEventListener('click', toggleVideo);
// 키보드 단축키 지원 (Ctrl+M: 음소거, Ctrl+E: 비디오)
document.addEventListener('keydown', (event) => {
if (event.ctrlKey && event.key === 'm') {
event.preventDefault();
toggleMute();
}
if (event.ctrlKey && event.key === 'e') {
event.preventDefault();
toggleVideo();
}
});
// 상대방의 상태 변화 수신
function handleRemoteStatus(data) {
if (data.type === 'audio-status') {
showNotification(data.muted ? '상대방이 음소거했습니다' : '상대방이 음소거를 해제했습니다');
}
if (data.type === 'video-status') {
// 상대방 비디오 placeholder 표시/숨김
const remotePlaceholder = document.getElementById('remote-video-placeholder');
remotePlaceholder.style.display = data.videoOff ? 'block' : 'none';
}
}
설명
이것이 하는 일: 이 코드는 사용자가 자신의 마이크와 카메라를 언제든지 켜고 끌 수 있게 해줍니다. 마치 TV 리모컨의 음소거 버튼처럼, 누르면 바로 반응하고 다시 누르면 원래대로 돌아옵니다.
첫 번째로, getAudioTracks()[0] 또는 getVideoTracks()[0]로 제어할 트랙을 찾습니다. 한 스트림에는 보통 오디오 트랙 하나, 비디오 트랙 하나가 들어있습니다.
[0]은 첫 번째(그리고 유일한) 트랙을 가져오는 것입니다. 왜 이렇게 하냐고요?
스트림은 여러 트랙을 담을 수 있는 "컨테이너"이고, 우리는 그 중에서 필요한 것만 제어하기 때문입니다. 그 다음으로, audioTrack.enabled = !audioTrack.enabled 같은 코드가 실행됩니다.
이것은 "현재 상태의 반대로 설정하라"는 의미입니다. 켜져 있으면 끄고, 꺼져 있으면 켭니다.
여기서 핵심은 track.stop()이 아니라 enabled = false를 사용한다는 점입니다. stop()은 완전히 끄는 것이라 다시 켤 수 없지만, enabled는 "일시정지" 같은 개념이라 언제든 다시 true로 바꿀 수 있습니다.
세 번째로, UI를 업데이트합니다. 버튼의 텍스트와 아이콘을 바꿔서 사용자에게 "지금 음소거 중이야" 또는 "지금 카메라 꺼져 있어"라고 알려줍니다.
classList.toggle('active')는 CSS 클래스를 추가/제거하여 버튼 색상을 빨간색으로 바꾸는 등의 시각적 피드백을 줍니다. 이것이 중요한 이유는, 사용자가 화면을 힐끗 보기만 해도 현재 상태를 알 수 있어야 하기 때문입니다.
네 번째로, 시그널링 서버를 통해 상대방에게 알립니다. 상대방의 화면에 "상대방이 음소거했습니다"라는 메시지를 표시하거나, 상대방 비디오가 꺼지면 아바타나 이름을 표시합니다.
또한 키보드 단축키(Ctrl+M, Ctrl+E)를 지원하여 파워 유저들이 빠르게 제어할 수 있게 합니다. 여러분이 이 코드를 사용하면 사용자들이 화상 통화를 훨씬 편하게 사용할 수 있습니다.
예상치 못한 소음이 발생해도 당황하지 않고 즉시 음소거할 수 있으며, 프라이버시가 필요한 순간에 카메라를 빠르게 끌 수 있습니다. 또한 트랙을 재생성할 필요 없이 즉각적으로 반응하므로 부드러운 사용자 경험을 제공합니다.
실전 팁
💡 enabled 속성은 즉시 반영되지만, 네트워크로 전송되는 데이터는 약간의 지연이 있을 수 있습니다. 상대방 화면에서는 0.5-1초 후에 음소거가 적용될 수 있으니, "즉시 음소거됩니다"라고 과장하지 마세요.
💡 비디오를 끌 때 검은 화면만 보여주지 말고, 사용자의 이름이나 아바타를 표시하세요. Zoom이나 Teams처럼 "홍길동" 이름과 이니셜을 보여주면 훨씬 전문적입니다.
💡 음소거 상태에서 말하려고 하면 "지금 음소거 중입니다"라고 알려주는 기능을 추가하세요. 오디오 레벨을 모니터링하다가 특정 threshold를 넘으면 경고를 표시할 수 있습니다.
💡 모바일에서는 화면 잠금 상태에서도 음소거 버튼이 작동하도록 구현하세요. 이어폰의 버튼이나 알림 센터의 컨트롤을 통해서도 제어할 수 있으면 더욱 편리합니다.
💡 기본 상태를 사용자가 설정할 수 있게 하세요. "통화 시작 시 자동 음소거" 같은 옵션을 localStorage에 저장하면, 조용한 환경이 필요한 사용자들이 매우 좋아합니다.
6. 연결_품질_표시
시작하며
여러분이 화상 통화 중에 상대방 목소리가 끊기거나, 화면이 멈추는 경험을 해본 적 있나요? 이럴 때 사용자는 "내 인터넷이 문제인가?", "상대방 인터넷이 문제인가?", "앱이 잘못된 건가?"라고 궁금해합니다.
명확한 정보 없이는 불안하고 답답합니다. 실제 개발 현장에서 가장 많이 받는 사용자 불만이 바로 "왜 끊기는지 모르겠어요"입니다.
기술적으로는 네트워크 지연(latency), 패킷 손실(packet loss), 대역폭 부족 등 여러 원인이 있지만, 일반 사용자는 이런 용어를 이해하기 어렵습니다. 바로 이럴 때 필요한 것이 연결 품질 표시입니다.
실시간으로 연결 상태를 모니터링하고, 문제가 생기면 사용자에게 알려주며, 가능하면 자동으로 개선하는 기능입니다. 이것이 전문적인 화상 통화 앱의 차별점입니다.
개요
간단히 말해서, 연결 품질 표시는 WebRTC의 통계 API(getStats)를 사용하여 네트워크 상태를 실시간으로 측정하고 시각화하는 기능입니다. 지연 시간, 패킷 손실률, 비트레이트 등을 확인하여 품질을 판단합니다.
왜 이것이 중요할까요? 투명한 정보 제공은 사용자 만족도를 크게 높입니다.
연결이 나빠지면 "네트워크 연결 불량" 아이콘을 표시하거나, "와이파이를 확인하세요"라는 안내를 주면 사용자는 상황을 이해하고 조치를 취할 수 있습니다. 예를 들어, 중요한 화상 면접 중에 연결이 불안정하면, 사용자는 유선 랜으로 바꾸거나, 다른 기기의 와이파이를 끄거나, 장소를 옮길 수 있습니다.
기존에는 단순히 "연결됨" 또는 "끊김" 두 가지 상태만 표시했다면, 이제는 "좋음", "보통", "나쁨" 같은 세분화된 품질 정보를 제공할 수 있습니다. 이 기능의 핵심 특징은: 첫째, getStats API로 실시간 네트워크 메트릭 수집, 둘째, 품질 점수 계산 알고리즘으로 간단한 지표 제공, 셋째, 시각적 인디케이터(색상 바, 신호 강도 아이콘)로 직관적 표시입니다.
이러한 특징들이 사용자에게 안심감을 주고, 문제 해결을 돕습니다.
코드 예제
// 연결 품질 표시 요소
const qualityIndicator = document.getElementById('quality-indicator');
const qualityText = document.getElementById('quality-text');
const statsPanel = document.getElementById('stats-panel');
let statsInterval;
// 연결 품질 모니터링 시작
function startQualityMonitoring() {
// 1초마다 통계 수집
statsInterval = setInterval(async () => {
if (!peerConnection) return;
// WebRTC 통계 가져오기
const stats = await peerConnection.getStats();
const quality = analyzeConnectionQuality(stats);
// UI 업데이트
updateQualityUI(quality);
}, 1000);
}
// 통계 분석 함수
function analyzeConnectionQuality(stats) {
let packetsLost = 0;
let packetsReceived = 0;
let currentRoundTripTime = 0;
let availableIncomingBitrate = 0;
// stats 객체를 순회하며 필요한 지표 추출
stats.forEach(report => {
if (report.type === 'inbound-rtp' && report.kind === 'video') {
// 수신 비디오 통계
packetsLost += report.packetsLost || 0;
packetsReceived += report.packetsReceived || 0;
}
if (report.type === 'candidate-pair' && report.state === 'succeeded') {
// 네트워크 지연 시간 (RTT: Round Trip Time)
currentRoundTripTime = report.currentRoundTripTime || 0;
availableIncomingBitrate = report.availableIncomingBitrate || 0;
}
});
// 패킷 손실률 계산 (%)
const packetLossRate = packetsReceived > 0
? (packetsLost / (packetsLost + packetsReceived)) * 100
: 0;
// 지연 시간을 밀리초로 변환
const latency = currentRoundTripTime * 1000;
// 품질 점수 계산 (0-100)
let score = 100;
// 패킷 손실이 많으면 감점
if (packetLossRate > 5) score -= 30;
else if (packetLossRate > 2) score -= 15;
// 지연 시간이 길면 감점
if (latency > 300) score -= 40;
else if (latency > 150) score -= 20;
// 대역폭이 부족하면 감점
if (availableIncomingBitrate < 500000) score -= 20; // 500kbps 미만
return {
score: Math.max(0, score),
latency: latency.toFixed(0),
packetLoss: packetLossRate.toFixed(1),
bitrate: (availableIncomingBitrate / 1000).toFixed(0) // kbps로 변환
};
}
// UI 업데이트 함수
function updateQualityUI(quality) {
// 품질 등급 결정
let grade, color, emoji;
if (quality.score >= 80) {
grade = '우수';
color = '#4CAF50'; // 초록색
emoji = '📶';
} else if (quality.score >= 50) {
grade = '보통';
color = '#FF9800'; // 주황색
emoji = '📶';
} else {
grade = '불량';
color = '#F44336'; // 빨간색
emoji = '📵';
}
// 표시 업데이트
qualityIndicator.style.backgroundColor = color;
qualityText.textContent = `${emoji} ${grade}`;
// 상세 통계 패널 (개발자용 또는 고급 사용자용)
statsPanel.innerHTML = `
지연: ${quality.latency}ms |
패킷손실: ${quality.packetLoss}% |
속도: ${quality.bitrate}kbps
`;
// 품질이 나쁘면 경고 표시
if (quality.score < 50) {
showWarning('네트워크 연결이 불안정합니다. 와이파이를 확인해주세요.');
}
}
// 모니터링 중지
function stopQualityMonitoring() {
if (statsInterval) {
clearInterval(statsInterval);
statsInterval = null;
}
}
// 통화 시작 시 모니터링 시작
peerConnection.onconnectionstatechange = () => {
if (peerConnection.connectionState === 'connected') {
startQualityMonitoring();
} else if (peerConnection.connectionState === 'disconnected') {
stopQualityMonitoring();
}
};
설명
이것이 하는 일: 이 코드는 화상 통화의 네트워크 상태를 실시간으로 감시하고, 사용자가 이해하기 쉬운 형태로 보여줍니다. 마치 자동차의 계기판이 속도와 연료를 보여주듯이, 연결 상태를 한눈에 파악할 수 있게 해줍니다.
첫 번째로, setInterval로 1초마다 getStats()를 호출합니다. 이것은 WebRTC가 내부적으로 수집하는 방대한 통계 데이터를 가져오는 명령입니다.
왜 1초마다 하냐고요? 네트워크 상태는 실시간으로 변하기 때문입니다.
누군가 같은 와이파이에서 큰 파일을 다운받기 시작하면, 여러분의 화상 통화 품질이 즉시 나빠질 수 있습니다. 1초 간격이면 빠르게 감지하면서도 CPU를 많이 사용하지 않습니다.
그 다음으로, stats 객체를 순회하며 필요한 지표를 추출합니다. stats에는 수십 가지 정보가 들어있지만, 우리가 주목하는 것은 세 가지입니다: (1) packetsLost: 전송 중 손실된 데이터 패킷 수, (2) currentRoundTripTime: 데이터가 왕복하는 데 걸리는 시간(지연), (3) availableIncomingBitrate: 사용 가능한 다운로드 속도입니다.
이 세 가지만 알아도 연결 품질을 충분히 판단할 수 있습니다. 세 번째로, 품질 점수를 계산합니다.
100점 만점에서 시작하여, 문제가 있으면 감점하는 방식입니다. 예를 들어, 패킷 손실이 5% 이상이면 30점 감점, 지연이 300ms 이상이면 40점 감점 등입니다.
이 점수를 "우수", "보통", "불량" 같은 등급으로 변환하여 사용자에게 보여줍니다. 왜 이렇게 하냐고요?
"currentRoundTripTime: 0.287초"보다는 "연결 품질: 우수 📶"가 훨씬 이해하기 쉽기 때문입니다. 마지막으로, UI를 업데이트합니다.
품질에 따라 초록색, 주황색, 빨간색으로 색상을 바꾸고, 이모지를 표시합니다. 신호등처럼 직관적인 시각적 피드백입니다.
품질이 50점 미만으로 떨어지면 "네트워크 연결이 불안정합니다"라는 경고를 표시하여, 사용자가 조치를 취할 수 있게 합니다. 여러분이 이 코드를 사용하면 사용자들이 화상 통화 중 발생하는 문제를 이해하고 대응할 수 있습니다.
"왜 끊기지?"라는 막연한 불안 대신, "아, 지금 와이파이가 약하구나"라고 명확히 알 수 있습니다. 또한 개발자는 통계 데이터를 서버로 전송하여 전체 서비스의 품질을 모니터링하고, 개선점을 찾을 수 있습니다.
예를 들어, 특정 지역이나 시간대에 품질 문제가 많다면 서버 증설을 고려할 수 있습니다.
실전 팁
💡 getStats는 비동기 함수이고 많은 데이터를 반환하므로, 너무 자주 호출하면 성능에 영향을 줍니다. 1초 간격이 적당하며, 디버깅 시에만 0.5초로 줄이세요.
💡 품질 점수 계산 알고리즘은 여러분의 서비스 특성에 맞게 조정하세요. 화상 회의는 지연에 민감하고, 영상 스트리밍은 대역폭에 민감합니다. 가중치를 다르게 설정하세요.
💡 패킷 손실률이 갑자기 튀는 경우가 있으므로, 이동 평균(moving average)을 사용하여 부드럽게 만드세요. 최근 5초간의 평균을 내면 더 안정적인 지표를 얻을 수 있습니다.
💡 사용자에게는 간단한 등급만 보여주고, 상세 통계는 "고급 정보 보기" 버튼 뒤에 숨기세요. 일반 사용자는 숫자보다 색상과 이모지가 더 직관적입니다.
💡 품질이 일정 수준 이하로 떨어지면 자동으로 비디오 해상도를 낮추는 "적응형 비트레이트" 기능을 구현하세요. sender.setParameters()로 비트레이트를 동적으로 조정할 수 있습니다. Zoom이 바로 이 방식으로 안정성을 확보합니다.
댓글 (0)
함께 보면 좋은 카드 뉴스
WebRTC 화면 공유 완벽 가이드
WebRTC의 화면 공유 기능을 처음부터 끝까지 배워봅니다. getDisplayMedia API 사용법부터 화면과 카메라 동시 표시, 공유 중단 감지, 해상도 설정까지 실무에서 바로 활용할 수 있는 모든 것을 다룹니다.
WebRTC Mesh 방식 다자간 화상회의 완벽 가이드
다자간 화상회의를 구현하는 Mesh, SFU, MCU 방식의 차이부터 실제 구현까지. PeerConnection 관리, 참여자 추가/제거, 대역폭 최적화까지 실무에 바로 적용할 수 있는 완벽한 가이드입니다.
ICE Candidate 처리 완벽 가이드
WebRTC 연결을 위한 ICE Candidate 처리 과정을 초급 개발자도 쉽게 이해할 수 있도록 설명합니다. onicecandidate 이벤트부터 addIceCandidate 메서드, Trickle ICE 구현, NAT 트래버설까지 실무에서 바로 적용 가능한 예제와 함께 안내합니다.
WebRTC RTCPeerConnection 생성과 관리 완벽 가이드
WebRTC의 핵심인 RTCPeerConnection을 생성하고 관리하는 방법을 배웁니다. ICE 서버 설정부터 미디어 트랙 추가, 이벤트 관리, 연결 상태 모니터링까지 실무에서 바로 사용할 수 있는 완전한 가이드입니다.
Socket.io로 배우는 WebRTC 시그널링 서버 구축
WebRTC 연결을 위한 필수 요소인 시그널링 서버를 Socket.io로 구축하는 방법을 배웁니다. Room 관리, Offer/Answer 교환, ICE Candidate 전달 등 실시간 통신의 핵심 개념을 실무 예제와 함께 친절하게 설명합니다.