Node.js · 2026-04-11

WebRTC 기반 실시간 영상 통화와 Node.js 시그널링

Node.js로 WebRTC 시그널링 서버를 구현하고 브라우저 간 실시간 영상 통화를 연결하는 방법을 단계별로 설명, 배포와 보안 고려사항 중심의 설명

작성일 : 2026-04-11 ㆍ 작성자 : 관리자
post
목차

소개

실시간 영상 통화는 브라우저에서 직접 미디어 스트림을 주고받는 WebRTC로 구현된다. 하지만 피어 간 연결을 수립하려면 시그널링 서버가 필요하다. 이 글에서는 Node.js 기반 시그널링 서버의 구조와 핵심 흐름을 이해하고 간단한 구현 예제를 통해 브라우저 간 영상 통화를 만드는 방법을 설명한다.

기본 아키텍처

전체 흐름은 다음과 같다.

  • 클라이언트는 카메라와 마이크 권한을 얻어 로컬 미디어를 준비한다.
  • RTCPeerConnection을 생성하고 로컬 트랙을 추가한다.
  • 시그널링 서버를 통해 offer/answer와 ICE 후보 정보를 교환한다.
  • 정상적으로 ICE 연결이 수립되면 미디어 스트림이 전달된다.

사전 준비

필수 요소는 Node.js, WebSocket 라이브러리(ws 또는 socket.io), 그리고 HTTPS 환경(또는 localhost)이다. HTTPS가 없는 경우 일부 브라우저에서 getUserMedia 또는 트랙 전송이 제한될 수 있다.

시그널링 서버 구현 (Node.js + ws)

시그널링 서버는 단순히 메시지를 중계한다. 방(room) 개념을 사용하면 같은 방에 있는 피어들끼리만 메시지를 주고받을 수 있다.

const http = require('http')
const WebSocket = require('ws')

const server = http.createServer()
const wss = new WebSocket.Server({ server })

const rooms = new Map() // roomId -> Set of sockets

wss.on('connection', (ws) => {
  ws.on('message', (msg) => {
    let data
    try { data = JSON.parse(msg) } catch(e) { return }
    const { type, room, payload } = data

    if (type === 'join') {
      if (!rooms.has(room)) rooms.set(room, new Set())
      rooms.get(room).add(ws)
      ws.room = room
      return
    }

    // 브로드캐스트: 같은 방의 다른 소켓으로 전달
    const peers = rooms.get(ws.room) || new Set()
    peers.forEach(peer => {
      if (peer !== ws && peer.readyState === WebSocket.OPEN) {
        peer.send(JSON.stringify({ type, payload }))
      }
    })
  })

  ws.on('close', () => {
    const r = ws.room
    if (r && rooms.has(r)) {
      rooms.get(r).delete(ws)
      if (rooms.get(r).size === 0) rooms.delete(r)
    }
  })
})

server.listen(3000)

설명

이 서버는 매우 단순하다. 클라이언트는 'join' 메시지로 방에 참여하고, 이후의 offer/answer/ice 메시지는 같은 방의 다른 참여자로 전달된다. 실 서비스에서는 인증, 권한, 메시지 크기 검증, 에러 핸들링을 추가해야 한다.

클라이언트 주요 흐름

클라이언트는 getUserMedia, RTCPeerConnection을 사용해 로컬 스트림을 생성하고 시그널링 메시지를 주고받아 연결을 성립시킨다.

// 미디어 얻기
const localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
const pc = new RTCPeerConnection()

// 로컬 트랙 추가
localStream.getTracks().forEach(t => pc.addTrack(t, localStream))

// 원격 트랙 수신
pc.ontrack = (ev) => {
  // <video id='remote' autoplay playsinline></video> 요소에 연결
  const remoteVideo = document.getElementById('remote')
  remoteVideo.srcObject = ev.streams[0]
}

// ICE 후보 발생 시 시그널링 서버로 전송
pc.onicecandidate = (ev) => {
  if (ev.candidate) {
    ws.send(JSON.stringify({ type: 'ice', payload: ev.candidate }))
  }
}

// offer 생성 예
const offer = await pc.createOffer()
await pc.setLocalDescription(offer)
ws.send(JSON.stringify({ type: 'offer', payload: offer }))

시그널 메시지 처리

서버로부터 받은 메시지는 type에 따라 처리한다.

  • 'offer' 수신: setRemoteDescription 후 answer 생성 및 전송
  • 'answer' 수신: setRemoteDescription로 완료
  • 'ice' 수신: addIceCandidate로 후보 추가

배포와 보안 고려사항

실무에서는 HTTPS 적용과 함께 시그널링 채널의 인증이 필수다. 토큰 기반 인증을 통해 방 접근을 제어하고, 메시지 크기 및 형태를 검증해 악의적 입력을 차단한다. 또한 TURN 서버를 활용하면 NAT/방화벽 환경에서도 연결 안정성이 높아진다.

문제 해결 팁

  • ICE 연결 실패 시 콘솔의 ICE candidate 로그를 확인한다.
  • 로컬에서 작동하는 경우 HTTPS 없이도 테스트 가능하지만, 배포는 반드시 HTTPS 권장.
  • 멀티 피어 환경에서는 각 피어별로 PeerConnection을 생성해야 한다.

결론

WebRTC는 브라우저 간 고품질 실시간 미디어 통신을 가능하게 한다. 시그널링 서버는 연결 설정을 중계하는 역할만 담당하므로 구조가 단순하다. Node.js와 WebSocket으로 기본 서버를 빠르게 만들고, 필요에 따라 인증과 TURN을 추가하면 안정적인 실시간 영상 통화를 제공할 수 있다.

Node.js WebRTC 시그널링 예제 WebRTC 시그널링 서버 구현 Node 실시간 영상 통화 Node.js 시그널링 서버 Node.js RTCPeerConnection 예제 getUserMedia 실시간 영상 ws WebSocket WebRTC TURN 서버 구성