WebRTC 파일 전송: 브라우저 간 튜토리얼
WebRTC를 사용해 브라우저 간에 직접 파일을 전송하세요. 실시간 파일 공유를 위한 데이터 채널, 시그널링, 피어 연결 설정입니다.
WebRTC 파일 전송은 초기 핸드셰이크 이후 서버가 데이터 경로에 개입하지 않고 RTCDataChannel을 통해 두 브라우저 사이에서 직접 바이트를 이동합니다. 필요한 구성 요소: SDP 오퍼와 ICE 후보를 교환하는 시그널링 채널(WebSocket 또는 소형 릴레이), NAT 탐지용 STUN 서버, 대칭 NAT용 TURN 폴백, 신뢰할 수 있는 순서 있는 전달로 구성된 데이터 채널. 피어 연결이 성립되면 16 KB~256 KB 청크를 channel.send()로 전송하고 DTLS 1.2로 암호화된 SCTP 연결을 통해 거의 네트워크 회선 속도로 바이트가 흐르는 것을 확인할 수 있습니다.
WebRTC가 실제로 P2P를 실현하는 방법
WebRTC는 마법이 아닙니다. ICE, SDP, DTLS, SCTP를 쌓아 올린 구조입니다. 송신자가 RTCPeerConnection을 생성하고 데이터 채널을 열며 SDP 오퍼를 생성한 뒤 시그널링 채널을 통해 수신자에게 전달합니다. 수신자는 응답합니다. 양측이 ICE 후보(로컬 IP, STUN을 통한 반사 IP, TURN을 통한 릴레이 IP)를 교환해 작동하는 경로를 찾습니다. DTLS 1.2 핸드셰이크가 종단 간 이루어지고 SCTP가 신뢰할 수 있는 스트리밍을 위해 그 위에서 동작하며 바이트가 흐르기 시작합니다.
암호화는 의무적이며 기본 내장됩니다. 선택 해제가 불가능합니다. 이는 의미 있는 보안상의 이점입니다. WebSocket과 달리 네트워크 도청으로부터 보호하기 위해 애플리케이션 레이어 암호화를 별도로 추가할 필요가 없습니다. 자체 시그널링 서버에 대한 진정한 E2EE를 원한다면 AES-256-GCM을 추가로 레이어링하십시오. 악의적인 시그널링 서버가 자체 DTLS 인증서로 교체할 수 있기 때문입니다.
시그널링: WebRTC가 정의하지 않는 부분
WebRTC는 의도적으로 시그널링을 개발자에게 맡깁니다. 서버의 WebSocket이 적합하고, Firebase 실시간 DB에 게시된 공유 룸 코드도 괜찮으며, 수동으로 붙여넣은 SDP 문자열도 됩니다. 중요한 것은 양측이 최종적으로 오퍼, 응답, ICE 후보 스트림을 교환하는 것입니다.
Node.js 최소 시그널링 서버:
const rooms = new Map();
wss.on('connection', (ws) => {
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
if (msg.type === 'join') {
const room = rooms.get(msg.room) ?? new Set();
room.add(ws); rooms.set(msg.room, room);
} else {
for (const peer of rooms.get(msg.room) ?? []) {
if (peer !== ws) peer.send(raw);
}
}
});
});
20줄 미만입니다. 서버는 파일 바이트를 보지 않고 SDP와 ICE 메타데이터만 처리합니다. $5 VPS 또는 Cloudflare Workers에서 수백 개의 동시 전송을 처리할 수 있습니다.
피어 연결 설정
Google의 공개 STUN과 TURN 폴백으로 연결을 생성합니다.
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:turn.example.com:3478',
username: 'user', credential: 'pass' }
]
});
가정용 연결의 약 15~25%는 STUN이 통과할 수 없는 대칭 NAT 뒤에 있으므로 프로덕션 서비스에서 TURN은 선택 사항이 아닙니다. 충분한 대역폭이 있는 VPS에서 coturn을 실행하거나 Xirsys 또는 Twilio 같은 관리형 TURN 공급자를 이용하십시오.
오퍼를 생성하기 전에 오퍼러 측에서 데이터 채널을 엽니다.
const channel = pc.createDataChannel('file', {
ordered: true, maxRetransmits: null
});
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signaling.send({ type: 'offer', sdp: offer.sdp });
순서 있음 + 무제한 재전송은 TCP에 준하는 신뢰성을 제공합니다. 비순서 모드는 더 빠르지만 애플리케이션 레벨의 재조립이 필요합니다.
채널을 위한 파일 청킹
SCTP는 실질적으로 메시지당 256 KB 한도가 있으며 구형 브라우저는 16 KB 이상에서 어려움을 겪습니다. 안전한 기본값은 File.slice()를 통해 파일을 읽은 16 KB 청크입니다.
const chunkSize = 16 * 1024;
let offset = 0;
channel.bufferedAmountLowThreshold = 1024 * 1024;
function sendNext() {
while (offset < file.size && channel.bufferedAmount < 4 * 1024 * 1024) {
const chunk = file.slice(offset, offset + chunkSize);
chunk.arrayBuffer().then((buf) => channel.send(buf));
offset += chunkSize;
}
}
channel.onbufferedamountlow = sendNext;
sendNext();
bufferedAmount 워터마크는 기가바이트를 SCTP 전송 버퍼에 큐잉해 메모리가 부족해지는 것을 방지합니다. 버퍼가 1 MB 미만으로 떨어지면 4 MB까지 다시 채웁니다. 이 방식으로 로컬 네트워크에서 40~80 MB/s, 일반 가정용 인터넷에서 5~20 MB/s의 처리량을 달성할 수 있습니다.
바이트 수신 및 디스크 쓰기
응답자 측에서 수신 채널을 수신합니다.
pc.ondatachannel = ({ channel }) => {
const chunks = [];
let received = 0;
channel.onmessage = ({ data }) => {
chunks.push(data);
received += data.byteLength;
updateProgress(received);
if (received === expectedSize) finish(chunks);
};
};
500 MB보다 큰 파일의 경우 RAM에 누적하지 마십시오. File System Access API를 사용해 디스크에 직접 스트리밍하십시오.
const handle = await window.showSaveFilePicker({ suggestedName: fileName });
const writable = await handle.createWritable();
channel.onmessage = async ({ data }) => writable.write(data);
Firefox와 Safari는 아직 showSaveFilePicker를 지원하지 않으므로 해당 브라우저에서는 Blob + URL.createObjectURL 다운로드로 폴백하십시오. 최대 2 GB로 제한됩니다.
바이트 전에 파일 메타데이터 전송
수신자는 바이트 스트림이 시작되기 전에 파일명, 크기, MIME 유형을 알아야 합니다. 데이터 채널에서 작은 JSON 핸드셰이크를 사용하십시오.
channel.send(JSON.stringify({
type: 'metadata', name: file.name,
size: file.size, mime: file.type, sha256: fileHash
}));
그런 다음 바이너리 모드로 전환합니다. 수신자는 data가 문자열인지 ArrayBuffer인지에 따라 전환합니다. 전송 후 무결성 검증을 위해 파일의 SHA-256을 포함하고, 애플리케이션 레이어 AES-GCM을 추가로 레이어링한다면 키 지문도 선택적으로 포함하십시오.
연결 실패 처리
WebRTC 데이터 채널은 세 가지 방식으로 실패합니다. ICE가 완료되지 않는 경우(NAT, 방화벽 차단), DTLS 핸드셰이크 실패(클럭 스큐, 인증서 문제), 전송 중 연결 끊김(랩톱 슬립, 네트워크 변경). pc.oniceconnectionstatechange를 수신하고 'failed' 또는 'disconnected'에 반응하십시오. Chrome은 'failed'로 이동하기 전에 'disconnected'를 몇 초 유지하고, Safari는 덜 관대합니다.
전송 중간 실패 시 전체 연결을 재구성하지 않고 ICE를 재시작하십시오.
await pc.restartIce();
const offer = await pc.createOffer({ iceRestart: true });
// 시그널링을 통해 재전송
재시작이 실패하면 서버를 통한 재개 가능한 업로드로 폴백하십시오. 이는 하이브리드 P2P + 서버 아키텍처입니다. Wormhole과 justbeamit 같은 일부 WebRTC 전송 도구가 이 패턴을 사용합니다. 순수 P2P가 작동하지 않는 15%의 네트워크 환경을 처리하기 위해서입니다.
자체 서버에 대한 종단 간 암호화 추가
WebRTC의 내장 DTLS는 네트워크 공격자로부터 보호하지만 악의적이거나 침해된 시그널링 서버는 막지 못합니다. 진정한 E2EE를 위해 양측이 ECDH P-256 키쌍을 생성하고 짧은 대역 외 코드(QR 또는 6단어 패스프레이즈)를 통해 공개 키를 교환하며 HKDF-SHA256으로 공유 비밀을 파생하고 send 호출 전에 각 데이터 채널 메시지를 AES-256-GCM으로 암호화하십시오. 이렇게 하면 시그널링 서버가 DTLS 인증서를 교체하더라도 파일을 읽을 수 없습니다.
HexaTransfer는 하이브리드 아키텍처를 사용합니다. P2P 방식 대신 클라이언트 측 AES-256-GCM으로 암호화된 암호문을 서버에 저장하여 직접성을 오프라인 허용 공유로 맞바꿉니다. hexatransfer.com에서 사용해보세요 — 무료, 계정 불필요, 최대 10 GB.
WebRTC의 강점과 약점
WebRTC 파일 전송은 두 피어가 동시에 온라인 상태이고 자체 서버로부터의 프라이버시가 중요하며 파일이 릴레이 대역폭 비용이 부담스러울 만큼 큰 경우(100 MB 이상)에 탁월합니다. 사용자가 전송 후 자리를 비울 때, 수신자가 몇 시간 후에 링크를 열 때, 또는 수신자가 STUN과 TURN을 차단하는 제한적인 기업 네트워크에 있을 때는 적합하지 않습니다. 범용 전송 도구로서 순수 P2P는 사용 사례의 약 60%를 처리합니다. 나머지 40%는 서버 기반 폴백이 필요합니다. 거의 모든 "P2P 파일 전송" 제품이 아키텍처 어딘가에 릴레이를 두는 이유가 바로 이것입니다.
엔드투엔드 암호화로 대용량 파일을 안전하게 전송
엔드투엔드 암호화로 최대 10GB의 파일을 무료로 전송하세요. 계정이 필요하지 않습니다. 업로드 전에 브라우저에서 파일이 암호화되어 다른 사람은 읽을 수 없습니다.
파일 보내기