WebRTC 박살내기 마지막 시리즈 입니다. WebRTC의 RTCDataChannel을 이해하고, 채팅·파일 전송·게임 동기화까지 실시간 데이터 전송의 모든 것을 알아봅니다.
WebRTC 박살내기 세번째 시리즈 입니다. WebRTC의 핵심 RTCPeerConnection을 완벽하게 이해하고, 연결 생성부터 이벤트 처리, 품질 관리, 연결 복구까지 실전 예제와 함께 알아봅니다.
WebRTC 박살내기 첫번째 시리즈 입니다. WebRTC 기본 개념부터 시그널링, Offer/Answer(SDP), Trickle ICE, STUN/TURN, NAT, 그리고 Mesh·SFU·MCU 아키텍처까지 한 번에 정리합니다.
WebRTC 박살내기 두번째 시리즈 입니다. WebRTC의 MediaStream과 MediaStreamTrack을 깊이 이해하고, getUserMedia부터 트랙 제어, 품질 관리까지 실전 예제와 함께 알아봅니다.
안녕하세요! 지난 글에서는 RTCPeerConnection의 이벤트 흐름을 배웠습니다.
PeerConnection을 생성하고, 미디어 트랙을 주고받는 방법까지 알아보았는데요.
그런데 혹시 이런 생각 해보신 적 없으신가요?
"WebRTC로 채팅도 만들 수 있을까?"
"서버 없이 브라우저끼리 파일을 주고받을 수 있을까?"
"게임에서 플레이어 위치를 실시간으로 동기화하려면?"
네, 전부 가능합니다! WebRTC는 영상·음성뿐만 아니라 모든 종류의 데이터를 P2P로 전송할 수 있습니다.
이를 담당하는 것이 바로 RTCDataChannel입니다.
이번 글에서는 데이터 채널이 무엇인지, 언제 어떻게 사용하는지 차근차근 알아보겠습니다.
RTCDataChannel은 두 브라우저가 직접 데이터를 주고받을 수 있는 통로입니다.
마치 두 사람이 실로 전화기를 만들어 대화하듯이, 브라우저끼리 직접 연결되는 거죠.
영상 통화를 하면서 채팅도 하고, 파일도 주고받고, 게임 데이터도 동기화할 수 있습니다.
서버를 거치지 않기 때문에 빠르고, 서버 비용도 절약됩니다.
🧐 서버를 완전히 안 거칠까?
DataChannel은 연결이 성립되면 보통 브라우저 간 P2P로 통신합니다.
다만 일부 네트워크에서는 TURN 릴레이를 통해 서버를 경유할 수 있어요.
이 경우 대역폭 비용이 발생합니다.
채팅이나 실시간 데이터 전송이라고 하면 보통 WebSocket을 떠올리시죠?
DataChannel도 비슷하지만 큰 차이가 있습니다.
사용자A -> 서버 -> 사용자B
모든 데이터가 서버를 거쳐야 합니다.
사용자A -> -> -> 사용자B
한 번 연결되면 피어끼리 직접 통신합니다.
| 특징 | WebSocket | RTCDataChannel |
|---|---|---|
| 연결 방식 | 서버를 통한 중계 | P2P 직접 연결 |
| 지연 시간 | 중간 | 매우 낮음 |
| 서버 부하 | 높음 | 없음 (연결 후) |
| 신뢰성 | 항상 보장 | 선택 가능 |
| 순서 보장 | 항상 보장 | 선택 가능 |
가장 큰 차이는 중간에 서버가 없다는 것입니다.
화상 통화 중에 채팅을 하거나 파일을 보내도 서버 트래픽이 증가하지 않습니다.
RTCDataChannel은 내부적으로 SCTP라는 프로토콜을 사용합니다.
💡 스트림 제어 전송 프로토콜 (Stream Control Transmission Protocol, SCTP)이란?
"우체국"처럼 생각하면 쉽습니다.
창구가 여러 개라서 줄이 안 꼬이고, 예비 도로까지 확보한 배송 시스템이에요.각 소포(메시지)마다 일반 택배(신뢰성 보장)처럼 반드시 전달할지,
퀵(빠른 전달)처럼 늦으면 버리기로 할지 고를 수 있고,
순서대로 받을지? 빨리 오는 대로 받을지?에 대한 방식도 창구별로 정할 수 있어요.한 창구가 막혀도 다른 창구는 계속 처리되고,
한 도로가 막히면 다른 경로로 자동 우회합니다.
각 채널별로 "Reliable/Unreliable, Ordered/Unordered" 정책을 정합니다
SCTP는 두 가지 옵션을 조합할 수 있습니다.
실시간 게임을 예로 들어볼게요.
플레이어 위치 전송{
ordered: false,
maxRetransmits: 0
} {
ordered: true, // 기본값
} 이런 식으로 상황에 맞는 최적의 방법을 선택할 수 있습니다!

