Jungle

[TIL] WEB RTC 구현 기록 - FE

손가든 2024. 2. 29. 15:55

나만무 프로젝트를 진행하면서 webRTC를 이용하여 음성채팅 기능을 구현하기 위해 공부했다.

 

프로젝트에서 백엔드 개발을 담당했지만, webRTC를 활용하여 음성채팅을 기능화하기 위해 프론트 작성도 담당하기 위해 따로 공부했다.

 

리액트에서 발생하는 버그들을 고치기 위해 javascript에서의 리액트 마운트 순서를 공부했고,

리액트 훅의 사용법과 상하위 컴포넌트간의 관계에 대해 공부했다.

 

이 기능이 가능할 수 있게 해주는 원리에 대해 머리속에 복잡한 내용을 A4용지에 쓰던걸 여기에 적도록 하겠다.

 


 

WEB RTC 란?

 

WEBRTC는 기본적으로 서버가 중간에 개입하지 않고 peerTopeer로 연결하여 음성 채팅과 같은 스트림 전송을 수행할 수 있게 한다.

 

이를 위해선 Peer - Peer가 서로의 웹 세팅, 브라우저 버전 등과 같은 호환 문제를 서로 합의보기 위해 SDP 정보를 주고받는데,

WEBRTC는 이 SDP의 생성 및 전송을 돕는다.

 

하지만 직접적인 SDP의 전송은 Signaling Server를 통해 이루어져야 한다.

 

출처 : https://www.wowza.com/blog/webrtc-server-what-it-is-and-why-you-need-one

 

WebRTC를 공부하다 보면 수많은 포스팅을 통해 연결된 구성도를 사진으로 여럿 접할 수 있는데,

내가 본 사진 중 이 사진이 연결 과정 및 구성을 가장 잘 표현했다고 생각했다.

 

보라색으로 주고받는 SDP는 offer , answer ,ice candidate 총 세가지가 있다.

이 SDP는 WebRTC가 제공하는 메소드를 통해 로컬에서 생성할 수 있다.

 

Signaling Server는 시그널링 과정을 자동화해주는 Twilio , Google firebase 과 같은 툴이 있다고 알고 있는데,

나는 그냥 버그가 생기면 디버깅하기 쉽기도 하고, 직접 해봐야 과정을 확실히 이해할 수 있다고 생각해서 우리 프로젝트의 Spring 서버의 웹소켓을 사용하여 Peer 끼리 SDP를 주고받도록 설계했다.

 

STUN 서버는 google에서 제공해주는 프로토콜 기반 서버로 Peer(클라이언트)에게 자신이 어떤 NAT 네트워크에 존재하는지에 대해 알려준다.

 

하지만 STUN 서버는 같은 NAT 네트워크에 존재하는 컴퓨터끼리만 연결을 지원해서 이후 Turn 서버를 인스턴스로 세팅하여 다른 WIFI 에서 접속하는 컴퓨터끼리도 연결될 수 있도록 했다.

(하지만 TURN 서버도 거리가 매우 멀 경우에 정상적으로 통신되지 않는 문제가 있어서 해당 문제는 추후 공부하여 해결할 예정이다.) 


WEB RTC PeerToPeer 연결

 

클라이언트 간 연결을 위해서 일련의 과정들

 

1. 웹캠 및 마이크 접근 권한 허용:

  • 필요성: 음성채팅을 위해서는 사용자의 웹캠 및 마이크에 접근해야 합니다.
  • 과정: 사용자가 작업실에 들어갈 때, 웹캠 및 마이크의 접근 권한을 요청하고, 허용한 경우 웹캠과 마이크에 대한 스트림을 가져옵니다.

2. WebSocket 연결:

  • 필요성: WebRTC 연결을 위한 시그널링 서버와의 통신을 위해 WebSocket 연결이 필요합니다.
  • 과정: 사용자가 작업실에 들어가면, WebSocket을 통해 시그널링 서버에 연결합니다.

