이미지 로딩 중...

WebRTC 기초와 화상회의 개념 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 20. · 2 Views

WebRTC 기초와 화상회의 개념 완벽 가이드

실시간 화상회의를 구현하고 싶으신가요? WebRTC의 기초부터 P2P 통신, NAT 문제 해결, 실제 화상회의 서비스 구현까지 초급 개발자도 쉽게 이해할 수 있도록 친절하게 안내합니다. 실무에서 바로 활용할 수 있는 코드 예제와 함께 WebRTC의 모든 것을 배워보세요.


목차

  1. WebRTC란 무엇인가?
  2. P2P 통신의 장단점
  3. NAT와 방화벽 문제
  4. STUN/TURN 서버 역할
  5. Zoom과 WebRTC 비교
  6. 브라우저 호환성 확인

1. WebRTC란 무엇인가?

시작하며

여러분이 웹사이트에서 화상통화를 하거나 친구와 영상으로 대화할 때, 이게 어떻게 가능한지 궁금해본 적 있나요? 예전에는 화상통화를 하려면 Skype 같은 별도의 프로그램을 설치해야 했습니다.

하지만 지금은 웹 브라우저만 있으면 바로 화상통화를 시작할 수 있습니다. 줌(Zoom), 구글 미트(Google Meet), 디스코드(Discord) 같은 서비스들이 모두 웹에서 작동하죠.

어떻게 이런 일이 가능해졌을까요? 바로 여기서 WebRTC가 등장합니다.

WebRTC는 웹 브라우저끼리 직접 연결해서 영상, 음성, 데이터를 주고받을 수 있게 해주는 마법 같은 기술입니다. 별도의 프로그램 설치 없이 말이죠!

개요

간단히 말해서, WebRTC는 "Web Real-Time Communication"의 약자로, 웹 브라우저에서 실시간으로 영상, 음성, 데이터를 주고받을 수 있게 해주는 기술입니다. 여러분이 친구에게 편지를 보낼 때를 생각해보세요.

일반적으로는 우체국(서버)을 거쳐야 하죠. 하지만 WebRTC는 친구 집에 직접 찾아가서 대화하는 것과 같습니다.

중간에 우체국(서버)을 거치지 않고 직접 연결되니까 훨씬 빠르고 효율적입니다. 특히 화상회의, 온라인 게임, 파일 공유 같은 실시간 소통이 필요한 곳에서 매우 유용합니다.

전통적인 방법에서는 모든 데이터가 서버를 거쳐야 했습니다. 하지만 WebRTC를 사용하면 사용자끼리 직접 연결됩니다.

서버는 처음 연결할 때만 도와주고, 실제 영상과 음성은 사용자 간에 직접 전달됩니다. WebRTC의 핵심 특징은 세 가지입니다.

첫째, 플러그인이 필요 없습니다(크롬, 파이어폭스, 사파리 등 주요 브라우저가 모두 지원). 둘째, 실시간 통신이 가능합니다(거의 지연 없이 영상과 음성 전달).

셋째, 무료로 사용할 수 있는 오픈소스 기술입니다. 이러한 특징들이 WebRTC를 현대 웹 개발에서 필수적인 기술로 만들었습니다.

코드 예제

// 간단한 WebRTC 연결 시작하기
const configuration = {
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
};

// PeerConnection 생성 - 이게 바로 친구와의 직접 연결 통로입니다
const peerConnection = new RTCPeerConnection(configuration);

// 내 컴퓨터의 카메라와 마이크 가져오기
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
  .then(stream => {
    // 내 영상을 화면에 표시
    document.getElementById('myVideo').srcObject = stream;

    // 내 영상을 상대방에게 보낼 준비
    stream.getTracks().forEach(track => {
      peerConnection.addTrack(track, stream);
    });
  });

설명

이것이 하는 일: 위 코드는 WebRTC를 사용해서 화상통화를 시작하는 가장 기본적인 과정을 보여줍니다. 마치 전화기를 들고 친구의 번호를 누르는 것처럼, 상대방과 연결할 준비를 하는 것이죠.

첫 번째로, configuration 객체를 만들어서 STUN 서버 주소를 설정합니다. 이건 마치 친구 집 주소를 찾기 위해 지도를 여는 것과 같습니다.

STUN 서버는 여러분의 컴퓨터가 인터넷에서 어떤 주소를 가지고 있는지 알려주는 역할을 합니다. 구글이 무료로 제공하는 STUN 서버를 사용했습니다.

그 다음으로, RTCPeerConnection이라는 객체를 생성합니다. 이게 바로 상대방과의 직접적인 연결 통로입니다.

전화선을 깔아놓는 것처럼, 이 객체를 통해 영상과 음성이 오가게 됩니다. 이 객체는 WebRTC의 핵심으로, 모든 통신이 여기를 통해 이루어집니다.

세 번째 단계에서는 getUserMedia를 사용해서 여러분의 카메라와 마이크에 접근합니다. 브라우저가 "카메라와 마이크를 사용하시겠습니까?"라고 물어보는 바로 그 순간입니다.

허용하면 stream이라는 객체로 여러분의 영상과 음성 데이터를 받아옵니다. 그리고 이 데이터를 화면에 표시하고(myVideo 요소), addTrack으로 상대방에게 보낼 준비를 합니다.

여러분이 이 코드를 사용하면 웹 페이지에서 바로 카메라와 마이크를 활성화할 수 있습니다. 줌이나 구글 미트 같은 화상회의 서비스의 첫 단계가 바로 이것입니다.

추가로 시그널링 서버와 연결하면 실제로 상대방과 통화할 수 있게 됩니다.

실전 팁

💡 getUserMedia는 반드시 HTTPS 환경에서만 작동합니다. 로컬 개발 시에는 localhost는 예외적으로 허용되니 걱정하지 마세요.

💡 사용자가 카메라/마이크 권한을 거부할 수 있으니 항상 .catch()로 에러 처리를 해주세요. "권한이 필요합니다"라는 친절한 메시지를 보여주면 좋습니다.

