IOSignal로 WebRTC 구현하기
대상: 브라우저 JavaScript, IOSignal 7.2.0, 1:1 음성·영상 통화. AI 안내에서 플랫폼별 자료를 먼저 선택할 수 있습니다.
WebRTC Lab 공개 데모에서 한 장치는 방 만들기, 다른 장치는 같은 채널로 방 참여를 누르세요. 링크·QR로 같은 채널과 서버 주소를 전달할 수 있습니다.
역할과 구현 범위
| 구성 | 담당하는 일 |
|---|---|
| IOSignal | 공개 채널을 통한 상대 발견, CID 직접 메시지로 SDP·ICE 전달 |
| WebRTC | 브라우저의 미디어 캡처, 연결 협상, 암호화된 음성·영상 전송 |
| STUN / TURN | 연결 후보 탐색 / 직접 연결이 불가능할 때 미디어 중계 |
| 예제 앱 | 방·역할·세션, 메시지 검증, 재시도·종료, 화면 제어 |
영상·음성은 IOSignal 서버로 보내지 않습니다. QR은 초대 URL을 전달하며 NAT를 통과시키는 장치가 아닙니다. 상세한 ICE 동작은 WebRTC peer connections, TURN 서버를 참고하세요.
아래 메시지 형식과 개설자/참여자 역할은 WebRTC Lab의 응용 설계입니다. IOSignal이 이 메시지 이름을 예약하거나 방 인원을 서버에서 보장하는 것은 아닙니다.
먼저 익힐 IOSignal API
다음은 Vite 등 번들러 환경의 브라우저 코드입니다. 로컬 IOSignal 서버를 ws://localhost:7781에서 실행한 뒤 두 탭에 같은 코드를 로드하면 공개 채널로 발견하고 CID로 응답합니다. 이것만으로 화상통화가 만들어지는 것은 아닙니다.
import IO from 'iosignal/io';
const io = new IO();
const channel = 'webrtc-lab-learning';
io.on('message', (tag, message) => {
if (tag !== channel || message?.type !== 'hello') return;
if (typeof message.from !== 'string' || message.from === io.cid) return;
// 데모의 from은 클라이언트가 입력한 값이며 인증된 신원이 아닙니다.
io.signal(`${message.from}@rtc`, { type: 'welcome' });
});
io.on('@rtc', (tag, message) => {
console.log('직접 수신', tag, message); // @rtc, { type: 'welcome' }
});
io.on('ready', () => {
io.subscribe(channel);
io.signal(channel, { type: 'hello', from: io.cid });
});
io.on('error', () => console.error('IOSignal 연결 오류'));
io.open('ws://localhost:7781');
window.addEventListener('pagehide', () => io.destroy(), { once: true });
첫 탭이 준비된 뒤 두 번째 탭을 열면 두 번째 탭에 welcome이 표시되는 것이 최소 성공 조건입니다. 직접 메시지는 message가 아닌 @rtc 이벤트로 받으며, @rtc의 서버 구독은 필요하지 않습니다. API와 이벤트에서 전체 규칙을 확인하세요.
외부 접속에서는 두 장치가 같은 공개 WSS 서버를 사용합니다. #rtc처럼 HOME CHANNEL을 쓰면 서로 다른 외부 IP 사이의 발견 용도로 적합하지 않습니다. 이 예제는 webrtc-lab-방이름이라는 일반 공개 채널을 사용합니다.
1:1 연결 순서와 메시지
개설자 참여자
│ 공개 채널 knock ←───────────│
├──────── CID@rtc invite ───────────→│
│←─────── CID@rtc accept ────────────┤
├──────── CID@rtc offer ────────────→│
│←─────── CID@rtc answer ────────────┤
│←─────── CID@rtc ice ──────────────→│
│══════ WebRTC 음성·영상 연결 ═══════│
│←─────── CID@rtc bye ──────────────→│
ICE 이벤트는 SDP 생성·적용 중에도 발생하므로 위 도식처럼 항상 SDP 뒤에 도착하는 것은 아닙니다.
| type | 경로 | 의미 |
|---|---|---|
host | 공개 채널 | 개설 알림, 같은 채널의 다른 개설자 감지 |
knock | 공개 채널 | 참여자가 개설자를 탐색 |
invite / accept | CID 직접 | 상대 CID와 통화 세션 선택·수락 |
busy | CID 직접 | 이미 다른 상대가 선택된 상태 안내 |
offer / answer | CID 직접 | description: { type, sdp } 전달 |
ice | CID 직접 | candidate.toJSON() 결과 전달 |
bye | CID 직접 | 통화 종료 안내 |
공통 필드는 v, room, from, type, session입니다. v는 앱 메시지 버전이며 IOSignal 패키지 버전이 아닙니다. 발견 단계의 session은 null이고, 개설자가 상대를 선택하면서 새 세션 ID를 만듭니다. from은 응용 필드이므로 암호학적으로 검증된 발신자 ID가 아닙니다.
공개 데모의 정책은 참여자 탐색과 미디어 연결 대기에 각각 45초 상한을 두고, 참여 요청을 2.5초마다 재전송하는 것입니다. 숫자는 앱의 정책이며 IOSignal의 고정 제한이 아닙니다.
SDP·ICE와 비동기 처리
- 사용자의 참여 동작으로
getUserMedia()를 호출하고 성공한 스트림을 보관합니다. - 개설자는
accept이후RTCPeerConnection을 한 번만 만들고 로컬 트랙을 추가합니다. - 개설자는 offer를 생성·적용한 뒤 상대에게 전송합니다. 참여자는 offer를 remote description으로 적용하고 answer를 생성·적용해 응답합니다.
- 양쪽
onicecandidate에서 유효한 후보를 직접 메시지로 보냅니다. remote description이 없으면 후보를 큐에 보관하고, 적용 후 순서대로addIceCandidate()합니다. - 수신 처리는 Promise 큐 등으로 직렬화합니다. 같은 연결에 SDP 적용을 동시에 실행하지 마세요.
- 방·상대 CID·세션 ID와 payload 형식·크기를 검사합니다. 중복
accept로 연결을 다시 만들지 않고, 이전 통화의 메시지는 무시합니다. - 모든 비동기 작업 뒤에 세션이 아직 활성 상태인지 확인합니다. 통화 종료 후 완료된 미디어 요청의 트랙도 즉시 정리합니다.
연결 실패나 거부된 ICE 후보를 로그로 구분하세요. 후보 하나의 실패가 모든 후보의 실패를 뜻하지 않습니다. 후보 큐와 이벤트 로그에는 크기 상한을 두세요.
초기 리디렉션과 실제 단절
wss://io.iosignal.net/ws는 초기 접속을 다른 IOSignal 서버로 넘길 수 있습니다. 이 과정에서 이전 소켓의 closed가 발생합니다. 모든 close에 즉시 destroy()하면 정상 초기 연결까지 중단할 수 있습니다.
WebRTC Lab은 다음 정책을 사용합니다.
- 최초
ready까지 최대 15초를 기다립니다. 정상 리디렉션 중 소켓 종료는 이 대기 안에서 처리합니다. ready이후 시그널 연결이 끊기면 통화를 종료하고 사용자가 다시 참여하게 합니다.- WebRTC
disconnected는 15초 동안 복구를 기다리고,failed또는 복구 실패 시 종료합니다.
이는 예제의 단순한 정책입니다. 시그널 재연결 후 통화를 유지하는 앱은 CID 변경·재구독·세션 재협상까지 별도로 설계해야 합니다.
종료와 화면 제어
종료 시 상대에게 가능한 경우 bye를 보내고, 이후 세션을 비활성화합니다. 다음 자원을 정리하세요.
- 탐색·통계·연결 대기 타이머
RTCPeerConnection.close()와io.destroy()- 로컬 스트림의 모든 트랙
stop() - 영상 요소의
srcObject와 대기 ICE 후보
마이크·카메라 토글은 해당 트랙의 enabled를 바꾸며, 장치 해제는 종료 시 stop()으로 수행합니다. 내 영상은 muted로 두고, 상대 음량은 원격 영상 요소에서 조절합니다. 모바일 브라우저의 음량·전체 화면·자동재생 제약은 별도 확인하세요. 자동재생이 거절되면 사용자가 누를 재생 버튼을 제공합니다.
다른 네트워크·공개 배포
- 화면은 HTTPS, 시그널 서버는 두 장치가 접근 가능한 WSS 주소를 사용합니다.
localhost는 링크를 연 각 장치 자신입니다. - 직접 연결이 불가능한 네트워크에는 유효한 TURN 서버가 필요합니다. TURN credential을 코드·초대 링크·로그에 넣지 마세요.
- 데모의 TURN 설정은 각 탭에서 입력합니다. 초대 링크는 서버 주소와 채널만 전달합니다.
iceTransportPolicy: 'relay'로 TURN 경로를 강제할 수 있습니다. 설정만 했다고 성공한 것은 아니며, 선택된 candidate pair의 candidate type이 relay인지와 실제 미디어 수신을 확인합니다.- 공개 채널명·QR은 접근 제어가 아닙니다. 이 데모의
busy는 앱의 인원 안내이며 인증된 서버 측 입장 제한이 아닙니다. - 일반 서비스로 확장할 때는 참여 인증·권한·만료되는 TURN 자격증명 발급을 별도로 구현합니다.
서버 주소와 연결 환경도 함께 확인하세요.
예제 소스 읽는 순서
기준 소스는 공개 iosignal-examples의 iosignal_webrtc입니다. 7.2.0 실행 예제 묶음에도 포함되어 있습니다.
| 파일 | 확인할 내용 |
|---|---|
package.json, package-lock.json | IOSignal 7.2.0, Node.js 조건, 실행·테스트 명령 |
src/main.js | begin, send, receive, createPeer, finish의 역할과 세션 검사 |
vite.config.js, server.js | 개발 웹 5180 / IOSignal 7781, 독립 서버 실행 |
tests/call.spec.js | 실제 IOSignal + 브라우저 통합 검증 |
README.md | 장치 테스트, 배포, 알려진 제약 |
예제 묶음의 iosignal_webrtc 폴더에서 실행합니다.
npm ci
npm run dev
로컬 화면은 http://localhost:5180입니다. 빌드에는 npm run build, 브라우저 테스트에는 npx playwright install chromium 후 npm test를 사용합니다. 빌드·preview는 IOSignal 서버를 자동 실행하지 않으며, 필요하면 npm run server를 별도로 실행합니다.
검증 기록과 성공 조건
2026-10-18, IOSignal JS 7.2.0으로 프로덕션 빌드와 로컬 통합 테스트 5개를 통과했습니다. 두 Chrome 컨텍스트의 원격 영상·오디오 트랙, 장치 제어, 세 번째 참여자 busy, 종료·재참여, 참여자 선입장, 권한 거부 복구, 사용자 지정 서버의 초대 URL·QR 왕복을 검사했습니다.
로컬 IOSignal 서버와 같은 컴퓨터의 가상 캡처 장치를 사용했습니다. 실제 카메라·마이크, Safari/iOS, 서로 다른 인터넷망, 공개 배포 상태, TURN relay 실측은 이번에 검증하지 않았습니다.
새 구현에서는 io.ready뿐 아니라 양쪽 peer 상태, 원격 영상 프레임·오디오 수신, 제어 동작, 종료 후 장치 해제를 확인합니다. TURN은 relay 후보가 실제 선택되었는지 별도로 검사합니다.