카테고리 없음

WebRTC

gudwns5533 2026. 7. 8. 17:47

WebRTC란?

Web Real-Time Communication의 약자로, 브라우저나 앱에서 실시간 음성, 영상, 화면 공유, 파일/데이터 전송을 구현할 수 있게 해주는 기술을 말한다. Zoom, Google Meet, Discord 음성/영상 통화 같은 기능을 웹에서 만들 수 있게 해주는 기술이라 생각하면 된다.

WebRTC의 기능

기능 예시
화상 통화 1:1 영상 통화, 온라인 수업
음성 통화 브라우저 기반 전화, 음성 채팅
화면 공유 회의 화면 공유, 원격 지원
실시간 데이터 전송 채팅, 파일 전송, 게임 상태 동기화
AI 서비스 연동 웹캠 영상 기반 자세 분석, 손 인식, 얼굴 인식

WebRTC의 핵심 개념

mediaStream

카메라, 마이크, 화면 공유 같은 미디어 데이터 흐름을 말한다.

navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true
});

 

웹캠을 켜면 브라우저는 카메라 영상을 MediaStream으로 가져온다. 이렇게 가져온 영상은 <video> 태그에 보여주거나, 다른 사용자에게 전송할 수 있다.

RTCPeerConnection

WebRTC의 가장 핵심 객체이다.

두 사용자, 즉 Peer A와 Peer B를 연결해서 영상, 음성, 데이터를 주고받게 해주는 객체이다.

const peerConnection = new RTCPeerConnection();

 

RTCPeerConnection을 로컬 컴퓨터와 원격 피어 사이의 WebRTC 연결을 나타내며, 효율적인 데이터 스트리밍을 처리하는 인터페이스로 설명한다. 여기에 카메라 영상, 마이크 음성, 데이터 채널 등을 붙여서 상대방과 통신한다.

즉, RTCPeerConnection의 역할은 다음과 같다.

  • 내 영상/음성 트랙을 연결에 추가
  • 상대방 영상/음성 트랙 수신
  • Offer 생성
  • Answer 생성
  • ICE Candidate 수집
  • 연결 상태 관리

RTCDataChannel

영상이나 음성이 아니라 일반 데이터를 실시간으로 주고받을 때 사용한다.

WebRTC의 연결 원리

WebRTC는 최종적으로 P2P, 즉 사용자끼리 직접 연결되는 구조를 목표로 한다. 하지만 처음 연결할 때는 서로의 정보를 교환해야 한다. 이 과정을 시그널링(Signaling)이라 한다.

사용자 A 브라우저
   │
   │ 연결 정보 교환
   ▼
시그널링 서버
   ▲
   │ 연결 정보 교환
   │
사용자 B 브라우저

이후 연결 성공

사용자 A 브라우저 ◀────────▶ 사용자 B 브라우저
        영상 / 음성 / 데이터 직접 전송

 

WebRTC는 P2P 통신을 지원하지만, 실제 연결을 조율하기 위한 메타데이터 교환에는 시그널링 서버가 필요하다. 또한 NAT나 방화벽 환경을 처리하기 위해 STUN/TURN 서버가 사용된다.

Signaling 서버란?

Signaling 서버는 WebRTC 연결을 만들기 위해 브라우저 사이에서 연결 정보를 전달하는 서버를 말한다.

Signaling 서버가 전달하는 대표적인 정보

  • Offer
  • Answer
  • ICE Candidate
  • 방 입장 정보
  • 사용자 나가기 정보

Offer와  Answer

Offer란?

Offer는 연결을 먼저 시작하는 쪽이 만드는 연결 제안 정보를 말한다.

const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);

socket.emit("offer", {
    roomId,
    offer
});

 

Offer 안에는 다음과 같은 정보가 들어간다.

  • 내가 영상/음성을 보낼 수 있는지
  • 어떤 코덱을 사용할 수 있는지
  • 어떤 해상도나 미디어 타입을 사용할 수 있는지
  • WebRTC 연결을 위한 세션 정보
  • ICE Candidate 수집을 위한 기반정보

Offer는 보통 SDP(Session Description Protocol)형식으로 만들어진다. 

