React Native WebRTC 실시간 통신 구현: 미디어 스트림부터 P2P 연결까지

React Native 환경에서 WebRTC를 활용하여 실시간 영상 통신 기능을 구축하는 방법을 알아봅니다. 모바일 앱에서 피어 간 직접 연결을 구성하고 미디어 데이터를 교환하는 전체 프로세스를 다룹니다.

필수 컴포넌트 불러오기

라이브러리에서 필요한 클래스와 인터페이스를 가져옵니다:

import {
    RTCView,
    mediaDevices,
    MediaStream,
    RTCIceCandidate,
    RTCSessionDescription,
    RTCPeerConnection,
    MediaStreamTrack,
    registerGlobals,
    ScreenCapturePickerView
} from 'react-native-webrtc';

각 구성요소의 역할:

  • mediaDevices: 카메라/마이크 접근 및 장치 관리
  • RTCPeerConnection: 상대방과의 P2P 연결 관리
  • RTCView: 로컬/원격 비디오 스트림 렌더링
  • RTCSessionDescription/RTCIceCandidate: 연결 협상 정보

브라우저 호환성 설정

웹 환경과의 API 일관성이 필요한 경우 전역 등록을 수행합니다:

registerGlobals();

실행 후 navigator.mediaDeviceswindow.RTCPeerConnection 등이 전역에서 사용 가능합니다.

미디어 장치 탐색

사용 가능한 장치 조회

async function scanDevices() {
    try {
        const deviceList = await mediaDevices.enumerateDevices();
        const videoDevices = deviceList.filter(
            d => d.kind === 'videoinput'
        );
        console.log(`사용 가능한 카메라: ${videoDevices.length}대`);
        return videoDevices;
    } catch (err) {
        console.error('장치 검색 오류:', err);
        return [];
    }
}

캡처 품질 설정

const captureConfig = {
    audio: {
        echoCancellation: true,
        noiseSuppression: true
    },
    video: {
        width: { ideal: 1280, min: 640 },
        height: { ideal: 720, min: 480 },
        frameRate: { ideal: 24, max: 30 },
        facingMode: 'environment'
    }
};

스트림 획득 및 관리

카메라 스트림 시작

let activeStream = null;

async function initializeCamera() {
    try {
        activeStream = await mediaDevices.getUserMedia(captureConfig);
        
        // 오디오 전용 모드 전환 시
        if (audioOnlyMode) {
            activeStream.getVideoTracks().forEach(t => {
                t.enabled = false;
            });
        }
    } catch (err) {
        console.error('카메라 접근 실패:', err);
    }
}

화면 공유 스트림

async function beginScreenCapture() {
    try {
        activeStream = await mediaDevices.getDisplayMedia({
            video: true,
            audio: true
        });
    } catch (err) {
        console.error('화면 공사 시작 실패:', err);
    }
}

리소스 정리

function terminateStream() {
    if (!activeStream) return;
    
    activeStream.getTracks().forEach(track => {
        track.stop();
    });
    activeStream = null;
}

P2P 연결 구성

ICE 서버 설정

const connectionConfig = {
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
        { urls: 'stun:stun1.l.google.com:19302' }
    ],
    iceCandidatePoolSize: 10
};

PeerConnection 인스턴스화

const pc = new RTCPeerConnection(connectionConfig);

pc.addEventListener('icecandidate', onIceCandidate);
pc.addEventListener('track', onRemoteTrack);
pc.addEventListener('connectionstatechange', onStateChange);

로컬 트랙 등록

function attachLocalMedia() {
    activeStream.getTracks().forEach(track => {
        pc.addTrack(track, activeStream);
    });
}

시그널링 교환

Offer 생성 및 전송

async function generateOffer() {
    try {
        const offer = await pc.createOffer({
            offerToReceiveAudio: true,
            offerToReceiveVideo: true
        });
        await pc.setLocalDescription(offer);
        
        signalingServer.emit('signal', {
            type: 'offer',
            payload: offer
        });
    } catch (err) {
        console.error('Offer 생성 실패:', err);
    }
}

Answer 수신 처리

async function processAnswer(remoteAnswer) {
    try {
        const desc = new RTCSessionDescription(remoteAnswer);
        await pc.setRemoteDescription(desc);
    } catch (err) {
        console.error('원격 설명 설정 오류:', err);
    }
}

데이터 채널 활용

채널 생성

const messageChannel = pc.createDataChannel('messaging', {
    ordered: true,
    maxRetransmits: 3
});

messageChannel.onopen = () => {
    console.log('메시지 채널 활성화');
};

messageChannel.onmessage = ({ data }) => {
    handleIncomingData(data);
};

메시지 전송

function transmitData(payload) {
    if (messageChannel?.readyState === 'open') {
        messageChannel.send(JSON.stringify(payload));
    }
}

미디어 제어 기능

음소거 토글

function toggleAudioState() {
    const audioTracks = activeStream.getAudioTracks();
    audioTracks.forEach(track => {
        track.enabled = !track.enabled;
    });
    return audioTracks[0]?.enabled ?? false;
}

카메라 전환

let currentCamera = 'environment';

async function flipCamera() {
    const videoTrack = activeStream.getVideoTracks()[0];
    const newFacing = currentCamera === 'user' ? 'environment' : 'user';
    
    await videoTrack.applyConstraints({
        facingMode: newFacing
    });
    currentCamera = newFacing;
}

UI 렌더링

<RTCView
    streamURL={activeStream?.toURL()}
    style={styles.preview}
    objectFit="cover"
    mirror={currentCamera === 'user'}
    zOrder={2}
/>

<RTCView
    streamURL={remoteMedia?.toURL()}
    style={styles.remoteView}
    objectFit="contain"
    zOrder={1}
/>

운영 환경 고려사항

항목권장 설정
STUN/TURN코텔 서버 반드시 구성
권한Android: FOREGROUND_SERVICE, iOS: NSCameraUsageDescription
배터리화면 공유 시 Wake Lock 활성화
네트워크ICE 연결 상태 모니터링 및 재시도 로직

디버깅 체크포인트

  • pc.iceConnectionState: connected 상태 확인
  • pc.signalingState: stable 도달 여부
  • 원격 스트림 ontrack 이벤트 수신 타이밍
  • ICE candidate 수집 완료 후 연결 시도

위 구성으로 기본적인 1:1 영상 통화 기능을 구현할 수 있습니다. 다인원 통신, SFU 구조, 또는 녹화 기능이 필요한 경우 추가 아키텍처 설계가 필요합니다.

태그: React Native WebRTC RTCPeerConnection RTCView MediaStream

8월 12일 01:29에 게시됨