
서버 경유 없이 데이터 주고 받기
WebRTC DataChannel로 실시간 미디어 상태 공유하기
이 글은 노션에서 마이그레이션된 글입니다.
섬네일
DataChannel이란?
DataChannel은 WebRTC의 기능 중 하나로 수립된 RTCPeerConnection 사이에 DataChannel을 열고 해당 채널을 통해 서로 임의의 데이터를 주고받을 수 있게 해주는 기능이다.
공부하는 이유
WebRTC 화상회의를 구현했지만, 유저가 비디오를 끄거나 마이크를 끄고 켜는 것에 대한 상태 공유가 이루어지지 않고 있었다.
내가 마이크를 꺼도 상대방은 내가 마이크를 끈건지 말을 안하고 있는건지 분간이 안갈 수 있다고 생각되어 이런 기능을 필수로 구현해야겠다고 생각했다.
DataChannel의 주요 특징
- 저지연 데이터 전송
- 신뢰성 있는 전송 모드 지원 (reliable/unreliable)
- 양방향 통신
- 다양한 데이터 타입 전송 가능 (문자열, 바이너리 등)
createDataChannel
데이터 채널을 생성하는 함수로, PeerConnection의 메서드이다.
const channel = pc.createDataChannel('채널의 라벨', {
여긴 옵션이 들어간다.
});
구현하기
dataChannel은 P2P 연결 간에 생성된다. 하지만 비동기 메서드이기 때문에 생성 후 바로 메시지를 보낼 수 없다. state가 open 인 경우에만 메시지를 보낼 수 있다. 그렇기 때문에 열자마자 바로 보내는 것이 안된다.
이를 해결하기 위해서는 peerConnection의 연결 생성과 종료를 위임받은 커스텀 훅에 아래 데이터 채널을 저장하는 ref 객체를 만들었다. 그리고 피어들의 미디어 상태를 저장할 수 있는 useState 상태를 만들어서 각 피어마다 미디어 상태를 저장하도록 했다.
const dataChannels = useRef<{ [peerId: string]: RTCDataChannel }>({});
const [peerMediaStatus, setPeerMediaStatus] = useState<{
[peerId: string]: {
audio: boolean;
video: boolean;
};
}>({});
그리고 화상 회의에 다른 유저가 참가하여 PeerConnection을 생성할 때 데이터채널을 열고 dataChannel ref 객체에 저장한다.
const mediaDataChannel = pc.createDataChannel("media-status", {
ordered: true,
});
mediaDataChannel.onopen = () => {
dataChannels.current[peerSocketId] = mediaDataChannel;
};
mediaDataChannel.onclose = () => {
console.log("Media data channel closed.");
};
// DataChannel 해보자
pc.ondatachannel = (event) => {
const channel = event.channel;
channel.onmessage = (e) => {
const data = JSON.parse(e.data);
console.log(data);
const { type, status } = data;
if (type === "audio") {
if (status) {
console.log("상대방의 오디오가 켜졌습니다.");
setPeerMediaStatus((prev) => ({
...prev,
[peerSocketId]: {
audio: true,
video: prev[peerSocketId].video ?? true,
},
}));
} else {
console.log("상대방의 오디오가 꺼졌습니다.");
setPeerMediaStatus((prev) => ({
...prev,
[peerSocketId]: {
audio: false,
video: prev[peerSocketId].video ?? true,
},
}));
}
} else if (type === "video") {
// 상대방의 오디오가 켜졌을 때의 처리
if (status) {
console.log("상대방의 비디오가 켜졌습니다.");
setPeerMediaStatus((prev) => ({
...prev,
[peerSocketId]: {
audio: prev[peerSocketId].audio ?? true,
video: true,
},
}));
} else {
console.log("상대방의 비디오가 꺼졌습니다.");
setPeerMediaStatus((prev) => ({
...prev,
[peerSocketId]: {
audio: prev[peerSocketId].audio ?? true,
video: false,
},
}));
}
}
};
};
이렇게 데이터 채널을 생성한 뒤에 아래 코드로 피어 간 존재하는 모든 데이터채널에 메시지를 보내는 함수를 만들어서 사용했다. 이렇게 하면 위 코드에서 onmessage 이벤트로 수신된 메시지를 처리할 수 있다.
const sendMessageToDataChannels = (message: DataChannelMessage) => {
Object.values(dataChannels.current).forEach((channel) => {
channel.send(JSON.stringify(message));
});
};
sendMessageToDataChannels({
type: "video",
status: true,
});
메시지를 통해 업데이트된 peerMediaStatus는 피어들의 비디오 컴포넌트를 렌더링할 때 사용되어 실시간으로 마이크나 비디오의 상태를 서버를 경유하지 않고 공유할 수 있게 되었다!
결과
WebRTC 데이터채널을 활용하여 화상 스터디를 개선하기, 한번 RTC PeerConnection이 수립되면 DataChannel을 통해 서버 경유 없이도 피어 간 직접 통신이 가능한 점을 활용했다. 이를 통해 팀원들의 비디오/마이크 상태 변경을 실시간으로 공유할 수 있게 되었다!
미디어온오프 상태공유.gif