Answer란?

Answer는 Offer를 받은 쪽이 만드는 응답 정보를 말한다.

B 브라우저가 A의 Offer를 받으면 먼저 Offer를 자신의 remoteDescription으로 등록한다.

await peerConnection.setRemoteDescription(offer);

 

그 후에 Answer를 만든다.

const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);

 

그리고 이 Answer를 다시 A에게 보낸다.

socket.emit("answer", {
  roomId,
  answer
});

 

즉, Answer는 Offer에 대한 수락 및 연결 조건 응답이라고 생각하면 된다.

SDP

SDP는 Session Description Protocol의 줄임말이다. Offer와 Answer안에 들어있는 실제 연결 설명 정보이다.

SDP안에는 다음과 같은 내용이 들어있다.

  • 오디오를 쓸지
  • 비디오를 쓸지
  • 어떤 코덱을 쓸지
  • 어떤 암호화 정보를 쓸지
  • 어떤 미디어 라인을 사용할지
const offer = await peerConnection.createOffer();

STUN, TURN, ICE Candidate란?

STUN 서버

사용자가 공유기나 NAT 뒤에 있을 때, 외부 인터넷에서 어떤 IP와 포트로 보이는지 알려주는 서버이다.

ICE는 두 피어를 연결하기 위한 후보 경로를 찾는 프레임워크이고, STUN은 NAT 뒤의 사용자가 자신의 공개 주소와 포트를 알 수 있게 도와준다.

const peerConnection = new RTCPeerConnection({
    iceServers: [
    	{ urls: "stun:stun.l.google.com:19302" }
    ]
});

TURN 서버

브라우저끼리 직접 연결이 안 될 때 사용하는 중계 서버를 말한다.

사용자 A ─── TURN 서버 ─── 사용자 B

 

직접 연결보다 비용과 지연시간이 더 들지만, 회사망, 학교망, 강한 방화벽 환경에서는 TURN이 필요할 수 있다.

const peerConnection = new RTCPeerConnection ({
    iceServers: [{
    	urls: "turn:your-turn-server.com:3478",
        username: "turn-user",
        credential: "turn-password"
    }]
});

ICE Candidate

Offer와 Answer만 교환한다고 연결이 되는 것이 아니다. 브라우저끼리 실제로 통신할 수 있는 네트워크 경로를 찾아야 한다.

이때 사용하는 것이 ICE Candidate이다. 쉽게 말하자면 연결 가능한 네트워크 후보군을 찾는거라 생각하면 된다.

// 보내는 코드

peerConnection.onicecandidate = (event) => {
    if (event.candidate) {
    	socket.emit("ice-candidate", {
            roomId,
            candidate: event.candidate
        });
    }
};

// 받는 코드

await peerConnection.addIceCandidate(candidate);

 

ICE Candidate는 두 브라우저가 실제로 연결 가능한 네트워크 경로 후보를 말한다. 후보는 하나가 아니라 여러 개 생길 수 있고, 양쪽 브라우저가 이 후보들을 서로 교환하면서 가장 적절한 경로를 찾게된다.

WebSocket과 WebRTC

구분 WebSocket WebRTC
구조 클라이언트 ↔ 서버 클라이언트 ↔ 클라이언트
주요 용도 채팅, 알림, 실시간 상태 업데이트 영상통화, 음성통화, 화면 공유, P2P 데이터
서버 필요성 서버가 핵심 연결 조율용 서버 필요 이후 P2P 가능
대용량 미디어 직접 구현 부담 큼 미디어 전송에 특화
지연시간 낮은 편 매주 낮은 실시간 통신 지향

전체 WebRTC 연결 흐름

[1] 웹페이지 접속
      ↓
[2] Socket.IO 서버 연결
      ↓
[3] 방 생성 / 방 입장
      ↓
[4] getUserMedia() 또는 getDisplayMedia()로 미디어 스트림 획득
      ↓
[5] RTCPeerConnection 생성
      ↓
[6] addTrack()으로 영상/음성 track 추가
      ↓
[7] Offer 생성
      ↓
