WebRTC(Web Real-Time Communication)는 웹 애플리케이션이 플러그인 없이도 브라우저들 간에 실시간으로 오디오, 비디오 등의 미디어를 캡처하고 스트리밍하며, 그외 임의의 데이터까지 교환할 수 있도록 해주는 기술입니다. 이러한 일련의 표준을 통해 별도의 소프트웨어 설치 없이 피어 투 피어(P2P) 방식으로 데이터 공유나 화상 회의를 수행할 수 있게 됩니다.
즉, 브라우저 내장 기능만으로 사용자들 끼리 직접 영상 통화, 음성 채팅, 파일 전송 등을 할 수 있도록 해주는 기술입니다.
카메라 영상이나 마이크 음성 등의 미디어 스트림을 나타내는 객체입니다. 일반적으로 navigator.mediaDevices.getUserMedia()
함수를 통해 사용자의 카메라/마이크 접근 허가 이후 Media Stream을 얻습니다. 이렇게 얻은 Media Stream은 비디오 요소에 출력하거나 P2P 연결로 전달할 수 있습니다.
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
const videoElement = document.getElementById('localVideo');
videoElement.srcObject = stream; // MediaStream을 video 요소에 연결
})
.catch(error => {
console.error('카메라/마이크 접근 실패:', error);
});
두 피어(peer) 간의 P2P 연결을 대표하는 객체입니다. WebRTC에서 네트워크 연결의 생성과 관리를 담당하며, 이 객체에 MediaStream 트랙이나 데이터 채널을 추가하여 실제 미디어/데이터 교환을 수행합니다. 또한 이 객체는 대역폭 관리, 네트워크 장애 대응, ICE 후보 처리 등을 내부적으로 수행하여 안정적인 연결을 유지합니다.
WebRTC 연결상에서 임의의 데이터(P2P 데이터)를 교환할 수 있게 해주는 데이터 통신 채널입니다. RTCDataChaanel은 RTCPeerConnection을 통해 생성되며, 전송에는 내부적으로 SCTP 프로토콜이 사용되어 신뢰성 있는 데이터 전송을 제공합니다.
일반 WebSocket처럼 send
와 onmessage
등을 이용해 손쉽게 P2P 데이터 통신을 구현할 수 있습니다.
위 세 가지 API를 조합함으로써 미디어 스트림과 임의의 데이터의 직접 통신을 구현할 수 있습니다.
WebRTC의 미디어 캡처 (MediaStream 획득) → 피어 연결 (RTCPeerConnection 생성 및 활용) → 데이터 교환 채널 (RTCDataChannel 사용)로 이어지는 구성이 P2P 실시간 통신의 기반입니다.
두 피어 간 네트워크 경로를 찾아내고 연결을 성립하기 위한 프레임워크입니다.
인터넷 환경에서는 방화벽이나 NAT 라우터 때문에 단순히 IP만 알아서는 통신이 안되는 경우가 많기 떄문에, ICE는 연결을 뚫기 위해 필요한 여러 기법(STUN/TURN)을 종합적으로 시도합니다.
즉, 직접 연결이 막혀있을 경우 대안을 찾고, 가능한 최선의 경로를 찾아주는 역할을 ICE가 담당합니다.
자신의 공인 IP주소와 포트 정보를 알아내기 위한 프로토콜입니다. NAT 뒤에 있는 피어는 직접 접속이 어려우므로, STUN 서버에게 요청을 보내서 Public IP와 포트를 응답받습니다. 또한 STUN 응답으로 해당 피어가 NAT 뒤에서 직접 접속 가능한지 여부도 알 수 있습니다.
이를 통해 각 피어는 상대방에게 전달할 자신의 공개 주소(ICE 후보)를 확보합니다.
릴레이 서버를 통한 우회 연결을 위한 프로토콜입니다. 일부 NAT 환경에서는 STUN으로 받은 주소로도 직접 통신이 불가능한데, 이 경우 TURN 서버를 경유하여 통신합니다. 피어는 TURN 서버와 연결을 맺고, 모든 데이터를 TURN 서버로 보내 중계하도록 합니다. 성능상 오버헤드가 있기 때문에 직접 통신이 불가능한 최후의 수단으로 사용됩니다
WebRTC에서 미디어 세션의 각종 정보를 표현하는 서식/포맷입니다. 예를 들어 두 피어가 주고받을 코덱, 해상도, 암호화 방식, 미디어 종류 등의 정보를 문자열로 기술한 것입니다. SDP 자체는 프로토콜이라기보다 데이터 포맷이며, 이것을 이용해 Offer/Answer 형식으로 피어 간 협상(negotiation)을 진행합니다. 한 마디로, 서로 어떤 환경과 조건으로 통신할지 묘사한 메타데이터가 SDP라고 할 수 있습니다.
이 외에도 WebRTC는 내부적으로 RTP/RTCP(실시간 전송 프로토콜 및 제어 프로토콜)를 통해 오디오/비디오 패킷을 교환하고, SCTP(Stream Control Transmission Protocol)를 통해 데이터 채널의 패킷을 전송합니다. 이러한 세부 프로토콜을 직접 다루기보다는, RTCPeerConnection API를 사용하면 브라우저가 ICE/STUN/TURN을 통한 경로 설정부터 SDP 협상과 패킷 전송까지 모두 내부적으로 처리해줍니다.
WebRTC를 통해 두 피어를 연결하려면 사전에 약속된 신호 교환(시그널링) 과정을 거쳐 서로를 찾고 연결 협상을 해야 합니다. WebRTC 자체는 시그널링 방법을 규정하지 않으며, 이를 위해 별도의 신호 서버(웹 소켓 서버 등)를 활용합니다. 아래는 WebRTC 연결 수립 과정의 흐름을 순서대로 나타낸 것입니다.
생성을 시작하는 쪽(Peer A)은 필요 시 getUSerMedia()
등으로 로컬 미디어 스트림을 얻고, new RTCPeerConnection()
을 호출하여 피어 연결 객체를 생성합니다. 이 때 피어 연결 설정에 STUN/TURN 서버 정보를 포함하여 NAT 통과 설정도 함께 합니다.
MediaStream의 각 트랙(track)을 RTCPeerConnection.addTrack()
등을 통해 peer A의 RTCPeerConnection에 추가합니다. 이렇게 하면 해당 미디어를 상대에게 보낼 준비가 됩니다.
이때 필요시, 데이터 채널도 createDataChannel()
로 미리 열 수 있습니다.
peer A는 RTCPeerConnection.createOffer()
를 호출하여 SDP 제안서(offer)를 생성합니다. 이 SDP에는 자신이 보내고자 하는 미디어 형식, 코덱, 네트워크 후보(ICE candidates) 등이 포함됩니다. 이어 RTCPeerConnection의 setLocalDescript(offer)
를 호출하여 peer A의 로컬 SDP로 설정합니다.
peer A는 신호 서버를 통해 SDP Offer 내용을 peer B에게 전달합니다.
상대 피어(peer B)는 신호 서버로부터 peer A의 Offer(SDP)를 받아서 자신의 setRemoteDescription(answer)
로 이를 원격 SDP로 설정합니다. 그런 다음 peer B측에서도 createAnswer()
를 호출하여 SDP 응답(answer)을 생성합니다. 생성된 answer SDP를 setLocalDescription(answer)로 설정하여 peer B의 로컬 SDP로 적용합니다.
peer B는 신호 서버를 통해 이 answer SDP를 peer A에게 전달합니다.
peer A는 peer B로부터 answer SDP를 건네 받으면 setRemoteDescription(answer)
로 이를 원격 SDP로 설정합니다. 이로써 양측의 SDP 협상(offer/answer 교환)이 완료되어, 서로의 미디어 및 연결 조건을 이해하게 됩니다.
양 피어는 각자의 RTCPeerConnection에서 ICE 후보지(네트워크 주소)들을 지속적으로 수집하며, 이는 icecandidate
이벤트로 콜백을 받습니다. 이 이벤트가 발생할 때마다 서로 신호 서버를 통해 상대에게 ICE 후보 정보를 전달합니다. 상대 피어는 받은 ICE 후보를 addIceCandidate()
로 추가합니다. 이러한 방식으로 SDP 교환 이후에도 추가 후보들을 계속 교환하여, 최적의 통신 경로를 찾습니다
서로 전달받은 ICE 후보들 중에서 직접 통신 가능한 쌍을 찾아 네트워크 검증을 마치면, RTCPeerConnection의 iceConnectionState
가 "connected" 또는 "completed"로 바뀌고 P2P 연결이 확립됩니다. 이 과정에서 양측 브라우저는 DTLS 핸드셰이크를 실행하여 암호화 채널을 설정하며, 이후 실시간 미디어/데이터 전송이 시작됩니다. 한쪽 피어에서 ontrack
이벤트를 통해 상대방의 MediaStream 트랙이 도착했다는 사실을 알려주고, 이를 비디오 요소에 출력하면 화상 통신이 구현됩니다.
Peer A
// 1. RTCPeerConnection 생성 (ICE 서버 정보 포함 가능)
const configuration = {
iceServers: [
{ urls: 'stun:stun.example.com' }, // STUN 서버
{ urls: 'turn:turn.example.com', credential: 'pwd', username: 'user' } // TURN 서버 (예시)
]
};
const pc = new RTCPeerConnection(configuration);
// 2. (선택) 데이터 채널 생성
const dataChannel = pc.createDataChannel("chat");
dataChannel.onopen = () => console.log("데이터채널 열린됨");
dataChannel.onmessage = (e) => console.log("상대 메세지:", e.data);
// 3. 로컬 미디어 스트림 획득 후 PeerConnection에 추가
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
stream.getTracks().forEach(track => pc.addTrack(track, stream));
// 로컬 비디오 출력 (자기 화면)
document.getElementById('localVideo').srcObject = stream;
// 4. Offer 생성 및 LocalDescription 설정
return pc.createOffer();
})
.then(offer => {
return pc.setLocalDescription(offer);
})
.then(() => {
// 5. 신호 서버를 통해 Offer SDP 전송
signalingSend({ type: 'offer', sdp: pc.localDescription });
})
.catch(err => console.error(err));
// ICE 후보 발생 시 상대에게 전송
pc.onicecandidate = (event) => {
if (event.candidate) {
signalingSend({ type: 'ice-candidate', candidate: event.candidate });
}
};
// 원격 트랙 수신 시 (상대 영상 스트림 수신)
pc.ontrack = (event) => {
const [remoteStream] = event.streams;
document.getElementById('remoteVideo').srcObject = remoteStream; // 원격 비디오 출력
};
Peer B
const pc = new RTCPeerConnection(configuration);
// (선택) 데이터 채널 수신 대기
pc.ondatachannel = (event) => {
const dataChannel = event.channel;
dataChannel.onmessage = (e) => console.log("상대 메세지:", e.data);
};
// 원격 Offer 수신 및 설정
signaling.on('offer', (message) => {
pc.setRemoteDescription(message.sdp)
.then(() => {
// (옵션) 필요하면 이 시점에 getUserMedia로 로컬 스트림 획득 후 pc.addTrack
// 6. Answer 생성
return pc.createAnswer();
})
.then(answer => {
// 7. LocalDescription 설정 (Answer)
return pc.setLocalDescription(answer);
})
.then(() => {
// 8. Answer SDP를 신호 서버 통해 응답 전송
signalingSend({ type: 'answer', sdp: pc.localDescription });
})
.catch(err => console.error(err));
});
// ICE 후보 처리
pc.onicecandidate = (event) => {
if (event.candidate) {
signalingSend({ type: 'ice-candidate', candidate: event.candidate });
}
};
signaling.on('ice-candidate', (message) => {
pc.addIceCandidate(message.candidate);
});
// 원격 트랙 수신 처리
pc.ontrack = (event) => {
const remoteStream = event.streams[0];
document.getElementById('remoteVideo').srcObject = remoteStream;
};
양쪽 피어 모두 RTCPeerConnection
객체를 만들고, 로컬 미디어 스트림을 연결하며, Offer/Answer 생성 및 교환을 통해 SDP 협상을 마칩니다. 그 다음 ICE 후보들도 상호 교환하여 P2P 경로를 확정지으면, ontrack
이벤트를 통해 상대방의 스트림을 수신하게 됩니다. 데이터 채널의 경우 한쪽에서 createDataChannel()
로 만들면, 상대방에는 ondatachannel
이벤트가 발생하므로 이를 통해 P2P 데이터 통신 채널도 설정됩니다. 실제로 WebRTC API를 이용한 P2P 연결은 위 코드와 같은 일련의 비동기 promise 체인으로 이루어지며, 브라우저가 많은 부분을 자동 처리해주므로 비교적 적은 코드로 구현할 수 있습니다.
참고
MDN - WebRTC API
MDN - RTCPeerConnection
MDN - WebRTC Protocols
MDN - Signaling and video Calling
web.dev - Get started with WebRTC
WebRTC 연결성 및 NAT 통과 기법