💡 모바일 환경에서는 배터리와 데이터 소비가 크니, 화질 조정 옵션을 제공하는 것이 좋습니다. video: { width: 640, height: 480 } 같은 제약을 줄 수 있습니다.

💡 peerConnection은 사용 후 반드시 close()로 닫아주세요. 그렇지 않으면 메모리 누수가 발생할 수 있습니다.

💡 Chrome DevTools의 chrome://webrtc-internals 페이지에서 WebRTC 연결 상태를 실시간으로 모니터링할 수 있습니다. 디버깅할 때 매우 유용합니다.


2. P2P 통신의 장단점

시작하며

여러분이 파일을 친구에게 보낼 때, 카카오톡이나 이메일을 사용하면 파일이 먼저 서버에 업로드되고, 친구가 그걸 다운로드하죠. 만약 100MB 영상을 보낸다면?

서버에 100MB 올리고, 친구가 또 100MB를 받아야 합니다. 총 200MB의 데이터가 이동하는 셈입니다.

하지만 P2P(Peer-to-Peer) 방식은 다릅니다. 여러분의 컴퓨터에서 친구 컴퓨터로 직접 파일을 보냅니다.

서버는 처음에 "이 사람과 연결해!"라고 소개만 해주고, 실제 파일은 직접 전달됩니다. 100MB 한 번만 전송하면 끝이죠.

WebRTC가 바로 이 P2P 방식을 사용합니다. 그런데 이게 항상 좋기만 할까요?

장점도 있지만 단점도 분명히 존재합니다. 실무에서 WebRTC를 사용하려면 이 장단점을 정확히 알아야 합니다.

개요

간단히 말해서, P2P(Peer-to-Peer)는 중간 서버 없이 사용자끼리 직접 연결해서 데이터를 주고받는 방식입니다. 일반적인 방식(Client-Server)은 식당에 가서 주문하는 것과 같습니다.

여러분(고객)이 주문하면, 웨이터(서버)가 받아서 주방에 전달하고, 음식을 가져다줍니다. 하지만 P2P 방식은 여러분이 직접 주방장과 대화하는 것과 같습니다.

중간 단계가 없으니 더 빠르고, 웨이터(서버)의 부담도 줄어듭니다. 특히 화상회의처럼 실시간 데이터가 많이 오가는 상황에서 서버 비용을 크게 절감할 수 있습니다.

전통적인 방법에서는 10명이 화상회의를 하면 서버가 10명분의 영상을 모두 받아서 다시 10명에게 보내야 했습니다. 하지만 P2P를 사용하면 각자가 다른 사람들과 직접 연결되니 서버는 거의 일을 하지 않습니다.

P2P의 핵심 장점은 세 가지입니다. 첫째, 지연시간이 매우 짧습니다(서버를 거치지 않으니 0.1초도 안 걸림).

둘째, 서버 비용이 거의 들지 않습니다(많은 사용자가 있어도 서버 부담이 적음). 셋째, 확장성이 좋습니다(사용자가 늘어나도 각자가 직접 연결).

하지만 단점도 있습니다. 사용자 수가 많아지면 각 사용자의 네트워크 부담이 커지고, 모든 사용자가 서로 연결되려면 복잡한 설정이 필요합니다.

또한 보안 설정(방화벽, NAT)이 까다로울 수 있습니다.

코드 예제

// P2P 연결 상태 모니터링하기
const peerConnection = new RTCPeerConnection(configuration);

// 연결 상태 변화 감지 - 연결이 잘 되고 있는지 확인
peerConnection.onconnectionstatechange = () => {
  console.log('연결 상태:', peerConnection.connectionState);

  if (peerConnection.connectionState === 'connected') {
    // P2P 직접 연결 성공!
    console.log('🎉 상대방과 직접 연결되었습니다!');
    showNotification('연결 완료', 'success');
  } else if (peerConnection.connectionState === 'failed') {
    // 직접 연결 실패 - TURN 서버가 필요할 수 있음
    console.log('⚠️ 직접 연결 실패. 중계 서버를 사용합니다.');
    showNotification('연결 품질이 낮을 수 있습니다', 'warning');
  }
};

// 데이터 전송 통계 확인 - 얼마나 빠른지 측정
peerConnection.getStats().then(stats => {
  stats.forEach(report => {
    if (report.type === 'candidate-pair' && report.state === 'succeeded') {
      console.log('왕복 시간(RTT):', report.currentRoundTripTime, '초');
      console.log('전송 속도:', report.availableOutgoingBitrate, 'bps');
    }
  });
});

설명

이것이 하는 일: 위 코드는 P2P 연결이 제대로 이루어지고 있는지, 얼마나 빠른지를 실시간으로 확인하는 방법을 보여줍니다. 마치 통화 중에 "내 목소리 잘 들려?"라고 확인하는 것과 같습니다.

첫 번째로, onconnectionstatechange 이벤트 리스너를 설정합니다. 이건 연결 상태가 바뀔 때마다 자동으로 실행됩니다.

연결이 시도되고(connecting), 성공하거나(connected), 실패하는(failed) 모든 과정을 추적할 수 있습니다. 이걸 통해 사용자에게 적절한 피드백을 줄 수 있죠.

두 번째 부분에서는 연결 상태가 'connected'인지 확인합니다. 만약 직접 연결에 성공했다면, 축하 메시지를 보여줍니다.

이 상태가 되면 영상과 음성이 가장 빠르게 전달됩니다. 하지만 'failed' 상태가 되면, 방화벽이나 NAT 때문에 직접 연결이 불가능하다는 뜻입니다.

이때는 TURN 서버(중계 서버)를 사용해야 하며, 약간의 지연이 발생할 수 있음을 사용자에게 알려주는 것이 좋습니다. 세 번째 단계에서는 getStats()를 사용해서 실제 연결 품질을 측정합니다.

currentRoundTripTime(RTT)은 데이터가 왕복하는데 걸리는 시간입니다. 이게 0.05초 이하면 아주 좋은 연결이고, 0.2초가 넘으면 영상이 버벅일 수 있습니다.

