[DataChannel 딥다이브] WebRTC DataChannel은 파일 전송 API가 아니다
DataChannel로 파일을 보낼 수 있다는 말을 처음 들으면 브라우저가 파일 전송 API를 제공하는 것처럼 느껴진다. 실제로는 bytes를 주고받는 통로를 받은 것에 가깝다. 파일 이름, 크기, MIME 타입, chunk 순서, 무결성, 재시도, 저장 위치, 진행률은 DataChannel이 알아서 처리하지 않는다. 이 부분을 애플리케이션 프로토콜로 설계해야 파일 전송 기능이 된다. 같은 입력인데 어떤 환경에서만 DataChannel가 도드라졌다.

DataChannel을 파일 API로 착각한 이유
backpressure는 DataChannel 파일 전송에서 자주 놓치는 지점이다. bufferedAmount를 보지 않고 계속 send하면 메모리가 커지고 탭이 불안정해진다. 수신 저장이 느린데 송신만 빠르면 브라우저가 버티지 못한다. 해결은 화려하지 않다. chunk를 보낸 뒤 버퍼가 일정 기준 아래로 내려올 때까지 기다리고, 수신 ACK나 window 크기를 보며 속도를 맞춘다. 빠르게 보내는 것보다 끝까지 안전하게 보내는 것이 중요하다.
재시도 정책은 사용자 신뢰를 만든다. 연결이 잠깐 끊겼을 때 무조건 처음부터 다시 보내면 대용량 파일에서는 사실상 실패와 같다. 마지막으로 확인된 ACK, 저장된 offset, checksum 상태를 기준으로 이어 보낼 수 있어야 한다. 완전한 resume을 당장 구현하지 못한다면 최소한 어디까지 보냈는지 알려 주고, 왜 다시 시작해야 하는지 설명해야 한다. 프로토콜을 설계할 때는 메시지 타입부터 작게 나누는 것이 좋다.
예를 들어 metadata, chunk, ack, resume-request, complete, error 정도만 있어도 생각이 정리된다. 각 메시지에는 transfer id를 넣어 여러 파일이나 재시도 상황이 섞이지 않게 한다. 이 구조가 없으면 로그를 봐도 어느 파일의 어느 조각이 실패했는지 알기 어렵다. 관련 글로는 [과 [을 남긴다. 두 글을 같이 보면 WebRTC 연결과 파일 프로토콜을 따로 봐야 하는 이유가 더 분명해진다.

섞기 쉬운 인접 개념
무결성 검사는 제품 신뢰와 연결된다. 작은 파일 하나가 깨져도 사용자는 네트워크 구현의 사정을 봐주지 않는다. 전체 checksum을 마지막에 확인할 수도 있고, chunk별 checksum을 두어 중간 재전송을 쉽게 만들 수도 있다. 어떤 방식을 택하든 실패 이름을 남겨야 한다. 연결 끊김, checksum 불일치, 저장 실패, 사용자 취소는 서로 다른 사건이다. PonsWarp가 DataChannel 위에 별도 파일 전송 프로토콜을 고민한 이유도 여기에 있다.
DataChannel은 재료이고, 제품은 metadata, chunk, ACK, backpressure, writer 전략을 함께 설계해야 한다. PonsLink의 DataChannel 사례도 같은 방향으로 읽힌다. 연결이 열렸다는 사실만으로 파일 전송 기능이 완성되지 않는다. 사용자가 믿고 큰 파일을 보낼 수 있으려면 실패와 재개까지 제품 언어로 내려와야 한다. 마지막으로 DataChannel 파일 전송은 네트워크와 저장소가 동시에 느려질 수 있다는 사실을 인정해야 한다.
연결이 좋아도 디스크 쓰기가 느리면 전송을 늦춰야 하고, 저장이 빨라도 relay 경로가 느리면 네트워크에 맞춰야 한다. 둘 중 하나만 보면 병목을 잘못 짚는다. 그래서 전송 프로토콜은 bytes를 보내는 코드가 아니라 속도를 맞추고 상태를 설명하는 코드에 가깝다. 로그에는 transfer id, selected candidate type, chunk size, bufferedAmount 변화, ACK 지연, 재전송 횟수, 저장 실패 사유를 남긴다. 화면에는 그중 일부만 번역해 보여 준다.
"상대 기기가 저장하는 속도에 맞추는 중", "연결이 끊겨 이어받기를 준비하는 중", "현재 브라우저에서는 큰 파일 저장 방식이 제한됩니다" 같은 문장이 기술명보다 낫다. 사용자가 알아야 하는 것은 내부 계층이 아니라 지금 기다려도 되는지와 무엇을 선택할 수 있는지다. 중단 후 재개는 처음부터 범위를 정해야 한다. 모든 파일과 모든 브라우저에서 완벽한 resume을 약속하기 어렵다면 제한을 밝히는 편이 낫다.
transfer id와 file id, chunk offset, 마지막으로 확인된 ACK가 있어야 이어 보내기가 가능하다. 이 정보가 없으면 중간에 한 번 끊긴 대용량 파일은 처음부터 다시 보내야 한다. 사용자 입장에서는 기술적으로 어쩔 수 없다는 설명보다 왜 다시 시작해야 하는지, 다른 경로가 있는지가 더 중요하다.

DataChannel이 지키는 것
운영 지표는 연결 성공률보다 더 세밀해야 한다. 평균 속도, p95 전송 시간, 중단률, resume 성공률, checksum 실패율, 저장 실패율을 같이 본다. selected candidate type도 함께 남기면 직접 경로와 relay 경로의 품질 차이를 볼 수 있다. 사용자가 느린 이유가 네트워크인지, relay인지, 수신 저장인지 구분해야 다음 개선을 정할 수 있다. 다시 볼 질문은 네 가지다. transfer id와 file id를 두었는가. chunk offset과 sequence를 기록하는가. ACK와 재전송 기준을 정했는가. 수신 저장 전략을 브라우저별로 나누었는가. 여기에 bufferedAmount 기준과 checksum 정책까지 답할 수 있으면 DataChannel을 파일 API로 착각하지 않고, 그 위에 필요한 제품 프로토콜을 올리고 있는 셈이다.
DataChannel 파일 전송도 작은 텍스트 파일로만 보면 너무 쉬워 보인다. 실제로는 수백 MB 파일, 느린 디스크, 절전 모드, 브라우저 탭 전환, 모바일 네트워크가 들어온다. 채널이 열렸는지보다 오래 버티는지, 중간에 멈췄을 때 복구 가능한지가 더 중요하다. DataChannel 파일 전송을 설명할 때 가장 조심할 말은 "그냥 보내면 된다"다. 작은 텍스트 메시지는 그렇게 보여도 파일은 다르다.
파일은 이름과 크기, 저장 위치, 진행률, 취소, 재개, 검증을 기대한다. 사용자는 파일 앱이나 메신저에서 이미 그런 경험을 해 왔다. DataChannel 위에 이 기대를 새로 설계하지 않으면 기술 데모는 되지만 제품 기능은 비어 있다. DataChannel 전송 화면에서는 내부 용어보다 파일 상태가 중요하다. 조각을 보내는 중인지, 재시도 중인지, 수신 저장을 기다리는지 사용자가 알 수 있어야 한다.
로그에는 channel state와 bufferedAmount, chunk offset, 저장 완료 이벤트가 남아야 한다. 그래야 “전송 중”이라는 말이 실제 상태와 연결된다. 브라우저 저장은 제품에서 가장 현실적인 제약이다. 작은 파일은 메모리에 모았다가 Blob으로 내려받게 할 수 있다. 큰 파일은 그 방식이 위험하다. File System Access API를 쓸 수 있는 브라우저와 그렇지 않은 브라우저를 나누고, 지원하지 않는 환경에서는 크기 제한이나 cloud fallback을 제공해야 한다.
기술적으로 가능한 경로와 제품이 약속할 경로는 다르다. DataChannel를 설명할 때 서버/클라이언트/사용자 중 누구 문장으로 말하는지 먼저 고르면 논쟁이 줄었다.
API 관점에서 본 DataChannel
chunk 크기는 한 번 정하면 끝나는 값이 아니다. 너무 작으면 오버헤드가 커지고, 너무 크면 버퍼와 메모리 부담이 커진다. 네트워크와 브라우저, 저장 방식에 따라 적당한 값이 달라진다. 처음에는 보수적인 크기로 시작하고 bufferedAmount와 ACK 지연을 보며 조절하는 편이 안전하다. 중요한 것은 빠른 수치보다 멈추지 않는 흐름이다. 진행률 UI는 사용자 신뢰를 크게 좌우한다.
송신률만 빠르게 올라가다가 마지막에 오래 멈추면 사용자는 속았다고 느낀다. 수신 저장과 검증 시간을 별도 단계로 보여 주거나, 최소한 "저장 확인 중" 같은 문구를 두는 편이 낫다. 특히 큰 파일에서는 99%에서 멈춘 듯 보이는 시간이 길어질 수 있다. 이 시간을 설계하지 않으면 기술적으로는 정상이어도 경험은 나쁘다. 이렇게 보면 DataChannel 파일 전송은 작은 전송 시스템을 브라우저 안에 만드는 일이다.
채널을 여는 코드는 시작일 뿐이고, 사용자에게 필요한 기능은 그 위의 규칙에서 나온다. 파일을 안전하게 나누고, 확인하고, 저장하고, 실패했을 때 이어 가는 약속을 세워야 비로소 "파일 전송"이라고 부를 수 있다. 특히 DataChannel에서는 재현 조건을 같이 적어야 의미가 있었다. 호환성 문서도 필요하다. 어떤 브라우저에서 큰 파일 저장이 안정적인지, 어떤 환경에서는 저장 방식이 제한되는지, 모바일에서는 백그라운드 전송을 어디까지 기대할 수 있는지 적어야 한다.
DataChannel이 표준이라고 해서 파일 전송 제품의 모든 동작이 브라우저마다 똑같다는 뜻은 아니다. 제한을 문서화하면 제품이 약해 보이는 것이 아니라 예측 가능해진다.
브라우저가 주는 제약
WebRTC DataChannel은 브라우저 사이에서 임의의 데이터를 주고받는 채널이다. 내부적으로 SCTP/DTLS/UDP 계층 위에 올라가고 ordered, reliable 같은 옵션을 선택할 수 있다. 이 설명만 보면 충분해 보이지만 파일 전송에는 부족하다. 채널은 도로이고 파일 전송은 그 도로 위에서 어떤 상자를 어떤 순서로 보내고, 도착을 어떻게 확인하고, 실패하면 어디서 다시 시작할지 정하는 일이다.
보안과 개인정보도 빠지면 안 된다. 파일 이름과 크기 같은 metadata는 편의를 위해 필요하지만 노출 범위를 생각해야 한다. 방 권한이 없는 사용자가 metadata를 볼 수 없어야 하고, transfer id는 추측하기 어려워야 한다. DataChannel이 피어 간 통로라고 해서 애플리케이션 레벨 인증과 권한 확인이 사라지는 것은 아니다. 팀에서 DataChannel 파일 전송을 설명할 때는 “채널”과 “파일 프로토콜”을 분리해 말하는 것이 좋다.
채널은 메시지를 옮기는 통로이고, 파일 프로토콜은 그 메시지가 어떤 조각이며 어디에 저장될지 정한다. 이 둘을 섞으면 전송이 열렸다는 로그만 보고 기능이 완성됐다고 착각하기 쉽다. 특히 DataChannel에서는 재현 조건을 같이 적어야 의미가 있었다. DataChannel을 다시 읽을 때는 API 이름보다 애플리케이션 프로토콜이 남아 있는지 확인한다. 파일 이름, chunk 번호, checksum, 재시도 기준, 저장 완료 상태가 없다면 파일 전송 기능은 아직 완성되지 않았다.
채널이 열렸다는 사실은 시작일 뿐이고, 사용자가 믿을 수 있는 전송은 그 위의 약속에서 만들어진다. 가장 먼저 나눌 것은 metadata와 payload다. 수신자는 파일 이름과 크기, transfer id, file id, 전체 chunk 수, checksum 정보를 먼저 알아야 한다. 그래야 진행률을 계산하고 저장 공간을 준비하고, 중간에 끊겼을 때 어느 파일의 어느 위치부터 다시 받을지 판단한다. payload만 계속 보내면 작은 데모는 통과할 수 있지만 큰 파일이나 중단 상황에서 바로 막힌다.
'[DataChannel 딥다이브 WebRTC DataChannel은 파일 전송 API가 아니다'을 관통하는 질문은 이 섹션에서도 같았고, 답만 더 구체적이 됐다.
설계할 때 던져야 할 질문
동시에 여러 파일을 보낼 때는 우선순위와 취소가 필요하다. 모든 파일을 같은 채널과 같은 버퍼에 밀어 넣으면 큰 파일 하나가 작은 파일을 막을 수 있다. transfer id별 상태를 나누고, 사용자가 특정 파일만 취소할 수 있게 해야 한다. 취소 메시지도 상대에게 전달되어야 수신자가 불필요한 저장 작업을 멈춘다. 작은 기능처럼 보이지만 실제 사용에서는 자주 만나는 장면이다.
저장 전략도 브라우저마다 다르게 봐야 한다. 메모리에 다 모았다가 다운로드시키는 방식은 작은 파일에서는 간단하지만 큰 파일에서는 위험하다. Stream API나 File System Access API를 쓸 수 있는 환경과 그렇지 않은 환경을 나눠야 한다. 지원하지 않는 브라우저에서는 파일 크기 제한을 두거나 cloud fallback을 제안하는 것이 더 정직하다. 제한을 숨기면 실패 순간에 제품이 거짓말한 것처럼 보인다.
어떤 값을 기록할지, 어떤 제한을 문서화할지, 어떤 fallback을 정상 흐름으로 볼지 정한다. 이 정도까지 내려오면 기술 약어가 제품의 약속으로 바뀐다. 반대로 여기까지 내려오지 않으면 글은 그럴듯하지만 다음 장애 때 다시 검색창을 열게 된다. ordered와 unordered 선택도 파일 전체 기준으로만 보면 헷갈린다. 파일 바이트를 순서대로 쓰는 구조라면 ordered가 편하다.
하지만 chunk마다 offset이 있고 수신자가 위치를 맞춰 쓸 수 있다면 일부 상황에서는 unordered도 검토할 수 있다. 다만 순서가 자유로워질수록 애플리케이션 프로토콜의 책임이 커진다. 어떤 선택이든 저장, ACK, checksum 설계와 같이 봐야 한다. 처음 구현 범위를 줄이는 것은 괜찮다. 다만 줄인 범위를 명확히 해야 한다. 예를 들어 2GB 이상은 cloud fallback을 권한다거나, 특정 모바일 브라우저에서는 백그라운드 전송을 보장하지 않는다고 적을 수 있다.
제한을 적는 일은 기능을 약하게 만드는 것이 아니라 사용자가 실패를 예측하게 해 준다. 파일 전송에서는 예측 가능성이 속도만큼 중요하다. 진행률도 조심해야 한다. 송신자가 bytesSent를 올렸다고 해서 수신자가 디스크에 안전하게 저장했다는 뜻은 아니다. 브라우저 버퍼에 쌓였을 수도 있고, 수신 쪽 writer가 느려 밀리고 있을 수도 있다. 그래서 progress는 송신 기준과 수신 확인 기준을 나눠 보는 편이 좋다.
사용자에게는 단순한 퍼센트를 보여 주더라도 내부에서는 sent, received, written, verified 상태를 분리해야 한다.
마치며
이 글에서 확인한 경계는 다음 변경의 기준으로 남겼다.