WebRTC: 웹의 실시간 통신 혁명
WebRTC(Web Real-Time Communication)는 별도의 플러그인이나 소프트웨어 설치 없이, 웹 브라우저 간에 오디오, 비디오, 데이터를 직접 주고받을 수 있게 하는 W3C와 IETF의 표준 기술입니다. 과거 플러그인 의존성으로 인한 보안, 성능, 호환성 문제를 해결하고 개방형 웹을 위한 표준화되고 안전하며 효율적인 실시간 통신 솔루션을 제공하는 것을 목표로 합니다.
WebRTC의 여정
WebRTC는 하루아침에 나타난 기술이 아닙니다. 플러그인 기반 통신의 한계에서 시작하여 구글의 전략적 인수, 그리고 W3C와 IETF의 오랜 표준화 노력을 거쳐 지금의 모습에 이르렀습니다.
~2010년
플러그인 시대의 한계
Adobe Flash, Microsoft Silverlight 등 브라우저 플러그인에 의존했습니다. 이는 잦은 보안 취약점, 플랫폼(특히 모바일) 간 호환성 부족, 불안정한 성능 등의 문제를 야기했습니다.
2010-2011년
구글의 기반 기술 확보 및 공개
구글은 RTC 기술 전문 기업인 GIPS(에코 캔슬링, 패킷 손실 은닉 등 핵심 기술 보유)와 비디오 코덱 개발사 On2(VP8 코덱)를 인수했습니다. 이후 이 기술들을 WebRTC라는 이름의 오픈 소스 프로젝트로 공개하며 표준화의 초석을 다졌습니다.
2011년
W3C와 IETF의 표준화 착수
두 표준화 기구가 역할을 분담했습니다. W3C는 개발자용 JavaScript API(`RTCPeerConnection` 등)를, IETF의 RTCWEB 워킹 그룹은 통신에 사용될 프로토콜(ICE, DTLS, SRTP 등)의 명세를 정의하는 작업을 시작했습니다.
2021년
WebRTC 1.0 공식 표준 등극
10년간의 노력 끝에 WebRTC 1.0이 W3C 공식 권고안(Recommendation)으로 발표되었습니다. 이로써 주요 브라우저 간의 상호 운용성이 보장되었고, WebRTC는 웹의 핵심 기술로 자리매김했습니다.
WebRTC 아키텍처 심층 분석
WebRTC 아키텍처는 크게 웹 API 계층과 브라우저 엔진 계층으로 나뉩니다. 웹 API는 개발자가 JavaScript를 통해 WebRTC 기능을 사용하는 인터페이스를 제공하며, 브라우저 엔진은 실제 미디어 처리, 네트워크 통신, 보안 기능 등을 C++로 구현하여 담당합니다. 각 구성 요소를 클릭하여 자세한 설명을 확인해보세요.
웹 API 계층 (W3C 표준)
JavaScript를 통해 WebRTC 기능에 접근합니다.
getUserMedia()
RTCPeerConnection
RTCDataChannel
브라우저 엔진 계층 (C++ 구현)
실제 미디어 처리 및 통신을 담당합니다.
음성 엔진
비디오 엔진
전송 구성 요소
구성 요소를 선택하세요
다이어그램에서 구성 요소를 클릭하면 여기에 자세한 설명이 표시됩니다.
WebRTC 연결 과정 상세 분석
WebRTC 연결은 시그널링, 네트워크 탐색(ICE), 보안 연결 수립의 3단계로 이루어집니다. 아래 애니메이션은 이 흐름을 시각적으로 보여줍니다.
피어 A
시그널링 서버
피어 B
1. 시그널링
2. 네트워크 탐색
3. 보안 P2P 채널 수립
연결 과정 핵심 요소
SDP (Offer/Answer)
피어들은 시그널링 서버를 통해 통신 설정을 교환합니다. 이 때 사용되는 것이 SDP(Session Description Protocol)입니다. 한 피어가 자신의 미디어 정보를 담아 Offer를 보내면, 다른 피어가 이를 받고 자신의 정보를 담아 Answer로 응답합니다.
ICE Candidate
NAT 환경에서도 P2P 통신이 가능하도록, 각 피어는 연결 가능한 자신의 네트워크 주소 정보(ICE Candidate)를 수집하여 교환합니다. 이를 통해 최적의 통신 경로를 찾게 됩니다.
핵심 프로토콜 심층 분석
WebRTC는 다양한 IETF 표준 프로토콜을 활용하여 실시간 통신 기능을 제공합니다. 각 프로토콜은 특정 역할을 수행하며, 함께 작동하여 안정적이고 안전한 통신을 가능하게 합니다. 각 프로토콜 카드를 클릭하여 상세한 역할을 알아보세요.
코덱 비교 분석
코덱은 미디어 데이터를 압축하고 해제하여 네트워크를 통해 효율적으로 전송하는 역할을 합니다. 코덱의 선택은 품질과 대역폭 사용량에 직접적인 영향을 미칩니다. 버튼을 눌러 주요 오디오 및 비디오 코덱의 특성을 비교해보세요.
WebRTC 보안 심층 분석
WebRTC는 설계 단계부터 보안을 최우선으로 고려하여 개발되었습니다. 종단 간 암호화, 보안 시그널링, 상호 인증 등 다양한 보안 메커니즘을 통해 사용자의 통신을 보호합니다.
필수 종단간 암호화 (E2EE)
WebRTC는 모든 미디어 스트림(SRTP)과 데이터 채널(DTLS 기반 SCTP)에 대해 종단 간 암호화를 강제합니다. 이는 통신 내용이 중간 서버(시그널링, STUN/TURN 서버 포함)를 포함한 그 누구에게도 노출되지 않도록 보장합니다. 암호화는 협상 과정에서 생성된 세션 키를 사용하여 이루어집니다.
보안 컨텍스트 및 시그널링
WebRTC API는 `getUserMedia`와 같은 민감한 기능에 접근하기 위해 HTTPS와 같이 안전한 출처(Secure Origin)에서만 실행되도록 제한됩니다. 또한 시그널링 과정에서 교환되는 SDP와 ICE Candidate 정보는 잠재적인 정보 유출 및 조작 위험을 방지하기 위해 WSS(WebSocket Secure)와 같은 보안 프로토콜을 통해 암호화하여 전송하는 것이 강력히 권장됩니다.
인증 및 무결성 검증
DTLS 핸드셰이크 과정에서 각 피어는 자신의 X.509 인증서를 교환하여 신원을 확인하고, 통신 채널의 무결성을 보장합니다. SDP에 포함된 인증서 지문(`a=fingerprint`)을 통해 시그널링 과정에서 전달된 정보와 실제 연결의 보안 정보가 일치하는지 검증하여, 통신 상대를 스푸핑하려는 중간자 공격(MITM)을 효과적으로 방지합니다.
구현 예제 (TypeScript & Go)
WebRTC의 개념을 실제 코드에 적용하는 방법을 살펴봅니다. 클라이언트 측에서는 TypeScript를 사용하여 타입 안정성을 높이고, 서버 측에서는 Go를 사용하여 간단한 시그널링 서버 및 Pion 라이브러리를 활용한 Go 클라이언트를 구현하는 예제입니다.
클라이언트 측: TypeScript 예제
TypeScript를 사용하면 WebRTC API의 복잡한 객체와 이벤트에 타입을 명시하여 코드의 안정성과 가독성을 크게 향상시킬 수 있습니다. 아래는 기본적인 WebRTC 연결을 설정하는 클래스 예제입니다.
interface SignalingMessage {
type: 'offer' | 'answer' | 'candidate';
payload: any;
}
class WebRTCClient {
private pc: RTCPeerConnection;
private signaling: WebSocket;
constructor(signalingServerUrl: string) {
this.pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
this.signaling = new WebSocket(signalingServerUrl);
this.setupListeners();
}
private setupListeners(): void {
this.pc.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
if (event.candidate) {
this.sendMessage('candidate', event.candidate);
}
};
this.pc.ontrack = (event: RTCTrackEvent) => {
// 수신된 원격 미디어 트랙 처리 (예: video 엘리먼트에 추가)
console.log('Remote track received:', event.track);
};
this.signaling.onmessage = async (messageEvent: MessageEvent) => {
try {
const { type, payload } = JSON.parse(messageEvent.data) as SignalingMessage;
switch (type) {
case 'offer':
await this.pc.setRemoteDescription(new RTCSessionDescription(payload));
const answer = await this.pc.createAnswer();
await this.pc.setLocalDescription(answer);
this.sendMessage('answer', answer);
break;
case 'answer':
await this.pc.setRemoteDescription(new RTCSessionDescription(payload));
break;
case 'candidate':
await this.pc.addIceCandidate(new RTCIceCandidate(payload));
break;
}
} catch (error) {
console.error('Failed to parse signaling message:', error);
}
};
}
public async startCall(): Promise<void> {
// 로컬 미디어 트랙 추가 (예시)
// this.pc.addTrack(localStream.getAudioTracks()[0], localStream);
const offer = await this.pc.createOffer();
await this.pc.setLocalDescription(offer);
this.sendMessage('offer', offer);
}
private sendMessage(type: SignalingMessage['type'], payload: any): void {
this.signaling.send(JSON.stringify({ type, payload }));
}
}
서버 측: Go 시그널링 서버 예제
시그널링 서버는 WebRTC 표준에 포함되지 않지만, 피어 간의 연결 설정을 위해 필수적입니다. Go 언어와 Gorilla WebSocket 라이브러리를 사용하면 효율적인 시그널링 서버를 간단하게 구축할 수 있습니다. 아래 코드는 두 명의 클라이언트를 연결하고 메시지를 서로에게 전달하는 가장 기본적인 형태의 서버입니다.
package main
import (
"log"
"net/http"
"sync"
"github.com/gorilla/websocket"
)
var clients = make(map[*websocket.Conn]bool)
var broadcast = make(chan []byte)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // 모든 오리진 허용 (실제 환경에서는 특정 오리진만 허용)
},
}
var mutex = &sync.Mutex{}
func handleConnections(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Upgrade error:", err)
return
}
defer ws.Close()
mutex.Lock()
clients[ws] = true
mutex.Unlock()
log.Println("Client connected")
for {
_, msg, err := ws.ReadMessage()
if err != nil {
log.Printf("Read error: %v", err)
mutex.Lock()
delete(clients, ws)
mutex.Unlock()
break
}
broadcast <- msg
}
}
func handleMessages() {
for {
msg := <-broadcast
mutex.Lock()
for client := range clients {
err := client.WriteMessage(websocket.TextMessage, msg)
if err != nil {
log.Printf("Write error: %v", err)
client.Close()
delete(clients, client)
}
}
mutex.Unlock()
}
}
func main() {
http.HandleFunc("/ws", handleConnections)
go handleMessages()
log.Println("HTTP server started on :8000")
err := http.ListenAndServe(":8000", nil)
if err != nil {
log.<Fatal("ListenAndServe: ", err)
}
}
Go 클라이언트 (Pion) 예제
Pion은 Go 언어로 작성된 WebRTC 구현체로, Go 애플리케이션 내에서 WebRTC 클라이언트 또는 고급 서버 기능을 구현할 때 유용합니다. 아래는 Pion을 사용하여 시그널링 서버와 통신하며 간단한 데이터 채널 연결을 시도하는 예제입니다. 실제 사용 시에는 시그널링 로직(SDP 및 ICE Candidate 교환)을 이전 Go 서버 예제 또는 유사한 시그널링 메커니즘과 연동해야 합니다.
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"os"
"strings"
"time"
"github.com/pion/webrtc/v3"
"github.com/gorilla/websocket" // 시그널링을 위해 사용 (예시)
)
func main() {
config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
}
peerConnection, err := webrtc.NewPeerConnection(config)
if err != nil {
panic(err)
}
defer func() {
if err := peerConnection.Close(); err != nil {
fmt.Printf("cannot close peerConnection: %v\n", err)
}
}()
dataChannel, err := peerConnection.CreateDataChannel("data", nil)
if err != nil {
panic(err)
}
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
})
dataChannel.OnOpen(func() {
fmt.Printf("Data channel '%s'-'%d' open. Random messages will now be sent to any connected DataChannels every 5 seconds\n", dataChannel.Label(), dataChannel.ID())
for range time.NewTicker(5 * time.Second).C {
message := "Hello from Pion Go Client!"
fmt.Printf("Sending message: %s\n", message)
sendErr := dataChannel.SendText(message)
if sendErr != nil {
panic(sendErr)
}
}
})
dataChannel.OnMessage(func(msg webrtc.DataChannelMessage) {
fmt.Printf("Message from DataChannel '%s': '%s'\n", dataChannel.Label(), string(msg.Data))
})
// --- 시그널링 로직 (Offer/Answer, ICE Candidate 교환) 시작 ---
// 이 부분은 실제 시그널링 서버와 연동하여 구현해야 합니다.
// 예시: WebSocket을 사용한 메시지 교환
wsConn, _, wsErr := websocket.DefaultDialer.Dial("ws://localhost:8000/ws", nil)
if wsErr != nil {
log.Fatalf("WebSocket dial error: %v", wsErr)
}
defer wsConn.Close()
log.Println("Connected to signaling server")
peerConnection.OnICECandidate(func(c *webrtc.ICECandidate) {
if c == nil { return }
candidateJSON, _ := json.Marshal(c.ToJSON())
message := map[string]interface{}{"type": "candidate", "payload": c.ToJSON()}
msgBytes, _ := json.Marshal(message)
wsConn.WriteMessage(websocket.TextMessage, msgBytes)
fmt.Println("Sent ICE Candidate: ", string(candidateJSON))
})
// Offer 생성 및 전송 (만약 연결을 시작하는 측이라면)
// offer, err := peerConnection.CreateOffer(nil)
// if err != nil { panic(err) }
// peerConnection.SetLocalDescription(offer)
// offerJSON, _ := json.Marshal(offer)
// message := map[string]interface{}{"type": "offer", "payload": offer}
// msgBytes, _ := json.Marshal(message)
// fmt.Println("Sent Offer")
go func() {
for {
_, message, err := wsConn.ReadMessage()
if err != nil {
log.Println("WebSocket read error:", err)
return
}
var sigMsg map[string]interface{}
json.Unmarshal(message, &sigMsg)
switch sigMsg["type"] {
case "offer":
offer := webrtc.SessionDescription{}
payloadBytes, _ := json.Marshal(sigMsg["payload"])
json.Unmarshal(payloadBytes, &offer)
peerConnection.SetRemoteDescription(offer)
answer, err := peerConnection.CreateAnswer(nil)
if err != nil { panic(err) }
peerConnection.SetLocalDescription(answer)
answerMsg := map[string]interface{}{"type": "answer", "payload": answer}
answerBytes, _ := json.Marshal(answerMsg)
wsConn.WriteMessage(websocket.TextMessage, answerBytes)
fmt.Println("Received Offer, Sent Answer")
case "answer":
answer := webrtc.SessionDescription{}
payloadBytes, _ := json.Marshal(sigMsg["payload"])
json.Unmarshal(payloadBytes, &answer)
peerConnection.SetRemoteDescription(answer)
fmt.Println("Received Answer")
case "candidate":
candidate := webrtc.ICECandidateInit{}
payloadBytes, _ := json.Marshal(sigMsg["payload"])
json.Unmarshal(payloadBytes, &candidate)
peerConnection.AddICECandidate(candidate)
fmt.Println("Received ICE Candidate")
}
}
}()
// --- 시그널링 로직 끝 ---
fmt.Println("Pion WebRTC client started. Press Ctrl+C to exit.")
select {} // 프로그램이 종료되지 않도록 대기
}