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' 카테고리의 다른 글
| 로컬 WebRTC 테스트를 위한 Chrome 가상 미디어 장치 설정 (0) | 2026.03.04 |
|---|---|
| 5. RTCPeerConnection 간 ICE candidate 교환 과정 (0) | 2025.04.30 |
| 3. RTCPeerConnection 이해와 관련된 주요 인터페이스 (0) | 2025.04.22 |
| 2. 시그널링 서버 연결 및 미디어 장치 연결 (0) | 2025.04.18 |
| 1. 시그널링 서버 (0) | 2025.04.17 |