availableOutgoingBitrate는 초당 보낼 수 있는 데이터 양으로, 이게 높을수록 고화질 영상을 보낼 수 있습니다. 여러분이 이 코드를 사용하면 화상회의 중에 연결 품질을 실시간으로 모니터링할 수 있습니다.

연결이 나빠지면 자동으로 화질을 낮추거나, 사용자에게 "네트워크가 불안정합니다"라고 알려줄 수 있습니다. Zoom이나 Google Meet가 바로 이런 방식으로 작동합니다.

실전 팁

💡 3명 이하의 소규모 통화는 P2P가 완벽하지만, 5명 이상이면 SFU(Selective Forwarding Unit) 서버 사용을 고려하세요. 각 사용자가 4개 이상의 연결을 유지하면 CPU 부담이 커집니다.

💡 모바일 사용자는 데이터 요금제를 쓸 수 있으니, P2P 연결 시작 전에 "Wi-Fi에 연결하시겠습니까?"라고 확인하는 것이 좋습니다.

💡 connectionState가 'disconnected'가 되면 5초 정도 기다렸다가 자동으로 재연결을 시도하세요. 일시적인 네트워크 끊김일 수 있습니다.

💡 getStats()는 매우 상세한 정보를 제공하지만, 1초에 한 번 이상 호출하면 성능에 영향을 줄 수 있습니다. 5초 간격으로 체크하는 것을 권장합니다.

💡 RTT가 0.3초 이상이면 사용자에게 경고하고, 0.5초 이상이면 화질을 자동으로 낮추는 로직을 구현하면 사용자 경험이 크게 개선됩니다.


3. NAT와 방화벽 문제

시작하며

여러분이 집에서 인터넷을 사용할 때, 실제로는 공유기(라우터)를 통해 연결되어 있습니다. 공유기는 하나의 공인 IP 주소를 여러 기기가 나눠 쓸 수 있게 해주죠.

스마트폰, 노트북, 태블릿이 모두 같은 공유기를 쓰지만, 각각 다른 기기로 인식됩니다. 문제는 여기서 발생합니다.

외부에서 여러분의 컴퓨터로 직접 연결하려고 할 때, 공유기가 "이 요청을 누구한테 보내야 하지?"라고 헷갈려 합니다. 노트북인가?

스마트폰인가? 이게 바로 NAT(Network Address Translation) 문제입니다.

게다가 회사나 학교에서는 보안을 위해 방화벽을 설치해놓았습니다. 방화벽은 경비원처럼 "허가받지 않은 연결은 들어올 수 없어!"라고 막아버립니다.

WebRTC로 P2P 연결을 하려면 이 두 가지 문제를 해결해야 합니다.

개요

간단히 말해서, NAT는 하나의 공인 IP 주소를 여러 기기가 공유할 수 있게 해주는 기술이지만, 외부에서 내부로 직접 연결하는 것을 어렵게 만듭니다. NAT를 아파트 건물에 비유해볼까요?

건물의 주소는 하나지만(공인 IP), 그 안에는 101호, 102호, 103호처럼 여러 집(각 기기)이 있습니다. 우편배달부(외부 서버)가 "서울시 강남구 XX아파트"로 소포를 보내면, 정확히 몇 호에 전달해야 할지 모릅니다.

이것이 NAT의 문제입니다. 방화벽은 건물 입구의 경비실이라고 생각하면 됩니다.

허가받지 않은 사람(데이터)은 들어올 수 없게 막습니다. 전통적인 Client-Server 방식에서는 이게 문제가 되지 않았습니다.

서버는 항상 공인 IP를 가지고 있고, 클라이언트가 먼저 서버에 연결하기 때문입니다. 하지만 P2P에서는 양쪽 모두가 서로에게 연결을 시도해야 하는데, NAT와 방화벽이 이를 막아버립니다.

NAT의 종류는 크게 네 가지입니다. Full Cone(가장 관대함, P2P 연결 쉬움), Restricted Cone(조금 까다로움), Port Restricted Cone(더 까다로움), Symmetric NAT(가장 엄격함, P2P 연결 매우 어려움).

여러분의 네트워크가 어떤 NAT 타입인지에 따라 P2P 연결 성공 확률이 달라집니다. Symmetric NAT 환경에서는 TURN 서버가 거의 필수적입니다.

기업 네트워크는 보통 가장 엄격한 설정을 사용하므로, 회사에서 WebRTC가 잘 안 되는 이유가 바로 이것입니다.

코드 예제

// NAT 타입 감지하기
const configuration = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'stun:stun1.l.google.com:19302' }
  ]
};

const pc = new RTCPeerConnection(configuration);

// ICE 후보 수집 - 연결 가능한 경로 찾기
pc.onicecandidate = (event) => {
  if (event.candidate) {
    console.log('발견된 연결 경로:', event.candidate.candidate);

    // candidate 타입 확인
    if (event.candidate.type === 'host') {
      console.log('📍 내부 IP:', event.candidate.address);
    } else if (event.candidate.type === 'srflx') {
      console.log('🌐 공인 IP:', event.candidate.address);
      console.log('✅ STUN 서버를 통해 NAT 통과 가능!');
    } else if (event.candidate.type === 'relay') {
      console.log('🔄 TURN 서버 필요:', event.candidate.address);
      console.log('⚠️ 직접 연결 불가능, 중계 서버 사용');
    }
  }
};

// Offer 생성으로 ICE 수집 시작
pc.createOffer().then(offer => pc.setLocalDescription(offer));

설명

이것이 하는 일: 위 코드는 여러분의 네트워크 환경을 분석해서 P2P 연결이 가능한지, 아니면 중계 서버가 필요한지를 자동으로 판단합니다. 마치 지도 앱이 여러 경로를 찾아서 가장 빠른 길을 알려주는 것과 같습니다.

첫 번째로, iceServers 설정에 STUN 서버 주소를 넣어줍니다. 여기서는 구글의 무료 STUN 서버 두 개를 사용했습니다.

ICE(Interactive Connectivity Establishment)는 "연결 가능한 모든 경로를 찾아봐!"라는 프로토콜입니다. STUN 서버는 이 과정에서 여러분의 공인 IP 주소를 알려주는 역할을 합니다.