[8] Offer를 시그널링 서버로 전달
      ↓
[9] 상대방이 Offer 수신
      ↓
[10] 상대방이 Answer 생성
      ↓
[11] Answer를 시그널링 서버로 전달
      ↓
[12] Offer 생성자가 Answer 등록
      ↓
[13] ICE Candidate 교환
      ↓
[14] STUN/TURN을 통해 연결 경로 확정
      ↓
[15] WebRTC 연결 성공
      ↓
[16] 영상/음성/화면/데이터 실시간 전송
      ↓
[17] 필요 시 화면 공유 track 교체 또는 DataChannel 사용
      ↓
[18] 종료 시 track.stop() + peerConnection.close()

1. 사용자가 서비스에 접속한다

사용자 A → http://localhost:3000 접속
사용자 B → http://localhost:3000 접속

 

이 단계에서는 아직 WebRTC 연결이 된게 아니다. 그냥 웹페이지에만 접속한 상태이다.

브라우저는 HTML, CSS, JavaScript를 로드하고, Socket.IO 같은 시그널링 서버와 먼저 연결된다.

const socket = io();

 

여기서 Socket.IO는 영상이나 음성을 보내는 용도가 아니라 Offer, Answer, ICE Candidate 같은 연결 정보를 전달하는 용도이다.

2. 방 생성 / 방 입장

WebRTC는 기본적으로 "누구와 연결할지"를 스스로 찾아주지 않는다.

따라서 보통 방 코드나 사용자 ID를 사용한다.

A: study123 방 생성
B: study123 방 입장

 

이때, Signaling 서버에서는 같은 방에 들어온 사용자끼리 연결 정보를 주고받도록 한다.

socket.emit("join-room", roomId);

 

서버는 같은 방에 있는 다른 사용자에게 알려준다.

socket.to(roomId).emit("user-joined");

 

이를 통해 방에 참가한 A와 B를 연결 대상으로 묶는다.

3. 카메라, 마이크, 화면 공유 스트림 준비

만약 이 서비스가 화상 채팅이라면 먼저 카메라와 마이크를 가져온다.

// 화상채팅

const localStream = await navigator.mediaDevices.getUserMedia({
  video: true,
  audio: true
});

// 화면 공유

const screenStream = await navigator.mediaDevices.getDisplayMedia({
  video: true,
  audio: false
});

 

이렇게 가져온 결과는 MediaStream이다. MediaStream 안에는 실제로는 여러 개의 MediaStreamTrack이 들어 있다.

MediaStream
 ├─ video track
 └─ audio track
 
// 화상 채팅의 경우
카메라 영상 track
마이크 음성 track

// 화면 공유의 경우
화면 영상 track

 

이 트랙들을 나중에 RTCPeerConnection에 추가해서 상대방에게 보낸다.

4. RTCPeerConnection 생성

이제 WebRTC 연결 객체를 만든다.

const peerConnection = new RTCPeerConnection({
  iceServers: [
    {
      urls: "stun:stun.l.google.com:19302"
    }
  ]
});

 

RTCPeerConnection은 두 브라우저 사이의 WebRTC 연결을 나타내는 핵심 객체이다.

여기서 iceServers에 STUN이나 TURN 서버 정보를 넣는다.

STUN:
내가 외부에서 어떤 IP/포트로 보이는지 확인

TURN:
직접 연결이 안 될 때 중계 서버 역할

5. 내 미디어 track을 PeerConnection에 추가

카메라나 마이크 스트림을 가져왔다면 그 안의 track을 WebRTC 연결에 추가한다.

// 화상 채팅의 경우
localStream.getTracks().forEach((track) => {
  peerConnection.addTrack(track, localStream);
});

// 화면 공유의 경우
screenStream.getTracks().forEach((track) => {
  peerConnection.addTrack(track, screenStream);
});

 

이 단계의 의미는 다음과 같다.

video track → 상대방 video 태그에 표시됨
audio track → 상대방 스피커로 출력됨

6. 이벤트 리스너 등록

WebRTC에서는 중요한 이벤트를 미리 등록해둔다.

 

6-1. ICE Candidate 이벤트

