프론트엔드

[WEB-RTC] WEB-RTC Custom Hook 구현 도전기 - 2 (feat. turn server)

CandDoIt 2025. 4. 14. 23:32

안녕하세요? 오늘은 저번 포스팅에 이어서 WEB-RTC 도전기를 마저 작성해보고자합니다.

https://younghun123.tistory.com/12

 

[WEB-RTC] WEB-RTC Custom Hook 구현 도전기 - 1

안녕하세요? 오늘은 NextJS에서 web-rtc를 사용하고 이를 커스텀 훅으로 구현했던 경험을 포스팅하겠습니다.저는 4학년에 한 학기를 휴학하며 하나은행에서 주관한 부트캠프를 수료했습니다. 부트

younghun123.tistory.com

 

저번시간에는 시그널링 서버(SpringBoot)와 useRtc라는 커스텀 훅에 대해 설명했습니다. useRtc는 { startStream : () = >void, remoteStreams: MediaStream[]} 형태의 객체를 반환합니다.

 

startStream은 스트림의 시작을 제어하는 함수입니다. 한명의 클라이언트가 startStream 함수를 호출하면 중계서버(signaling server)는 /call/key 메시지를 브로드캐스팅합니다. 자신이 가지고있는 roomId와 동일한 값을 가진 클라이언트가 중계 서버를 구독하고 있다면, 중계서버는 두 클라이언트와의 만남을 주선합니다. 

 

remoteStreams는 내가 지금 통신하고 있는 상대방의 캠과 마이크로부터 전달되는 MediaStream 리스트입니다. 우리는 해당 리스트를 받아서 HTML VIDEO ELEMENT에 넣어주면 됩니다.

 

그렇다면 어떻게 사용하면 좋을지 예제를 보겠습니다! 아래는 내 화면이 상단에 오고 밑에 참여자들이 뜨도록하는 예제 컴포넌트입니다.

직관적인 이해를 위해서 로컬미디어 스트림을 가져오는 이펙트는 컴포넌트 내에 직접 작성하겠습니다.

const RtcView = () => {
  // RtcHook
  const { remoteStreams, startStream } = useRtc({ id: "room123", myKey: "userA" });
  
  // 내 비디오 화면(local)
  const [localStream, setLocalStream] = useState<MediaStream | null>(null);
  const localVideoRef = useRef<HTMLVideoElement | null>(null);

  // 컴포넌트 마운트 후 로컬 미디어 스트림을 가져오는 이펙트를 실행합니다.
  useEffect(() => {
    const getLocalStream = async () => {
      try {
        const stream = await navigator.mediaDevices.getUserMedia({
          video: true,
          audio: true,
        });
        setLocalStream(stream);
        if (localVideoRef.current) {
          localVideoRef.current.srcObject = stream;
        }
      } catch (error) {
        console.error("로컬 스트림을 가져오지 못했습니다.:", error);
      }
    };

    getLocalStream();

    return () => {
      if (localStream) {
        localStream.getTracks().forEach((track) => track.stop());
      }
    };
  }, []);

  return (
    <div className="p-4 space-y-4">
      <button onClick={startStream} className="px-4 py-2 bg-blue-500 text-white rounded">
        Start Stream
      </button>

      <video
        ref={localVideoRef}
        autoPlay
        playsInline
        muted
        className="w-64 h-36 border-2 border-blue-400 rounded"
      />

      <div className="grid grid-cols-2 gap-4">
        {remoteStreams.map((stream, idx) => (
          <video
            key={idx}
            autoPlay
            playsInline
            ref={(video) => {
              if (video) video.srcObject = stream;
            }}
            className="w-64 h-36 border rounded"
          />
        ))}
      </div>
    </div>
  );
};

 

이렇게 컴포넌트까지 모두 작성해도, WEB-RTC가 정상적으로 작동하지 않는 경우가 많습니다. 대표적인 이유는 다음과 같습니다.

 

1. 시그널링 서버와 클라이언트 서버는 둘 다 HTTPS 연결을 요구합니다.

즉, 백엔드 프론트엔드 모두 SSL 인증서를 적용한 상태로 배포해야합니다. 시그널링 서버의 경우 STOMP 프로토콜은 HTTPS 환경에서만 동작하고, 클라이언트의 경우 자신의 미디어 스트림을 가져오는 getUserMedia() 메서드 또한 HTTPS 연결이 보장되는 환경에서만 동작하기 때문입니다.

 

2. turn/stun server의 부재