두 번째로, onicecandidate 이벤트 리스너가 등록됩니다. 이 함수는 연결 가능한 경로(candidate)를 찾을 때마다 자동으로 호출됩니다.

보통 3-5개 정도의 후보가 발견됩니다. 'host' 타입은 같은 네트워크(같은 Wi-Fi) 안에서만 연결 가능한 내부 IP입니다.

'srflx' 타입은 STUN을 통해 발견한 공인 IP로, 이게 나오면 NAT를 통과해서 직접 연결할 수 있다는 뜻입니다. 'relay' 타입은 직접 연결이 불가능해서 TURN 서버를 통해 중계해야 한다는 의미입니다.

마지막으로, createOffer()를 호출해서 ICE 후보 수집을 시작합니다. 이건 실제 연결을 시작하는 것은 아니고, "나는 이런 경로들로 연결할 수 있어!"라는 정보를 모으는 과정입니다.

이 정보를 상대방과 교환하면, 서로가 연결 가능한 최선의 경로를 찾아서 연결합니다. 여러분이 이 코드를 사용하면 사용자의 네트워크 환경을 진단할 수 있습니다.

만약 'srflx' 후보만 나오면 일반적인 가정이나 카페 환경으로, P2P 직접 연결이 가능합니다. 하지만 'relay' 후보만 나오고 'srflx'가 없다면 기업 방화벽이나 Symmetric NAT 환경으로, TURN 서버가 필수입니다.

이 정보를 바탕으로 사용자에게 "기업 네트워크에서는 연결 품질이 낮을 수 있습니다"라고 미리 알려줄 수 있습니다.

실전 팁

💡 ICE 후보 수집은 보통 3-5초가 걸립니다. 너무 오래 걸리면(10초 이상) iceGatheringState를 확인해서 타임아웃 처리를 해주세요.

💡 'relay' 타입만 나오는 사용자를 위해 "설정 > 방화벽에서 UDP 포트를 열어주세요"라는 가이드를 제공하면 좋습니다. IT 부서에 요청하는 템플릿을 제공하는 것도 좋은 방법입니다.

💡 IPv4와 IPv6 후보가 섞여 나올 수 있습니다. IPv6가 있으면 더 좋지만, 아직 모든 네트워크가 지원하는 건 아니므로 IPv4를 우선으로 하세요.

💡 모바일 네트워크(4G/5G)는 보통 Symmetric NAT를 사용하므로, TURN 서버 없이는 연결이 어렵습니다. 모바일 사용자에게 Wi-Fi 사용을 권장하세요.

💡 개발 시 chrome://webrtc-internals에서 수집된 모든 ICE 후보를 상세히 볼 수 있습니다. 연결 문제를 디버깅할 때 가장 먼저 확인해야 할 곳입니다.


4. STUN/TURN 서버 역할

시작하며

여러분이 미로에 갇혀서 출구를 찾고 있다고 상상해보세요. 미로 위에 드론을 띄워서 위에서 보면 출구가 어디 있는지 한눈에 보이겠죠?

STUN 서버가 바로 이 드론과 같은 역할을 합니다. 하지만 때로는 미로가 너무 복잡해서 직접 출구로 갈 수 없을 때가 있습니다.

벽이 너무 높거나 길이 막혀있을 때죠. 이럴 때는 누군가 여러분을 업고 벽을 넘어서 출구까지 데려다줘야 합니다.

이게 바로 TURN 서버의 역할입니다. WebRTC에서 P2P 연결을 할 때, 대부분의 경우 STUN 서버만으로 충분합니다.

하지만 약 10-20%의 경우에는 NAT나 방화벽이 너무 엄격해서 TURN 서버가 필요합니다. 실무에서 화상회의 서비스를 만든다면 반드시 둘 다 준비해야 합니다.

개요

간단히 말해서, STUN 서버는 여러분의 공인 IP 주소를 알려주는 서버이고, TURN 서버는 직접 연결이 불가능할 때 데이터를 중계해주는 서버입니다. STUN(Session Traversal Utilities for NAT)을 거울에 비유해볼까요?

여러분은 자기 얼굴을 직접 볼 수 없지만, 거울을 보면 어떻게 생겼는지 알 수 있습니다. 마찬가지로 여러분의 컴퓨터는 자신의 공인 IP를 모르지만, STUN 서버에 물어보면 "당신의 공인 IP는 123.45.67.89입니다"라고 알려줍니다.

이 정보를 상대방과 공유하면 직접 연결할 수 있습니다. STUN은 가볍고 빠르며, 구글이 무료로 제공하는 서버를 사용할 수 있어서 비용이 들지 않습니다.

TURN(Traversal Using Relays around NAT)은 택배 기사와 같습니다. A에서 B로 직접 갈 수 없을 때, TURN 서버가 중간에서 물건을 받아서 전달해줍니다.

모든 영상과 음성 데이터가 TURN 서버를 거쳐가므로, 서버에 많은 부담이 됩니다. 그래서 TURN 서버는 유료인 경우가 많고, 대역폭 비용이 상당합니다.

실제 통계를 보면, 일반 사용자의 약 80-90%는 STUN만으로 연결이 가능하지만, 기업 네트워크 사용자의 경우 50% 이상이 TURN 서버를 필요로 합니다. 또한 두 사용자가 모두 Symmetric NAT 뒤에 있으면 거의 100% TURN이 필요합니다.

따라서 비즈니스용 화상회의 서비스라면 TURN 서버는 필수입니다. 무료 대안으로는 coturn이라는 오픈소스 TURN 서버를 직접 운영하거나, AWS나 Twilio 같은 서비스를 이용할 수 있습니다.

코드 예제