3. Offer 생성 및 브로드캐스팅:

  • 필요성: 연결하고자 하는 상대방에게 연결을 요청하기 위해 Offer를 생성하고 브로드캐스팅해야 합니다.
  • 과정: 시그널링 서버에게 생성한 Offer를 송신하고, 해당 Offer를 다른 사용자에게 브로드캐스팅합니다.

4. Answer 생성 및 브로드캐스팅:

  • 필요성: Offer를 받은 상대방은 Answer를 생성하여 응답해야 합니다.
  • 과정: 시그널링 서버에게 Offer 받은 사용자가 Answer 생성하고, 해당 Answer Offer 보낸 사용자에게 브로드캐스팅합니다.

5. Ice Candidate 교환:

  • 필요성: 네트워크 환경에 따라 P2P 연결을 수립하기 위한 네트워크 정보를 교환해야 합니다.
  • 과정: Offer Answer 교환이 완료되면, 사용자는 자신의 네트워크 정보(Ice Candidate) 상대방에게 전송하고, 서로의 Ice Candidate 교환합니다.

6. P2P 연결 수립:

  • 필요성: 최종적으로 P2P 연결을 수립하여 양쪽 사용자 간에 데이터 및 미디어 스트림을 교환할 수 있게 됩니다.
  • 과정: Ice Candidate 교환이 완료되면 WebRTC P2P 연결을 수립하고, 데이터 미디어 스트림을 교환합니다.

7. 오디오 컴포넌트 탑재 및 사용:

  • 필요성: P2P 연결이 수립되면 음성채팅을 위한 오디오 컴포넌트를 탑재하고 사용할 수 있습니다.
  • 과정: P2P 연결이 수립된 , 사용자는 RTC 연결을 통해 오디오를 송수신하며, 이를 오디오 컴포넌트에서 재생하거나 전송합니다.

8. 작업실에서 나갈 경우 RTC 및 WebSocket 연결 종료:

  • 필요성: 작업실을 나가면 RTC 연결 및 WebSocket 연결을 종료하여 자원을 해제해야 합니다.
  • 과정: 작업실을 나가는 경우, RTC 연결 WebSocket 연결을 종료하고 필요한 정리 작업을 수행합니다.

 

ice candidate는 offer -> answer -> ice candidate의 과정 중 가장 마지막에 전송하게 되는데,

그 과정이 매우 추상적이라 처음엔 연결하기 전엔 어떻게 돌아가는지 잘 파악하지 못했었다.

 

이 과정은 'remoteDescription()'이라는 webRTC 메소드를 통해 상대방의 offer나 answer를 저장할 때,

상대방에게 ice candidate 이벤트를 일으킨다.

 

이 이벤트를 듣고 있는 서로는 각자의 ice candidate도 전달하도록 코드를 구현하여 해당 offer - answer - ice candidate 절차를 수행할 수 있었다.


 

 

LocalStream On

 

작업실에 진입 시 클라이언트의 localStream을 On한다.

 

이 행위는 클라이언트의 마이크를 허용하는 절차이다.

 

이후 백엔드의 stomp websocket으로 이 작업실에 join했음을 알린다.

그러면 백엔드에서는 작업실에 참여중인 유저들의 아이디를 반환해줌으로써,

특정 클라이언트가 Offer과 answer를 특정 유저에게 1대1로 통신할 수 있도록 했다.

 

HandleJoin

 

백엔드로부터 join의 웹소켓 응답을 기다리는 subscribe 함수 선언 단이다.

 

작업실에 참여중인 모든 유저를 리스트로 받아 CreatePeerConnection 함수로 생성한 객체를 통해 생성한 offer sdt 정보를 모든 유저에게 각각 전송한다.

 

이때, 전송하는 데이터의 Nickname은  ui로 렌더링되는 유저의 이름을 해당 유저의 voice Stream과 일치시키기 위함이다.

이는 특정 유저를 mute , volume의 크기를 listen할 수 있어서 stream 제어를 nickname 버튼으로 가능하게 했다.

 

 

 