데이터 채널은 한쪽에서 만들고, 다른 쪽에서 받습니다.
마치 전화를 거는 사람과 받는 사람처럼요.
const pc = new RTCPeerConnection();
// 채널 생성
const channel = pc.createDataChannel("chat");
// 연결되면 메시지 보낼 수 있음
channel.onopen = () => {
console.log("✅ 채널 연결됨!");
channel.send("안녕하세요!");
};
// 메시지 받기
channel.onmessage = (event) => {
console.log("받은 메시지:", event.data);
}; // 상대방이 만든 채널 받기
pc.ondatachannel = (event) => {
const channel = event.channel;
console.log("📡 채널 수신됨");
channel.onmessage = (event) => {
console.log("받은 메시지:", event.data);
// 받은 메시지에 답장
channel.send("반가워요!");
};
}; ⚠️ 주의사항
createDataChannel()을 호출하는 쪽이 Offer를 먼저 보내야 합니다.
Answer를 보내는 쪽은ondatachannel로만 채널을 받을 수 있어요.
채널을 만들 때 전송 방식을 정할 수 있습니다.
// 채팅용 - 모든 메시지가 순서대로 도착
const chatChannel = pc.createDataChannel("chat", {
ordered: true // 순서 보장 (기본값)
});
// 게임용 - 빠른 전송, 손실 허용
const gameChannel = pc.createDataChannel("position", {
ordered: false, // 순서 상관없음
maxRetransmits: 0 // 재전송 안 함
});
// 파일 전송용 - 완전한 신뢰성
const fileChannel = pc.createDataChannel("file", {
ordered: true
// maxRetransmits와 maxPacketLifeTime 둘 다 없으면
// 무한 재전송 = 완전한 신뢰성
}); | 옵션 | 의미 | 사용 예시 |
|---|---|---|
ordered | 순서 보장 여부 | 채팅, 파일 |
maxRetransmits | 최대 재전송 횟수 | 중요한 명령 |
maxPacketLifeTime | 패킷 수명 (밀리초) | 실시간 센서 데이터 |
💡 재전송 옵션 선택 기준
- 둘 다 안 쓰면: 무한 재전송 (완전한 신뢰성)
maxRetransmits: 3: 3번까지만 재시도maxPacketLifeTime: 1000: 1초 안에 못 가면 포기- 둘 다 쓰면 안 됩니다!
실제로 어떤 설정을 써야 할까요? 대표적인 사용 사례를 볼게요.
요구사항: 모든 메시지가 순서대로 도착해야 함
const chatChannel = pc.createDataChannel("chat", {
ordered: true
// 완전한 신뢰성 (기본값)
});
chatChannel.onopen = () => {
// 연결되면 메시지 보낼 수 있음
document.getElementById("sendBtn").disabled = false;
};
chatChannel.onmessage = (event) => {
// 화면에 메시지 표시
const msg = JSON.parse(event.data);
addMessageToChat(msg.text, msg.sender);
};
// 메시지 보내기
function sendMessage(text) {
const message = {
text: text,
sender: "me",
timestamp: Date.now()
};
chatChannel.send(JSON.stringify(message));
} ordered: true요구사항: 최신 위치만 중요, 이전 데이터는 무시
const positionChannel = pc.createDataChannel("position", {
ordered: false, // 순서 상관없음
maxRetransmits: 0 // 재전송 안 함 = 빠른 전달
});
// 60fps로 위치 전송
setInterval(() => {
const position = {
x: player.x,
y: player.y,
timestamp: Date.now()
};
positionChannel.send(JSON.stringify(position));
}, 16); // 약 60fps
positionChannel.onmessage = (event) => {
const pos = JSON.parse(event.data);
updateRemotePlayer(pos.x, pos.y);
}; ordered: falsemaxRetransmits: 0요구사항: 모든 데이터 조각이 정확히 도착해야 함
const fileChannel = pc.createDataChannel("file", {
ordered: true
// 완전한 신뢰성
});
async function sendFile(file) {
// 1. 파일 정보 먼저 보내기
fileChannel.send(
JSON.stringify({
type: "file-start",
name: file.name,
size: file.size
})
);
// 2. 16KB씩 잘라서 보내기
const CHUNK_SIZE = 16384;
const buffer = await file.arrayBuffer();
for (let i = 0; i < buffer.byteLength; i += CHUNK_SIZE) {
const chunk = buffer.slice(i, i + CHUNK_SIZE);
fileChannel.send(chunk);
// 진행률 표시
const progress = ((i + CHUNK_SIZE) / buffer.byteLength) * 100;
console.log(`전송 중: ${Math.min(progress, 100).toFixed(1)}%`);
}
// 3. 전송 완료 신호
fileChannel.send(JSON.stringify({ type: "file-end" }));
} ordered: true데이터 채널은 여러 종류의 데이터를 보낼 수 있습니다.
// 1. 문자열
channel.send("안녕하세요!");
// 2. JSON 데이터
const data = { type: "move", x: 100, y: 200 };
channel.send(JSON.stringify(data));
// 3. 바이너리 데이터 (ArrayBuffer)
const buffer = new ArrayBuffer(8);
channel.send(buffer);
// 4. Blob (파일 등)
const blob = new Blob(["Hello"], { type: "text/plain" });
channel.send(blob); channel.onmessage = (event) => {
const data = event.data;
// 문자열인지 확인
if (typeof data === "string") {
console.log("텍스트:", data);
// JSON인지 확인
try {
const json = JSON.parse(data);
console.log("JSON:", json);
} catch (e) {
// 일반 문자열
}
}
// ArrayBuffer
else if (data instanceof ArrayBuffer) {
console.log("바이너리 데이터:", data.byteLength, "bytes");
}
// Blob
else if (data instanceof Blob) {
data.text().then((text) => console.log("Blob:", text));
}
}; 데이터를 너무 빨리 많이 보내면 버퍼가 가득 찹니다.
// ❌ 나쁜 예: 버퍼 확인 없이 계속 보내기
for (let i = 0; i < 10000; i++) {
channel.send(`메시지 ${i}`); // 버퍼 초과 가능!
}
// ✅ 좋은 예: 버퍼 확인하면서 보내기
function safeSend(data) {
// 버퍼가 1MB 미만일 때만 보내기
if (channel.bufferedAmount < 1024 * 1024) {
channel.send(data);
return true;
}
console.warn("버퍼 가득 참, 잠시 대기");
return false;
}
// 버퍼 여유 생기면 알림
channel.bufferedAmountLowThreshold = 65536; // 64KB
channel.onbufferedamountlow = () => {
console.log("버퍼 여유 생김!");
}; // 메시지 보내기 전에 항상 확인
if (channel.readyState === "open") {
channel.send("Hello!");
} else {
console.log("채널이 아직 열리지 않았습니다");
}
// readyState 값
// "connecting" - 연결 중
// "open" - 사용 가능
// "closing" - 닫히는 중
// "closed" - 닫힘
브라우저마다 한 번에 보낼 수 있는 크기가 다릅니다.
// 브라우저에 따라 지원 여부가 다름
console.log(pc.sctp?.maxMessageSize); // 지원 안 하면 undefined
// 안전하게 16KB 청크로 전송하는 것을 권장
// 큰 파일은 잘라서 보내기
const CHUNK_SIZE = 16384; // 16KB (안전한 크기)
async function sendLargeData(file) {
const buffer = await file.arrayBuffer();
for (let i = 0; i < buffer.byteLength; i += CHUNK_SIZE) {
const chunk = buffer.slice(i, i + CHUNK_SIZE);
// 버퍼 확인하면서 보내기
while (channel.bufferedAmount > CHUNK_SIZE * 2) {
await new Promise((resolve) => setTimeout(resolve, 10));
}
channel.send(chunk);
}
} RTCDataChannel은 모던 브라우저에서 지원합니다.
다만 iOS는 백그라운드/저전력 정책의 영향을 받아 연결 유지가 제한될 수 있습니다.
| 브라우저 | 지원 버전 | 특이사항 |
|---|---|---|
| Chrome | 26+ | 완전 지원 |
| Firefox | 22+ | 완전 지원 |
| Safari | 11+ | iOS 백그라운드 제한 (재연결 로직 권장) |
| Edge | 79+ | Chrome과 동일 |
이번 글에서는 RTCDataChannel에 대해 알아봤습니다.
| 용도 | 설정 |
|---|---|
| 채팅 | ordered: true (완전한 신뢰성) |
| 파일 전송 | ordered: true (완전한 신뢰성) |
| 게임 좌표 | ordered: false, maxRetransmits: 0 |
| 센서 데이터 | ordered: false, maxPacketLifeTime: 100 |
readyState)지금까지 WebRTC 시리즈에서 다룬 내용을 정리하면?
WebRTC는 영상·음성·데이터를 모두 P2P로 전송할 수 있는 강력한 플랫폼입니다.
화상 통화만이 아니라 실시간 협업, 파일 공유, 게임, IoT 등 다양한 분야에 활용할 수 있습니다.
지금까지 WebRTC의 핵심 개념과 동작 원리를 살펴봤어요.
WebRTC 박살내기 시리즈는 여기서 마무리하겠습니다.
긴 글 끝까지 읽어주셔서 감사합니다! 🙏