// STUN과 TURN 서버 설정하기
const configuration = {
  iceServers: [
    // 무료 STUN 서버 - 공인 IP 찾기
    {
      urls: 'stun:stun.l.google.com:19302'
    },
    // 예비 STUN 서버 - 첫 번째 서버 장애 대비
    {
      urls: 'stun:stun1.l.google.com:19302'
    },
    // TURN 서버 - 직접 연결 불가능할 때 중계
    {
      urls: 'turn:your-turn-server.com:3478',
      username: 'user123',
      credential: 'password123',
      credentialType: 'password'
    },
    // TURN over TLS - 보안이 중요한 경우
    {
      urls: 'turns:your-turn-server.com:5349',
      username: 'user123',
      credential: 'password123',
      credentialType: 'password'
    }
  ],
  // ICE 후보 수집 정책 설정
  iceTransportPolicy: 'all'  // 'all' 또는 'relay'
};

const pc = new RTCPeerConnection(configuration);

// 어떤 서버를 사용했는지 확인
pc.onicecandidate = (event) => {
  if (event.candidate) {
    const serverUsed = event.candidate.relatedAddress ?
      'STUN 사용 (직접 연결)' :
      event.candidate.type === 'relay' ? 'TURN 사용 (중계)' : '로컬';
    console.log(`${serverUsed}: ${event.candidate.address}`);
  }
};

설명

이것이 하는 일: 위 코드는 STUN과 TURN 서버를 모두 설정해서, 어떤 네트워크 환경에서도 연결할 수 있도록 만들어줍니다. 마치 여러 개의 비상구를 준비해두는 것과 같습니다.

첫 번째로, iceServers 배열에 여러 개의 서버를 등록합니다. WebRTC는 이 목록을 위에서부터 순서대로 시도합니다.

먼저 구글의 무료 STUN 서버 두 개를 등록했습니다. 하나가 장애가 나더라도 다른 하나가 작동하도록 이중화한 것입니다.

STUN 서버는 인증이 필요 없으므로 URL만 제공하면 됩니다. 두 번째로, TURN 서버를 등록합니다.

TURN 서버는 보안을 위해 username과 credential(비밀번호)이 필요합니다. 누구나 무료로 사용하면 서버 비용이 폭발적으로 증가하기 때문입니다.

'turn:'은 일반 연결이고, 'turns:'는 TLS로 암호화된 연결입니다. 금융 서비스처럼 보안이 중요하다면 'turns'를 사용하세요.

포트는 TURN이 3478, TURNS가 5349를 주로 사용합니다. 세 번째로, iceTransportPolicy를 설정합니다.

기본값인 'all'은 직접 연결을 먼저 시도하고, 안 되면 TURN을 사용합니다. 만약 'relay'로 설정하면 항상 TURN만 사용합니다.

보안이 극도로 중요해서 직접 P2P 연결을 금지하고 싶을 때 사용합니다(모든 트래픽이 회사 TURN 서버를 거쳐가도록). 마지막으로, onicecandidate에서 어떤 서버가 사용되었는지 확인합니다.

relatedAddress가 있으면 STUN을 통해 NAT를 통과한 것이고, type이 'relay'면 TURN 중계를 사용한 것입니다. 이 정보를 로깅하면 전체 사용자 중 몇 %가 TURN을 사용하는지 파악할 수 있고, TURN 서버 용량을 계획할 때 유용합니다.

여러분이 이 코드를 사용하면 거의 모든 네트워크 환경에서 연결이 가능한 안정적인 시스템을 만들 수 있습니다. Zoom이나 Google Meet도 비슷한 방식으로 STUN/TURN을 함께 사용합니다.

실무 팁: 전체 사용자의 10-20%가 TURN을 사용한다고 가정하고 서버 용량을 계획하세요.

실전 팁

💡 무료 STUN 서버는 구글, Mozilla, Cloudflare 등이 제공합니다. 하지만 무료 TURN 서버는 거의 없으므로, coturn을 AWS EC2에 직접 설치하거나 Twilio/Xirsys 같은 유료 서비스를 사용하세요.

💡 TURN 서버 비용은 데이터 전송량에 비례합니다. 1시간 화상회의는 약 1-2GB를 소비하므로, 사용자가 많으면 비용이 빠르게 증가합니다. 사용량 모니터링이 필수입니다.

💡 보안을 위해 TURN 서버의 username/credential은 절대 하드코딩하지 마세요. 서버에서 동적으로 생성해서 제공하고, 1시간마다 만료되도록 설정하세요(TURN REST API 사용).

💡 iceTransportPolicy를 'relay'로 강제하면 모든 연결이 TURN을 거치므로 비용이 크게 증가합니다. 정말 필요한 경우(고도의 보안 요구사항)가 아니면 'all'을 사용하세요.

💡 TURN 서버는 지리적으로 사용자와 가까운 곳에 위치해야 지연시간이 줄어듭니다. 글로벌 서비스라면 미국, 유럽, 아시아에 각각 TURN 서버를 두고 가장 가까운 서버를 선택하도록 하세요.


5. Zoom과 WebRTC 비교

시작하며

여러분이 화상회의를 생각하면 가장 먼저 떠오르는 게 뭔가요? 아마 Zoom일 겁니다.

전 세계 수백만 명이 매일 사용하는 화상회의의 대명사죠. 그런데 Zoom은 WebRTC를 사용할까요?

답은 "일부만 사용한다"입니다. Zoom은 자체 개발한 멀티미디어 라우터 기술을 주로 사용하고, 웹 브라우저에서만 제한적으로 WebRTC를 사용합니다.

왜 그럴까요? 각각의 장단점이 분명하기 때문입니다.

만약 여러분이 화상회의 서비스를 만든다면 어떤 기술을 선택해야 할까요? 실무에서 이 선택은 매우 중요합니다.

10명 이하의 소규모 회의인가요? 100명 이상의 웨비나인가요?

모바일 지원이 중요한가요? 브라우저만 지원하면 되나요?

이런 질문들에 따라 답이 달라집니다.

개요

간단히 말해서, Zoom은 서버 중심의 SFU(Selective Forwarding Unit) 아키텍처를 사용하고, 순수 WebRTC는 P2P(Peer-to-Peer) 방식을 기본으로 합니다. 레스토랑에 비유하면 이렇습니다.

WebRTC P2P 방식은 뷔페입니다. 각자가 직접 음식을 가져다 먹습니다(사용자끼리 직접 연결).

빠르고 자유롭지만, 사람이 많아지면 혼잡해집니다. Zoom의 SFU 방식은 정식 코스 요리입니다.

