1. 시그널링 서버를 통한 PeerConnection
📕 기본적인 시그널링 서버 및 클라이언트 연결은 시그널링 서버 연결 및 미디어 장치 연결 포스트 참고
RTCPeerConnection 의 이해가 필요 시 RTCPeerConnection 이해와 관련된 주요 인터페이스 포스트 참고
2. 시그널링 과정
두 사용자 A, B 가 피어 연결을 시도하는 과정 예시
- offer : offer는 연결을 시작하려는 피어(발신자)가 생성하는 SDP(Session Description Protocol) 정보
- answer: answer는 offer를 받은 피어(수신자)가 생성하는 SDP 정보입니다.
1️⃣ A와 B 방에 입장
- A: 방에 입장 → 시그널링 서버에 연결
- B: 뒤이어 입장 → 시그널링 서버에 연결됨
2️⃣ 서버가 A에게 B 입장 알림
- 시그널링 서버가 A에게 "B가 입장했다"는 알림 전송
3️⃣ A 측
- 새로 입장한 B 와 연결을 위한
RTCPeerConnection
생성 createOffer()
→setLocalDescription(offer)
이 시점에 ICE gathering(ICE Candidate 수집) 시작- offer를 시그널링 서버 통해 B에게 전송
4️⃣ B 측
- 기존 사용자 A 와 연결을 위한
RTCPeerConnection
생성 - 시그널링 서버를 통해 offer 전달 받음 ->
setRemoteDescription(offer)
이 시점에 ICE gathering(ICE Candidate 수집) 시작- ✅ offer 수신 시 ICE candidate는 수집 중이거나 이미 수집된 상태일 수 있음 (하위에 서술)
createAnswer()
→setLocalDescription(answer)
이 시점에서도 역시 ICE gathering(ICE Candidate 수집) 시작- answer를 시그널링 서버 통해 A에게 전송
5️⃣ A 입장
- 시그널링 서버를 통해 answer 전달 받음 ->
setRemoteDescription(answer)
✅ offer 수신 시 ICE candidate는 수집 중이거나 이미 수집된 상태일 수 있다?
📢 결론
사용자와의 연결을 위해 시그널링 서버에서 offer 수신 하는 과정 이전에 미리 ICE candidate가 도착할 수 있습니다.
📖 설명
사용자 A는 연결을 위해 offer 를 생성하고 이후 setLocalDescription
를 통해 내용을 저장하고 offer 를 다른 사용자들에게 전달하는 이 과정을 디테일하게 두가지 과정으로 분리한다면
setLocalDescription
호출 하여 ICE candidate 수집을 시작하고RTCPeerConnection
의onicecandidate
이벤트를 통해 수집된 후보를 실시간으로 시그널링 서버를 거쳐 다른 사용자들에게 ICE candidate 를 전달(Trickle ICE 방식)합니다.- 시그널링 서버에 요청하여 다른 사용자들에게 offer 를 전달합니다.
위 두 과정은 수신자 입장에서 모두 비동기적으로 처리 되므로 사용자 B는 offer를 받기 전에 ICE 후보를 먼저 받을 수도 있습니다. 문제는, 아직 setRemoteDescription
를 통해 offer 를 저장하지 않은 상태에서 미리 전달 받은ICE candidate 를 addIceCandidate
로 등록하면 오류가 발생할 수 있다는 점입니다.
Trickle ICE란?
ICE candidate를 모두 모을 때까지 기다리지 않고, 발견되는 즉시 상대 피어에 전송하여 연결 설정을 빠르게 완료하는 기술입니다. 이를 통해 미디어 통신 시작 시간이 크게 단축됩니다.
📗 해결 방법
이를 방지하기 위해 일반적으로는 offer 수신 전까지 도착한 ICE 후보들을 임시로 큐에 저장해두고, setRemoteDescription
이 완료된 뒤에 순차적으로 addIceCandidate
를 호출하는 패턴을 주로 사용합니다. 이러한 처리 방식은 연결 설정 시간 단축과 연결 무결성 보장이라는 두 가지 측면 모두에서 매우 중요합니다.
3. 구현 코드
※ 예시로 작성한 코드이며 일부 내용을 생략한 관계로 참고만 하시길 추천드립니다. ※
✅ 생략한 내용 및 추가할만한 부분
- 3인 이상의 peer 연결 및 connection 관리에 필요한 로직 (아래 예시는 간단한 1 대 1 상황을 예시로 구현)
- 클라이언트에서는 peer 의 고유한 id 를 부여, 시그널링 서버에서는 해당 id 를 기반으로 사용자 목록 사용자 및 device 상태 목록 관리
- 클라이언트에서 다른 사용자들의 Stream 및 PeerConnection 목록 관리 (시그널링 서버에서 사용자 목록도 가져와 활용 가능)
- 클라이언트에서 상위에 서술한 WebRTC의 ICE Candidate 처리 순서에 따른 에러 가능성에 따른 핸들링
3.1 클라이언트 코드
const VideoChat = () => {
// 현재 내 audio, video track 이 포함된 stream
const [myStream, setMyStream] = useState<MediaStream>(new MediaStream());
// 다른 사용자와의 연결을 위한 peerconnection
const peerConnection = useRef<RTCPeerConnection>(new RTCPeerConnection());
// 입장 함수
const enterRoom = async () => videoChatSocket.emit("enter_room");
useEffect(() => {
// 다른 사용자 입장시 실행되는 이벤트 핸들러 (offer 생성 및 전달 과정)
// 시그널링 서버에서 다른 사용자 입장시 그외 사용자들에게 "user_joined" 이벤트를 생성하는 구조
const onJoinUser = async (roomName: string) => {
// 새로 입장한 사용자와의 PeerConnection 생성
const newPC = new RTCPeerConnection();
// 로컬 미디어 트랙 추가
myStream.getTracks().forEach((track) => {
newPC.addTrack(track, myStream);
});
const offer = await newPC.createOffer(); // Offer 생성
await newPC.setLocalDescription(offer); // offer 저장
peerConnection.current = newPC; // PeerConnection 저장
// 시그널링 서버에 offer 전달
videoChatSocket.emit("send_offer", roomName, offer);
};
// 서버에서 offer 를 전달 받을 때 실행되는 이벤트 핸들러
const onReceiveOffer = async (roomName: string, offer: RTCSessionDescriptionInit) => {
const newPC = new RTCPeerConnection(); // 기존 사용자와의 PeerConnection 생성
await newPC.setRemoteDescription(offer); // 기존 사용자에게 전달받은 offer 저장
const answer = await newPC.createAnswer(); // 기존 사용자에게 전달할 answer 생성
await newPC.setLocalDescription(answer); // answer 저장
peerConnection.current = newPC; // PeerConnection 저장
// 시그널링 서버에 answer 전달
videoChatSocket.emit("send_answer", roomName, answer);
};
// 기존 사용자가 새로 입장한 사용자에 answer 전달받는 경우에 이벤트 핸들러
const onReceiveAnswer = async (answer: RTCSessionDescriptionInit) => {
await peerConnection.current.setRemoteDescription(answer); // 새로 입장한 사용자에게 전달받은 answer 저장
};
// 이벤트 핸들러 등록
videoChatSocket.on("user_joined", onJoinUser);
videoChatSocket.on("receive_offer", onReceiveOffer);
videoChatSocket.on("receive_answer", onReceiveAnswer);
// 프로세스 종료 후 이벤트 정리
return () => {
videoChatSocket.off("user_joined", onJoinUser);
videoChatSocket.off("receive_offer", onReceiveOffer);
videoChatSocket.off("receive_answer", onReceiveAnswer);
};
}, [myStream]);
return (
<div>
<button onClick={enterRoom}>입장</button>
</div>
);
};
3.2 서버 코드
// socket.io 서버 이벤트 리스너
socketServer.on("connection", (socket) => {
console.log(`🔵Client connected video chat ws server: ${socket.id}`);
// 입장
socket.on("enter_room", () => {
socket.join("videoChatRoom"); // "videoChatRoom" 라는 이름의 room 에 입장
// 다른 사용자에게 "user_joined" 이벤트 발송
socket.to("videoChatRoom").emit("user_joined");
});
// offer 전달
socket.on("send_offer", (offer: RTCSessionDescriptionInit) => {
// 다른 사용자에게 offer 와 함께 "receive_offer" 이벤트 발송
socket.to("videoChatRoom").emit("receive_offer", offer);
});
// answer 전달
socket.on("send_answer", (answer: RTCSessionDescriptionInit) => {
// 다른 사용자에게 answer 와 함께 "receive_answer" 이벤트 발송
socket.to("videoChatRoom").emit("receive_answer", answer);
});
// 연결 종료시 room 에서 퇴장
socket.on("disconnecting", () => {
socket.leave("videoChatRoom");
});
});
참고
https://lng1982.tistory.com/491026
https://github.com/w3c/webrtc-pc/issues/2519
https://webrtc.org/getting-started/peer-connections?hl=ko#trickle_ice
'websocket & webRTC' 카테고리의 다른 글
5. RTCPeerConnection 간 ICE candidate 교환 과정 (0) | 2025.04.30 |
---|---|
3. RTCPeerConnection 이해와 관련된 주요 인터페이스 (0) | 2025.04.22 |
2. 시그널링 서버 연결 및 미디어 장치 연결 (0) | 2025.04.18 |
1. 시그널링 서버 (0) | 2025.04.17 |
0. Web RTC 이해 (0) | 2025.04.16 |