peerConnection.onicecandidate = (event) => {
  if (event.candidate) {
    socket.emit("ice-candidate", {
      roomId,
      candidate: event.candidate
    });
  }
};

 

이 이벤트는 브라우저가 연결 가능한 네트워크 후보를 찾을 때마다 발생한다.

 

6-2. 상대방 미디어 수신 이벤트

peerConnection.ontrack = (event) => {
  remoteVideo.srcObject = event.streams[0];
};

 

이 이벤트는 상대방의 영상이나 음성이 도착했을 때 실행된다.

상대방 video track 도착
   ↓
remoteVideo에 출력

 

6-3. 연결 상태 확인 이벤트

peerConnection.onconnectionstatechange = () => {
  console.log(peerConnection.connectionState);
};

7. Offer 생성

이제 연결을 먼저 시작하는 쪽이 Offer를 생성한다.

const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);

 

Offer는 쉽게 말하면 연결 제안서다.

“나는 영상과 음성을 보낼 수 있고,
이런 코덱과 이런 보안 방식으로 통신할 수 있어.
연결할래?”

 

Offer 안에는 SDP라는 형식의 정보가 들어간다.

SDP 안에 들어가는 대표 정보:
- 오디오 사용 여부
- 비디오 사용 여부
- 코덱 정보
- 미디어 방향
- 암호화 관련 정보
- ICE 관련 정보

 

Offer를 만든 뒤에는 반드시 setLocalDescription()을 호출한다.

await peerConnection.setLocalDescription(offer);

8. Offer를 시그널링 서버로 전달

Offer는 WebRTC가 자동으로 상대방에게 보내주지 않는다. 직접 서버를 통해 전달해야 한다.

socket.emit("offer", {
  roomId,
  offer
});

 

서버는 같은 방의 상대방에게 이 Offer를 전달해준다.

socket.on("offer", ({ roomId, offer }) => {
  socket.to(roomId).emit("offer", offer);
});

 

이 과정의 흐름은 다음과 같다.

A 브라우저
   ↓ offer
시그널링 서버
   ↓ offer
B 브라우저

9. 상대방이 Offer를 받는다

B 브라우저는 A가 보낸 Offer를 받는다.

socket.on("offer", async (offer) => {
  await peerConnection.setRemoteDescription(offer);
});

 

여기서 setRemoteDescription()은 "상대방의 연결 정보는 이 offer야" 라는 의미를 갖는다.

즉, B의 입장에서는

localDescription = 내 정보
remoteDescription = 상대방 정보

10. Answer 생성

B는 Offer를 받은 뒤 Answer를 만든다.

const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);

 

Answer는 연결 응답서이다. 그리고 이 Answer를 다시 A에게 보낸다.

socket.emit("answer", {
  roomId,
  answer
});

 

이 과정의 흐름은 다음과 같다.

B 브라우저
   ↓ answer
시그널링 서버
   ↓ answer
A 브라우저

11. A가 Answer를 받는다

A는 B가 보낸 Answer를 받는다.

socket.on("answer", async (answer) => {
  await peerConnection.setRemoteDescription(answer);
});

 

이 단계가 끝나면 Offer / Answer 주고받기는 완료된다.

현재 까지를 정리하자면

A:
createOffer()
setLocalDescription(offer)
offer 전송

B:
setRemoteDescription(offer)
createAnswer()
setLocalDescription(answer)
answer 전송

A:
setRemoteDescription(answer)

12. ICE Candidate 교환

Offer / Answer가 "통신 조건 협상"이라면, ICE Candidate는 "실제 연결 경로 찾기"이다.

예를 들어 두 사용자가 각자 공유기 뒤에 있을 수 있다.

A 내부 IP: 192.168.x.x
B 내부 IP: 192.168.x.x

 

이 내부 IP는 인터넷에서 바로 접근할 수 없다. 그래서 WebRTC는 STUN 서버를 통해 외부에서 보이는 주소를 찾는다.

A:
내가 외부에서는 211.xxx.xxx.xxx:53000으로 보이는구나.

B:
나는 외부에서 118.xxx.xxx.xxx:62000으로 보이는구나.

 