웨이터(서버)가 모든 음식을 가져다줍니다. 조금 느릴 수 있지만 많은 사람이 와도 질서정연합니다.

WebRTC의 장점은 설치 없이 브라우저에서 바로 작동한다는 것입니다. 링크만 클릭하면 즉시 회의 시작.

서버 비용도 적게 듭니다(P2P 직접 연결). 오픈소스라 무료로 사용 가능합니다.

하지만 5명 이상이면 각 사용자의 CPU와 네트워크 부담이 커집니다. 브라우저 호환성 문제도 있고, 화질/음질 최적화가 Zoom보다 어렵습니다.

Zoom의 장점은 100명 이상의 대규모 회의도 안정적입니다. 전용 앱으로 성능이 최적화되어 있고, 화면 공유, 녹화, 배경 흐림 등 풍부한 기능을 제공합니다.

또한 전 세계 데이터센터를 통해 안정적인 품질을 보장합니다. 하지만 별도 설치가 필요하고(마케팅 팀이 싫어하는 부분), 서버 비용이 많이 들며, 자체 프로토콜이라 커스터마이징이 어렵습니다.

실무 선택 가이드: 1:1 또는 소규모(3-5명) 회의 → WebRTC P2P가 완벽. 중규모(5-20명) 회의 → WebRTC + SFU 서버(mediasoup, Janus 등).

대규모(20명 이상) 또는 웨비나 → Zoom이나 전용 솔루션 고려. 빠른 프로토타입 또는 스타트업 → WebRTC로 시작(Jitsi Meet, Daily.co 같은 오픈소스 활용).

기업용 안정성이 최우선 → Zoom이나 Microsoft Teams.

코드 예제

// WebRTC로 간단한 화상회의 만들기 (Zoom 대안)
class SimpleVideoChat {
  constructor() {
    this.localStream = null;
    this.peerConnections = new Map(); // 여러 명과 연결하기 위한 Map
    this.configuration = {
      iceServers: [
        { urls: 'stun:stun.l.google.com:19302' }
      ]
    };
  }

  // 내 카메라/마이크 시작
  async startLocalStream() {
    this.localStream = await navigator.mediaDevices.getUserMedia({
      video: { width: 1280, height: 720 },  // Zoom 기본 해상도
      audio: {
        echoCancellation: true,  // 에코 제거 (중요!)
        noiseSuppression: true,  // 배경 소음 제거
        autoGainControl: true    // 자동 볼륨 조절
      }
    });

    document.getElementById('myVideo').srcObject = this.localStream;
    console.log('✅ 카메라/마이크 준비 완료');
  }

  // 다른 참가자와 연결
  async connectToPeer(peerId) {
    const pc = new RTCPeerConnection(this.configuration);
    this.peerConnections.set(peerId, pc);

    // 내 영상 전송
    this.localStream.getTracks().forEach(track => {
      pc.addTrack(track, this.localStream);
    });

    // 상대방 영상 수신
    pc.ontrack = (event) => {
      const video = document.getElementById(`video-${peerId}`);
      video.srcObject = event.streams[0];
      console.log(`✅ ${peerId}의 영상 수신 시작`);
    };

    return pc;
  }

  // 화면 공유 (Zoom의 핵심 기능)
  async shareScreen() {
    const screenStream = await navigator.mediaDevices.getDisplayMedia({
      video: { cursor: 'always' }  // 마우스 커서도 함께 공유
    });

    // 기존 비디오 트랙을 화면 공유로 교체
    const screenTrack = screenStream.getVideoTracks()[0];
    this.peerConnections.forEach(pc => {
      const sender = pc.getSenders().find(s => s.track.kind === 'video');
      sender.replaceTrack(screenTrack);
    });

    console.log('✅ 화면 공유 시작');
  }
}

// 사용 예시
const chat = new SimpleVideoChat();
await chat.startLocalStream();

설명

이것이 하는 일: 위 코드는 Zoom과 유사한 화상회의 기능을 WebRTC로 간단하게 구현한 예제입니다. 클래스 기반으로 설계해서 확장하기 쉽게 만들었습니다.

첫 번째로, SimpleVideoChat 클래스의 생성자에서 peerConnections Map을 만듭니다. Zoom처럼 여러 명과 동시에 연결하려면, 각 참가자마다 별도의 RTCPeerConnection이 필요합니다.

Map을 사용하면 참가자 ID로 쉽게 관리할 수 있습니다. 5명이 회의에 참가하면 5개의 연결이 생깁니다.

두 번째로, startLocalStream()에서 카메라와 마이크를 가져올 때 Zoom이 사용하는 것과 비슷한 설정을 적용합니다. echoCancellation(에코 제거)은 스피커에서 나온 소리가 다시 마이크로 들어가는 것을 막아줍니다.

이게 없으면 "하울링" 현상이 발생합니다. noiseSuppression은 키보드 타이핑 소리나 배경 소음을 자동으로 줄여주고, autoGainControl은 목소리 크기를 자동으로 조절해줍니다.

이 세 가지 설정이 Zoom의 깨끗한 음질의 비밀입니다. 세 번째로, connectToPeer()는 새로운 참가자와 연결을 만듭니다.

addTrack으로 내 영상을 보내고, ontrack으로 상대방 영상을 받습니다. 실제로는 여기에 시그널링 서버(WebSocket)를 추가해서 Offer/Answer를 교환해야 합니다.

이 부분은 간소화했지만, 실무에서는 Socket.io나 Firebase를 많이 사용합니다. 마지막으로, shareScreen()은 Zoom의 가장 인기 있는 기능인 화면 공유를 구현합니다.

getDisplayMedia()로 화면을 캡처하고, replaceTrack()으로 기존 카메라 영상을 화면 공유로 바꿉니다. 화면 공유를 끝내면 다시 카메라로 되돌리면 됩니다.

이게 바로 Zoom에서 "화면 공유 시작/중지" 버튼을 누를 때 일어나는 일입니다. 여러분이 이 코드를 확장하면 Zoom과 유사한 서비스를 만들 수 있습니다.