CreatePeerConnection

 

이 함수는 작업실 내에 참여한 client의 connection 객체를 생성하는 역할을 한다.

 

이 함수에서 직접적으로 WebRTC 외부 함수 및 Stun, Turn 서버를 사용하여 나의 객체를 선언하고, 이 객체를 통한 offer, answer를 생성한 뒤 client끼리 주고받으면서 직접 P2P 위치를 저장하게 되고, 결과적으로 중간 서버 없이 stream 공유가 가능하게 된다.

 

그리고 참고로, Stun 서버만 있어도 문제없이 작동된다고 하여서 개별적인 turn 서버 없이 코드를 짜 구현했더니 다른 NAT 네트워크의 client끼리는 연결을 지원하지 않아서 Turn 서버를 이후에 붙였다.

 

offer과 answer가 client(=peer)에게 서로 전달되어 Remote & localDescription 함수를 통해 각각 서로의 정보를 저장하면 ice candidate가 상대방에게 event 호출되는데, 이 함수에서는 event를 listen하도록 하여 상대방에게 iceCandidate를 전달하고 최종적으로 stream은 event로 각자의 로컬에 전달된다.

 

 

 

 

HandleOffer

수많은 console.log의 흔적들..

 

웹 소켓 전송이 제대로 이루어지지 않아 머리속의 이론을 기반으로 한 코드를 짜놓고는 3일동안 디버깅만 진행했다..

가장 오랫동안 헤맸던 문제는 웹소켓 publish의 destination url 맨 앞에 '/' 를 붙이지 않아서 발생한 문제였다.

 

아무튼 HandleOffer는 작업실에 이미 접속해있는 클라이언트 기준으로 누군가 접속하여 나한테 Offer를 보냈을 경우 수행된다.

 

함수에서는 받은 Offer를 저장한 뒤, Answer를 생성하여 보낸 유저에게 웹 소켓으로 전달한다.

 

 

HandleAnswer

 

이 함수를 통해 음성 채팅 접속자는 바로 위의 offer를 받아 answer를 전송한 기존 접속자의 answer를 처리한다.

 

setRemoteDescription 함수를 통해 answer를 저장하면 ice Candidate event가 각자에게 전송되며, 이는 아까 CreatePeerConnection 함수에서 등록한 event를 통해 iceCandidate가 상대방에게 전송된다.

 

 

 

HandleIceCandidate

 

마지막으로 iceCandidate를 전송받았을 때 직접적으로 P2P가 연결이 시작되는 마지막 과정이다.

 

서로의 ice Candidate는 "내 네트워크의 위치는 이곳이에요!" 라는 정보를 가진다.

 

따라서 서로는 addIceCandidate 함수를 통해 서로의 위치를 저장하고 그 네트워크로 직접적인 통신을 수행하게 된다.

 

 

 


 

여기까지 React의 Component 내부에서 WEB RTC Signal을 주고받는 코드를 리뷰했다.

 

짧은 시간 내에 구현해야 했는데, WebRTC 연결 과정을 구체적으로 이해하고 있지 않으면 절대 코드를 짤 수 없어서 구현을 위해 학습하는데 애를 많이 썼다.

 

게다가 React.js에 대한 이해도 부족했던 터라, 프론트엔드 팀원들이 설계 해놓은 컨테이너 계층의 이해와 리액트 훅으로 전달 받은 상태 변수가 비동기 처리로 인해 마운트 순서와 함께 보장받지 않는 문제들 때문에 어려움이 많았다.

 

하지만 최종적으로 잘 구현을 마무리했고, 이 과정을 통해 학습한 내용이 많아 뿌듯함을 느꼈다.

 

 

컴포넌트 계층의 설계가 복잡하여 RTC 코드를 끼워넣다 보니 조금 코드 구조가 깔끔하지 못한데 이는 현재 리팩토링중이어서 추후 관련된 학습을 통해 포스팅하겠다.