이런 후보 주소들을 ICE Candidate라고 한다. 브라우저가 후보를 찾으면 onicecandidate 이벤트가 발생하고, 이 후보를 상대방에게 보낸다.

peerConnection.onicecandidate = (event) => {
  if (event.candidate) {
    socket.emit("ice-candidate", {
      roomId,
      candidate: event.candidate
    });
  }
};

 

상대방은 받은 후보를 추가한다.

socket.on("ice-candidate", async (candidate) => {
  await peerConnection.addIceCandidate(candidate);
});

13. ICE가 최적의 경로를 선택한다

ICE는 여러 후보 중에서 실제로 연결 가능한 경로를 테스트한다.

후보 1: 같은 네트워크 내부 연결
후보 2: 공인 IP를 통한 직접 연결
후보 3: TURN 서버 중계 연결

 

가능하다면 직접 P2P 연결을 선택한다.

A 브라우저 ───────────── B 브라우저

 

직접 연결이 안 되면 TURN 서버를 거쳐 연결한다.

A 브라우저 ───── TURN 서버 ───── B 브라우저

 

즉, WebRTC가 항상 100% 직접 P2P로만 연결되는 것은 아니다. 네트워크 환경에 따라 TURN 서버를 중계 경로로 사용할 수 있다.

14. 연결 성공 후 미디어 전송 시작

연결이 성공하면 실제 영상, 음성, 화면 공유 데이터가 흐르기 시작한다.

A의 카메라 영상 ─────▶ B의 remoteVideo
A의 마이크 음성 ─────▶ B의 스피커

B의 카메라 영상 ─────▶ A의 remoteVideo
B의 마이크 음성 ─────▶ A의 스피커

 

이때 중요한 점은 시그널링 서버는 더 이상 영상 / 음성 데이터를 중계하지 않는다는 점이다. 시그널링 서버는 처음 연결 정보를 전달하는 역할이고, 실제 미디어는 WebRTC 연결을 통해 전달된다.

단, TURN 서버를 사용하는 경우에는 실제 미디어가 TURN 서버를 통해 중계된다.

15. DataChannel이 있다면 데이터도 전송 가능

화상 / 음성 뿐만 아니라 RTCDataChannel을 사용하면 텍스트나 파일 같은 일반 데이터도 보낼 수 있다.

16. 화면 공유를 시작할 때

이미 화상 채팅이 연결된 상태에서 화면 공유를 시작하는 경우가 있을 수 있다. 처음에는 카메라 track을 보내가 있다고 가정해보면,

카메라 video track → 상대방에게 전송 중

 

이때, 화면 공유를 시작하면 getDisplayMedia()로 화면 track을 가져온다.

const screenStream = await navigator.mediaDevices.getDisplayMedia({
  video: true,
  audio: false
});

 

그리고 기존 video sender의 track을 교체한다.

await videoSender.replaceTrack(screenTrack);

 

그러면 상대방은 같은 remoteVideo에서 이제 카메라 대신 화면 공유를 보게 된다.

기존:
상대방이 내 카메라 영상 시청

화면 공유 후:
상대방이 내 화면 공유 시청

 

이 방식은 보통 재협상 없이 작동하게된다. 즉, 새 Offer / Answer 없이도 카메라 영상 track을 화면 공유 track으로 교체할 수 있다.

17. 연결 종료

사용자가 나가거나 통화를 종료하면 다음 처리를 하게된다.

localStream.getTracks().forEach((track) => {
  track.stop();
});

peerConnection.close();

 

이 코드의 역할을 다음과 같다.

1. 카메라 track 종료
2. 마이크 track 종료
3. 화면 공유 track 종료
4. PeerConnection 닫기
5. 상대방에게 나갔다고 알림
6. UI 초기화

 

👉 이 전체 과정을 요약하자면, WebRTC는 시그널링 서버로 연결 정보를 교환한 뒤, RTCPeerConnection이 ICE를 통해 실제 연결 경로를 찾고, 그 경로로 영상 / 음성 / 화면 / 데이터를 실시간 전송하는 구조라고 생각하면 된다.