추가하면 좋은 기능: 음소거 on/off (track.enabled = false), 참가자 목록 관리, 채팅 기능(RTCDataChannel 사용), 녹화 기능(MediaRecorder API), 가상 배경(Canvas + TensorFlow.js). 실제로 Jitsi Meet이라는 오픈소스 프로젝트가 이런 방식으로 만들어진 완전한 Zoom 대안입니다.

실전 팁

💡 5명 이상의 회의라면 P2P 대신 SFU 서버(mediasoup, Janus 등)를 사용하세요. 각 사용자가 n-1개의 연결을 유지하면 CPU가 버틸 수 없습니다.

💡 모바일에서는 배터리 절약을 위해 해상도를 낮추세요. video: { width: 640, height: 480 } 정도면 충분합니다. 또한 frameRate: 15로 제한하면 배터리 소모가 크게 줄어듭니다.

💡 화면 공유 시 cursor: 'always' 옵션을 주면 마우스 포인터도 함께 공유되어 발표할 때 유용합니다. 하지만 프라이버시가 중요하면 'never'로 설정하세요.

💡 네트워크가 불안정할 때 자동으로 화질을 낮추려면 RTCRtpSender.setParameters()로 bitrate를 조절할 수 있습니다. Zoom도 이런 adaptive bitrate 기술을 사용합니다.

💡 실무에서는 Twilio Video나 Agora.io 같은 CPaaS(Communication Platform as a Service)를 사용하면 SFU 서버 운영 부담 없이 빠르게 서비스를 만들 수 있습니다. 초기 스타트업에게 추천합니다.


6. 브라우저 호환성 확인

시작하며

여러분이 만든 멋진 화상회의 사이트를 친구에게 공유했는데, "나는 안 되는데?"라는 답변을 받은 적 있나요? 웹 개발에서 가장 골치 아픈 문제가 바로 브라우저 호환성입니다.

WebRTC는 2011년에 처음 등장했고, 지금은 대부분의 브라우저가 지원합니다. 하지만 "대부분"이라는 말이 중요합니다.

Internet Explorer는 완전히 지원하지 않고, Safari는 2017년에야 지원을 시작했으며, 모바일 브라우저는 또 다른 이야기입니다. 실무에서 서비스를 배포하기 전에 반드시 해야 할 일이 있습니다.

바로 사용자의 브라우저가 WebRTC를 지원하는지 확인하고, 지원하지 않으면 친절한 안내 메시지를 보여주는 것입니다. 에러 없이 그냥 작동하지 않으면 사용자는 여러분의 사이트가 고장 났다고 생각할 것입니다.

개요

간단히 말해서, 브라우저 호환성 확인은 사용자의 브라우저가 필요한 WebRTC 기능을 지원하는지 미리 체크하고, 지원하지 않으면 대안을 제시하는 과정입니다. 자동차 정비소에 비유해볼까요?

정비를 시작하기 전에 "이 차는 어떤 모델이고, 어떤 부품이 호환되나요?"를 먼저 확인합니다. 마찬가지로 WebRTC 기능을 사용하기 전에 "이 브라우저는 어떤 버전이고, 어떤 API를 지원하나요?"를 확인해야 합니다.

지원하지 않는 브라우저에서 무�턱대고 getUserMedia()를 호출하면 에러가 발생하고 사용자는 혼란스러워합니다. 2025년 1월 기준 주요 브라우저 지원 현황: Chrome 23+ (거의 완벽한 지원, 시장 점유율 65%), Firefox 22+ (완벽한 지원, 점유율 8%), Safari 11+ (2017년부터 지원, 일부 기능 제한, 점유율 19%), Edge (Chromium 기반으로 전환 후 완벽 지원, 점유율 5%), Opera (Chromium 기반, 완벽 지원), IE (전혀 지원 안 함, 다행히 2022년 종료).

모바일은 조금 복잡합니다. iOS Safari 11+ (iPhone/iPad의 유일한 선택, 지원하지만 일부 제약), Android Chrome (완벽 지원), Android Firefox (완벽 지원), 모바일 앱 내 WebView (제한적 지원, 특히 iOS WebView는 getUserMedia 제한).

특히 주의할 점은 카카오톡이나 페이스북 인앱 브라우저에서는 카메라 접근이 제한될 수 있다는 것입니다. 실무 전략: 먼저 기능 감지(Feature Detection)를 사용하세요.

"Chrome인가?"가 아니라 "getUserMedia를 지원하는가?"를 물어야 합니다. 브라우저는 계속 업데이트되므로 버전 번호로 판단하면 나중에 문제가 생깁니다.

지원하지 않으면 polyfill(adapter.js 같은 라이브러리)을 사용하거나, 친절한 안내 메시지를 보여주세요("Chrome 또는 Firefox 최신 버전을 사용해주세요").

코드 예제

// WebRTC 브라우저 호환성 완벽 체크
class WebRTCCompatibility {
  // 기본 WebRTC 지원 확인
  static isWebRTCSupported() {
    return !!(
      navigator.mediaDevices &&
      navigator.mediaDevices.getUserMedia &&
      window.RTCPeerConnection
    );
  }

  // 상세 기능별 지원 확인
  static getFeatureSupport() {
    return {
      getUserMedia: !!(navigator.mediaDevices?.getUserMedia),
      getDisplayMedia: !!(navigator.mediaDevices?.getDisplayMedia),
      RTCPeerConnection: !!(window.RTCPeerConnection),
      RTCDataChannel: this.checkDataChannel(),
      screenSharing: !!(navigator.mediaDevices?.getDisplayMedia),
      audioContext: !!(window.AudioContext || window.webkitAudioContext)
    };
  }

  // 데이터 채널 지원 확인 (채팅 기능용)
  static checkDataChannel() {
    try {
      const pc = new RTCPeerConnection();
      const dc = pc.createDataChannel('test');
      dc.close();
      pc.close();
      return true;
    } catch (e) {
      return false;
    }
  }