상대 클라이언트에게 전송되어야하는 미디어스트림이 NAT box를 통과하지 못합니다.(NAT은 사설 IP 영역에서 공용 IP 영역으로 전화해주는 장치입니다.) 그 결과, 상대방과 직접 연결(P2P)이 불가능해집니다. 대부분의 경우, WebRTC 연결이 초기 연결 시도 단계에서 실패합니다.

 

TURN Server

TURN 서버는 데이터를 중계해주는 역할을 합니다. 따라서 클라이언트의 공인 IP가 변경되거나 방화벽, NAT(Network Address Translation) 등의 제한이 있는 환경에서도 TURN 서버를 통해 안정적으로 미디어 데이터를 주고받을 수 있습니다.

TURN 서버는 하나의 공인 IP 주소를 가지고 있으며, 이를 통해 클라이언트 간 직접 연결이 불가능할 때 중간에서 미디어를 릴레이합니다. 

 

 

 

그렇다면 turn server는 어떻게 구현할 수 있을까요? 다행히 co-turn이라는 오픈소스가 존재합니다. 우리는 AWS EC2 ubuntu 인스턴스 환경에 co-turn을 설치하여 NAT을 우회하는 turn 서버를 만들수 있습니다.

 

먼저 EC2에 coturn을 설치합니다.

sudo apt-get update
sudo apt-get install coturn

 

그리고 자동 재시작 옵션을 설정한 후 turnserver.conf 파일을 확인합니다.

sudo vi /etc/default/coturn

TURNSERVER_ENABLED=1

cat /etc/turnserver.conf

 

/etc/turnserver.conf 파일에 turn 서버에대한 옵션을 설정해줍니다.

# 인증에 사용되는 도메인 또는 서버 이름 (TURN 인증에서 리얼름 값은 사용자 인증의 일부로 사용됨)
realm=[공용 아이피 주소 입력]

# 서버 이름을 정의. 로깅 및 디버깅 시 식별용 이름
server-name=turn                    

# TURN 서버가 바인딩할 내부 IP 주소 (0.0.0.0은 모든 네트워크 인터페이스에서 수신)
listening-ip=0.0.0.0              

# 외부(공용) IP 주소. NAT 환경에서 내부 IP와 외부 IP를 매핑해줌
external-ip=[공용 아이피 주소 입력]

# TURN 서버의 기본 수신 포트 (STUN/TURN 기본 포트는 UDP 3478)
listening-port=3478

# TURN 서버가 미디어 데이터 전달용으로 사용할 최소 포트 범위 (UDP)
min-port=49152                     

# TURN 서버가 미디어 데이터 전달용으로 사용할 최대 포트 범위
max-port=65535                     

# STUN 메시지에 지문(fingerprint)을 포함시켜 메시지 무결성을 확인함
fingerprint                       

# 로그 파일 경로. 서버 동작 로그를 저장하는 위치
log-file=/var/log/turnserver.log

# 로그를 상세하게 출력 (디버깅 시 유용)
verbose                            

# 사용자 인증 정보 (long-term credential 방식 사용 시 필요)
user=[username 입력]:[password 입력]

# Long-Term Credential 메커니즘 활성화 (TURN 서버 인증 방식 중 하나)
lt-cred-mech

 

마지막으로 coturn을 재시작합니다.

sudo service coturn restart

 

그리고 이곳에서 구축한 turn 서버가 잘 동작하는지 확인할 수 있습니다.

https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/

 

Trickle ICE

 

webrtc.github.io

 

이렇게 turn server를 구현했으면 useRtc에서 peerConnection객체를 생성할때 속성으로 넣어주면 됩니다.

(배포 환경에서는 환경변수로 관리해야합니다!)

const iceServers = [
  {
    urls: "구축한 turn server url",
    username: "username",
    credential: "password",
  },
];

const pc = new RTCPeerConnection({ iceServers });

 

마무리하며

WEB-RTC는 저에게 도전이었습니다. 사실 지금도 최적화 할 수 있는 부분이 존재할 수 있습니다. 하지만 관련 내용을 공부하면서 네트워크와 같은 컴퓨터 기초 지식과 코드 작성 및 디버깅의 중요함을 다시한번 느꼈습니다. 단순히 방법론되고 표준화된 개발 방식에서 벗어나 컴퓨터 및 네트워크 환경의 동작원리에 집중해서 개발했던 경험이었고, 나아가 리액트 라이브러리를 이용하여 hook을 만들었던 도전기를 꼭 작성하고 싶었습니다. 특히, API 요청에서 벗어나 코드 베이스를 통해 클라이언트간 스트리밍을 직접 제어하는 과정이 의미있었다고 생각합니다. 아직 갈 길은 멀지만 한 걸음 꾸준히 나아가겠습니다. 읽어주셔서 감사합니다!