  // 브라우저 정보 가져오기
  static getBrowserInfo() {
    const ua = navigator.userAgent;
    let browserName = 'Unknown';
    let browserVersion = 'Unknown';

    if (ua.indexOf('Chrome') > -1 && ua.indexOf('Edg') === -1) {
      browserName = 'Chrome';
      browserVersion = ua.match(/Chrome\/(\d+)/)?.[1] || 'Unknown';
    } else if (ua.indexOf('Firefox') > -1) {
      browserName = 'Firefox';
      browserVersion = ua.match(/Firefox\/(\d+)/)?.[1] || 'Unknown';
    } else if (ua.indexOf('Safari') > -1 && ua.indexOf('Chrome') === -1) {
      browserName = 'Safari';
      browserVersion = ua.match(/Version\/(\d+)/)?.[1] || 'Unknown';
    } else if (ua.indexOf('Edg') > -1) {
      browserName = 'Edge';
      browserVersion = ua.match(/Edg\/(\d+)/)?.[1] || 'Unknown';
    }

    return { browserName, browserVersion };
  }

  // 사용자에게 보여줄 호환성 리포트
  static generateReport() {
    const isSupported = this.isWebRTCSupported();
    const features = this.getFeatureSupport();
    const browser = this.getBrowserInfo();

    console.log('=== WebRTC 호환성 리포트 ===');
    console.log(`브라우저: ${browser.browserName} ${browser.browserVersion}`);
    console.log(`WebRTC 지원: ${isSupported ? '✅' : '❌'}`);
    console.log('기능별 지원:');
    Object.entries(features).forEach(([feature, supported]) => {
      console.log(`  ${feature}: ${supported ? '✅' : '❌'}`);
    });

    return { isSupported, features, browser };
  }
}

// 앱 시작 시 호환성 체크
const compatibility = WebRTCCompatibility.generateReport();

if (!compatibility.isSupported) {
  alert(`죄송합니다. 현재 브라우저(${compatibility.browser.browserName})는 화상회의를 지원하지 않습니다.\n\nChrome, Firefox, Safari 최신 버전을 사용해주세요.`);
}

설명

이것이 하는 일: 위 코드는 사용자의 브라우저가 WebRTC의 어떤 기능을 지원하는지 종합적으로 체크하고, 개발자와 사용자 모두에게 명확한 정보를 제공합니다. 마치 건강검진 리포트처럼 말이죠.

첫 번째로, isWebRTCSupported()는 가장 기본적인 체크입니다. getUserMedia(카메라/마이크 접근)와 RTCPeerConnection(P2P 연결)이 있는지 확인합니다.

이 두 가지는 WebRTC의 핵심이므로, 하나라도 없으면 화상회의가 불가능합니다. !!를 사용해서 undefined나 null을 false로 변환하고, ?.는 옵셔널 체이닝으로 에러 없이 안전하게 체크합니다.

두 번째로, getFeatureSupport()는 더 상세한 기능별 체크를 합니다. getDisplayMedia(화면 공유)는 최신 브라우저에만 있으므로 별도로 확인합니다.

RTCDataChannel(텍스트 채팅용)도 일부 구형 브라우저에서는 지원하지 않습니다. audioContext는 배경 소음 제거나 음성 효과를 넣을 때 필요한 Web Audio API입니다.

각 기능을 확인해서 "화면 공유는 안 되지만 영상 통화는 가능해"같은 부분 지원 상황을 파악할 수 있습니다. 세 번째로, getBrowserInfo()는 User Agent 문자열을 파싱해서 브라우저 이름과 버전을 추출합니다.

User Agent는 "나는 이런 브라우저입니다"라고 자기소개하는 긴 문자열입니다. 정규표현식으로 'Chrome/120', 'Firefox/115' 같은 부분을 찾아냅니다.

이 정보는 버그 리포트를 받을 때 매우 유용합니다. "어떤 브라우저에서 문제가 발생했나요?"라고 물어볼 필요 없이 자동으로 수집됩니다.

마지막으로, generateReport()는 모든 정보를 종합해서 콘솔에 예쁘게 출력합니다. 개발자는 DevTools에서 이걸 보고 문제를 빠르게 파악할 수 있습니다.

실무에서는 이 정보를 서버로 전송해서 "전체 사용자 중 몇 %가 Safari를 쓰는가?" 같은 통계를 낼 수 있습니다. 이걸 바탕으로 어떤 브라우저를 우선 지원할지 결정할 수 있죠.

여러분이 이 코드를 사용하면 웹사이트가 로드될 때 자동으로 호환성을 체크하고, 문제가 있으면 즉시 알려줄 수 있습니다. Google Meet이나 Zoom 웹 버전도 비슷한 체크를 합니다.

추가로 구현하면 좋은 것: 지원하지 않는 브라우저에는 "Chrome 다운로드" 버튼 보여주기, 모바일 앱이 있다면 앱 다운로드 유도, 호환성 정보를 localStorage에 저장해서 매번 체크하지 않기.

실전 팁

💡 adapter.js 라이브러리를 사용하면 브라우저별 WebRTC API 차이를 자동으로 보정해줍니다. CDN으로 간단히 추가할 수 있으니 프로젝트에 항상 포함하세요.

💡 iOS Safari는 getUserMedia가 있어도 인앱 브라우저(카카오톡, 인스타그램 등)에서는 작동하지 않습니다. 이런 경우 "Safari에서 열기" 버튼을 제공하세요.

💡 HTTPS가 아니면 getUserMedia가 작동하지 않습니다. 개발 중에는 localhost는 예외지만, 배포 시에는 반드시 SSL 인증서를 설치하세요. Let's Encrypt로 무료로 가능합니다.

💡 사용자에게 에러 메시지를 보여줄 때 기술 용어를 피하세요. "RTCPeerConnection is not defined" 대신 "최신 버전의 Chrome을 사용해주세요"처럼 친절하게 안내하세요.

💡 모바일 감지는 단순히 화면 크기로 하지 말고 navigator.userAgent에서 'Mobile'이나 'Android' 키워드를 찾으세요. 태블릿은 화면이 크지만 모바일 브라우저를 사용할 수 있습니다.


#WebRTC#P2P#STUN#TURN#VideoChat#WebRTC,화상회의

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.