์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŽฅ WebRTC๋กœ ์‹ค์‹œ๊ฐ„ ์˜์ƒ ํ†ตํ™” ๋งŒ๋“ค๊ธฐ

๐ŸŽฅ WebRTC๋กœ ์‹ค์‹œ๊ฐ„ ์˜์ƒ ํ†ตํ™” ๋งŒ๋“ค๊ธฐ

์นœ๊ตฌ์ฒ˜๋Ÿผ ์‰ฝ๊ฒŒ ๋ฐฐ์šฐ๋Š” ํ™”์ƒ ํ†ตํ™” ๊ตฌํ˜„ ์™„๋ฒฝ ๊ฐ€์ด๋“œ ๐Ÿ“นโœจ
์•ˆ๋…•! ๐Ÿ˜Š ์š”์ฆ˜ ํ™”์ƒ ํšŒ์˜, ์˜จ๋ผ์ธ ์ˆ˜์—…, ์›๊ฒฉ ์ง„๋ฃŒ ๋“ฑ ์‹ค์‹œ๊ฐ„ ์˜์ƒ ํ†ตํ™”๊ฐ€ ์ผ์ƒ์ด ๋˜์—ˆ์ง€? ์คŒ(Zoom)์ด๋‚˜ ๊ตฌ๊ธ€ ๋ฏธํŠธ(Google Meet) ๊ฐ™์€ ์„œ๋น„์Šค๋ฅผ ์“ฐ๋ฉด์„œ "๋‚˜๋„ ์ด๋Ÿฐ ๊ฑฐ ๋งŒ๋“ค ์ˆ˜ ์žˆ์„๊นŒ?" ๊ถ๊ธˆํ–ˆ๋˜ ์  ์žˆ์„ ๊ฑฐ์•ผ.

์˜ค๋Š˜์€ ๋ฐ”๋กœ ๊ทธ ๊ถ๊ธˆ์ฆ์„ ํ•ด๊ฒฐํ•ด์ค„ WebRTC(Web Real-Time Communication)์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณผ๊ฒŒ! ๐Ÿš€ ๋ณต์žกํ•ด ๋ณด์ด์ง€๋งŒ, ์ฐจ๊ทผ์ฐจ๊ทผ ๋”ฐ๋ผ์˜ค๋ฉด ์ƒ๊ฐ๋ณด๋‹ค ์žฌ๋ฏธ์žˆ๊ณ  ์‹ ๊ธฐํ•œ ๊ธฐ์ˆ ์ด์•ผ. ํ”„๋กœ๊ทธ๋ž˜๋ฐ์— ๊ด€์‹ฌ ์žˆ๋Š” ์นœ๊ตฌ๋“ค์ด๋ผ๋ฉด ๊ผญ ์•Œ์•„๋‘๋ฉด ์ข‹์„ JavaScript ๊ธฐ๋ฐ˜ ๊ธฐ์ˆ ์ด๋‹ˆ๊นŒ ์ง‘์ค‘ํ•ด๋ณด์ž!
์‚ฌ์šฉ์ž A ๐Ÿ“น ๋ธŒ๋ผ์šฐ์ € ์‚ฌ์šฉ์ž B ๐Ÿ“น ๋ธŒ๋ผ์šฐ์ € WebRTC P2P ์—ฐ๊ฒฐ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์˜์ƒ/์Œ์„ฑ ์ „์†ก โšก ์ดˆ์ €์ง€์—ฐ ํ†ต์‹  ํ”Œ๋Ÿฌ๊ทธ์ธ ๋ถˆํ•„์š” ๋ธŒ๋ผ์šฐ์ € ๋‚ด์žฅ API ๋ฌด๋ฃŒ ์˜คํ”ˆ์†Œ์Šค

๐ŸŽฏ WebRTC๊ฐ€ ๋ญ๊ธธ๋ž˜?

WebRTC๋Š” Web Real-Time Communication์˜ ์•ฝ์ž์•ผ. ์ด๋ฆ„์—์„œ ์•Œ ์ˆ˜ ์žˆ๋“ฏ์ด ์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์ฃผ๋Š” ๊ธฐ์ˆ ์ด์ง€! ๐ŸŒ

์˜ˆ์ „์—๋Š” ํ™”์ƒ ํ†ตํ™”๋ฅผ ํ•˜๋ ค๋ฉด ํ”Œ๋ž˜์‹œ(Flash)๋‚˜ ํŠน๋ณ„ํ•œ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์„ค์น˜ํ•ด์•ผ ํ–ˆ์–ด. ๊ธฐ์–ต๋‚˜? ๊ทธ๋Ÿฐ๋ฐ WebRTC๋Š” ๋ณ„๋„์˜ ์„ค์น˜ ์—†์ด ๋ธŒ๋ผ์šฐ์ €๋งŒ ์žˆ์œผ๋ฉด ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด. ํฌ๋กฌ, ํŒŒ์ด์–ดํญ์Šค, ์‚ฌํŒŒ๋ฆฌ, ์—ฃ์ง€ ๋“ฑ ์ฃผ์š” ๋ธŒ๋ผ์šฐ์ €๋“ค์ด ๋ชจ๋‘ ์ง€์›ํ•˜๊ณ  ์žˆ์ง€! ๐ŸŽ‰

๊ฐ€์žฅ ๋ฉ‹์ง„ ์ ์€ P2P(Peer-to-Peer) ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค๋Š” ๊ฑฐ์•ผ. ์„œ๋ฒ„๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๊ณ  ์‚ฌ์šฉ์ž๋ผ๋ฆฌ ์ง์ ‘ ์—ฐ๊ฒฐ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์ง€์—ฐ ์‹œ๊ฐ„์ด ์—„์ฒญ ์งง์•„. ๊ฒŒ์ž„ ์ŠคํŠธ๋ฆฌ๋ฐ์ด๋‚˜ ํ™”์ƒ ํšŒ์˜์ฒ˜๋Ÿผ ์‹ค์‹œ๊ฐ„์„ฑ์ด ์ค‘์š”ํ•œ ์„œ๋น„์Šค์— ๋”ฑ์ด์ง€! โšก
๐Ÿ’ก WebRTC์˜ ํ•ต์‹ฌ ํŠน์ง•
  • โœ… ๋ฌด๋ฃŒ ์˜คํ”ˆ์†Œ์Šค: ๊ตฌ๊ธ€์ด ์ฃผ๋„ํ•˜๋Š” ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ
  • โœ… ํ”Œ๋Ÿฌ๊ทธ์ธ ๋ถˆํ•„์š”: ๋ธŒ๋ผ์šฐ์ € ์ž์ฒด์— ๋‚ด์žฅ๋œ API
  • โœ… ์ดˆ์ €์ง€์—ฐ: P2P ์ง์ ‘ ์—ฐ๊ฒฐ๋กœ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ 
  • โœ… ๋‹ค์–‘ํ•œ ๋ฏธ๋””์–ด: ์˜์ƒ, ์Œ์„ฑ, ๋ฐ์ดํ„ฐ ๋ชจ๋‘ ์ „์†ก ๊ฐ€๋Šฅ
  • โœ… ๋ณด์•ˆ: ๊ธฐ๋ณธ์ ์œผ๋กœ ์•”ํ˜ธํ™”(DTLS, SRTP) ์ ์šฉ
  • โœ… ํฌ๋กœ์Šค ํ”Œ๋žซํผ: ์›น, ๋ชจ๋ฐ”์ผ, ๋ฐ์Šคํฌํ†ฑ ๋ชจ๋‘ ์ง€์›

๐Ÿ—๏ธ WebRTC์˜ ๊ตฌ์กฐ์™€ ํ•ต์‹ฌ API

WebRTC๋ฅผ ์ดํ•ดํ•˜๋ ค๋ฉด ์„ธ ๊ฐ€์ง€ ํ•ต์‹ฌ API๋ฅผ ์•Œ์•„์•ผ ํ•ด. ๊ฐ๊ฐ์˜ ์—ญํ• ์ด ๋ช…ํ™•ํ•˜๋‹ˆ๊นŒ ํ•˜๋‚˜์”ฉ ์‚ดํŽด๋ณด์ž! ๐Ÿ“š

1๏ธโƒฃ MediaStream API (getUserMedia)

์ด๊ฑด ์นด๋ฉ”๋ผ์™€ ๋งˆ์ดํฌ์— ์ ‘๊ทผํ•˜๋Š” API์•ผ. ์‚ฌ์šฉ์ž์˜ ์˜์ƒ๊ณผ ์Œ์„ฑ์„ ์บก์ฒ˜ํ•ด์„œ ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋งŒ๋“ค์–ด์ฃผ์ง€. ๐ŸŽค๐Ÿ“ท

๊ฐ„๋‹จํ•œ ์˜ˆ์ œ๋ฅผ ๋ณผ๊นŒ?
// ์นด๋ฉ”๋ผ์™€ ๋งˆ์ดํฌ ์ ‘๊ทผํ•˜๊ธฐ
async function startCamera() {
    try {
        const stream = await navigator.mediaDevices.getUserMedia({
            video: true,  // ๋น„๋””์˜ค ํ™œ์„ฑํ™”
            audio: true   // ์˜ค๋””์˜ค ํ™œ์„ฑํ™”
        });
        
        // ๋น„๋””์˜ค ์š”์†Œ์— ์ŠคํŠธ๋ฆผ ์—ฐ๊ฒฐ
        const videoElement = document.getElementById('myVideo');
        videoElement.srcObject = stream;
        
        console.log('์นด๋ฉ”๋ผ ์‹œ์ž‘ ์„ฑ๊ณต! ๐ŸŽ‰');
    } catch (error) {
        console.error('์นด๋ฉ”๋ผ ์ ‘๊ทผ ์‹คํŒจ:', error);
    }
}
๐Ÿ” ์‹ค์ „ ํŒ!
getUserMedia๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ถŒํ•œ์„ ์š”์ฒญํ•ด. ๋ฐ˜๋“œ์‹œ HTTPS ํ™˜๊ฒฝ์—์„œ๋งŒ ์ž‘๋™ํ•œ๋‹ค๋Š” ์  ๊ธฐ์–ตํ•ด! localhost๋Š” ์˜ˆ์™ธ์ ์œผ๋กœ HTTP๋„ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ์‹ค์ œ ๋ฐฐํฌํ•  ๋•Œ๋Š” SSL ์ธ์ฆ์„œ๊ฐ€ ํ•„์ˆ˜์•ผ. ๐Ÿ”’
๋” ์„ธ๋ฐ€ํ•œ ์„ค์ •๋„ ๊ฐ€๋Šฅํ•ด. ํ•ด์ƒ๋„, ํ”„๋ ˆ์ž„๋ ˆ์ดํŠธ, ์ „๋ฉด/ํ›„๋ฉด ์นด๋ฉ”๋ผ ์„ ํƒ ๋“ฑ์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์ง€:
const constraints = {
    video: {
        width: { ideal: 1280 },
        height: { ideal: 720 },
        frameRate: { ideal: 30 },
        facingMode: 'user'  // 'user'๋Š” ์ „๋ฉด, 'environment'๋Š” ํ›„๋ฉด
    },
    audio: {
        echoCancellation: true,  // ์—์ฝ” ์ œ๊ฑฐ
        noiseSuppression: true,  // ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ
        autoGainControl: true    // ์ž๋™ ๋ณผ๋ฅจ ์กฐ์ ˆ
    }
};

const stream = await navigator.mediaDevices.getUserMedia(constraints);

2๏ธโƒฃ RTCPeerConnection API

์ด๊ฒŒ ๋ฐ”๋กœ WebRTC์˜ ์‹ฌ์žฅ์ด์•ผ! ๐Ÿ’“ ๋‘ ์‚ฌ์šฉ์ž ๊ฐ„์˜ P2P ์—ฐ๊ฒฐ์„ ๊ด€๋ฆฌํ•˜๊ณ , ์‹ค์ œ ๋ฏธ๋””์–ด ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›๋Š” ์—ญํ• ์„ ํ•ด.

RTCPeerConnection์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ผ๋“ค์„ ์ฒ˜๋ฆฌํ•ด:
โ€ข ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์„ค์ • ๋ฐ ์œ ์ง€
โ€ข ๋ฏธ๋””์–ด ์ŠคํŠธ๋ฆผ ์†ก์ˆ˜์‹ 
โ€ข ๋Œ€์—ญํญ ๊ด€๋ฆฌ ๋ฐ ์ตœ์ ํ™”
โ€ข ์•”ํ˜ธํ™” ์ฒ˜๋ฆฌ
โ€ข ์—ฐ๊ฒฐ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง
// PeerConnection ์ƒ์„ฑ
const configuration = {
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },  // ๊ตฌ๊ธ€ STUN ์„œ๋ฒ„
        { urls: 'stun:stun1.l.google.com:19302' }
    ]
};

const peerConnection = new RTCPeerConnection(configuration);

// ๋กœ์ปฌ ์ŠคํŠธ๋ฆผ ์ถ”๊ฐ€
stream.getTracks().forEach(track => {
    peerConnection.addTrack(track, stream);
});

// ์›๊ฒฉ ์ŠคํŠธ๋ฆผ ์ˆ˜์‹  ์ฒ˜๋ฆฌ
peerConnection.ontrack = (event) => {
    const remoteVideo = document.getElementById('remoteVideo');
    remoteVideo.srcObject = event.streams[0];
    console.log('์ƒ๋Œ€๋ฐฉ ์˜์ƒ ์ˆ˜์‹ ! ๐Ÿ‘€');
};
๐Ÿค” STUN๊ณผ TURN ์„œ๋ฒ„๊ฐ€ ๋ญ์•ผ?

P2P ์—ฐ๊ฒฐ์ด ์ด์ƒ์ ์ด์ง€๋งŒ, ๋ฐฉํ™”๋ฒฝ์ด๋‚˜ NAT ๋•Œ๋ฌธ์— ์ง์ ‘ ์—ฐ๊ฒฐ์ด ์•ˆ ๋  ๋•Œ๊ฐ€ ์žˆ์–ด. ์ด๋•Œ ํ•„์š”ํ•œ ๊ฒŒ:

STUN (Session Traversal Utilities for NAT)
โ†’ ์ž์‹ ์˜ ๊ณต์ธ IP ์ฃผ์†Œ๋ฅผ ์•Œ์•„๋‚ด๋Š” ์„œ๋ฒ„. ๋ฌด๋ฃŒ๋กœ ๋งŽ์ด ์ œ๊ณต๋ผ! ๐Ÿ†“

TURN (Traversal Using Relays around NAT)
โ†’ ์ง์ ‘ ์—ฐ๊ฒฐ์ด ๋ถˆ๊ฐ€๋Šฅํ•  ๋•Œ ์ค‘๊ณ„ ์—ญํ• ์„ ํ•˜๋Š” ์„œ๋ฒ„. ํŠธ๋ž˜ํ”ฝ ๋น„์šฉ์ด ๋“ค์–ด์„œ ์œ ๋ฃŒ๊ฐ€ ๋งŽ์•„. ๐Ÿ’ฐ

3๏ธโƒฃ RTCDataChannel API

์˜์ƒ๊ณผ ์Œ์„ฑ๋งŒ ์ „์†กํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ์•ผ! RTCDataChannel์„ ์‚ฌ์šฉํ•˜๋ฉด ์ž„์˜์˜ ๋ฐ์ดํ„ฐ๋„ ์ฃผ๊ณ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด. ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€, ํŒŒ์ผ, ๊ฒŒ์ž„ ๋ฐ์ดํ„ฐ ๋“ฑ ๋ญ๋“ ์ง€ ๊ฐ€๋Šฅํ•˜์ง€! ๐Ÿ“จ
// ๋ฐ์ดํ„ฐ ์ฑ„๋„ ์ƒ์„ฑ
const dataChannel = peerConnection.createDataChannel('chat');

// ๋ฉ”์‹œ์ง€ ์ „์†ก
dataChannel.send('์•ˆ๋…•ํ•˜์„ธ์š”! ๐Ÿ‘‹');

// ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ 
dataChannel.onmessage = (event) => {
    console.log('๋ฐ›์€ ๋ฉ”์‹œ์ง€:', event.data);
};

// ์—ฐ๊ฒฐ ์ƒํƒœ ํ™•์ธ
dataChannel.onopen = () => {
    console.log('๋ฐ์ดํ„ฐ ์ฑ„๋„ ์—ฐ๊ฒฐ๋จ! โœ…');
};

dataChannel.onclose = () => {
    console.log('๋ฐ์ดํ„ฐ ์ฑ„๋„ ์ข…๋ฃŒ๋จ! โŒ');
};

๐Ÿ”„ ์‹œ๊ทธ๋„๋ง(Signaling) ํ”„๋กœ์„ธ์Šค

์ž, ์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ๊ฐœ๋…์ด ๋‚˜์™€. WebRTC๋Š” P2P๋กœ ์—ฐ๊ฒฐ๋˜์ง€๋งŒ, ์ฒ˜์Œ ์—ฐ๊ฒฐ์„ ๋งบ์„ ๋•Œ๋Š” ์„œ๋ฒ„๊ฐ€ ํ•„์š”ํ•ด. ์ด ๊ณผ์ •์„ ์‹œ๊ทธ๋„๋ง(Signaling)์ด๋ผ๊ณ  ๋ถˆ๋Ÿฌ. ๐Ÿค

์ƒ๊ฐํ•ด๋ด. ๋‘ ์‚ฌ๋žŒ์ด ์ „ํ™” ํ†ตํ™”๋ฅผ ํ•˜๋ ค๋ฉด ๋จผ์ € ์ „ํ™”๋ฒˆํ˜ธ๋ฅผ ๊ตํ™˜ํ•ด์•ผ ํ•˜์ž–์•„? WebRTC๋„ ๋งˆ์ฐฌ๊ฐ€์ง€์•ผ. ์„œ๋กœ์˜ ๋„คํŠธ์›Œํฌ ์ •๋ณด๋ฅผ ๊ตํ™˜ํ•ด์•ผ ์ง์ ‘ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ์ง€!
์‚ฌ์šฉ์ž A Offer ์ƒ์„ฑ ์‚ฌ์šฉ์ž B Answer ์ƒ์„ฑ ์‹œ๊ทธ๋„๋ง ์„œ๋ฒ„ WebSocket/HTTP โ‘  Offer ์ „์†ก โ‘ก Offer ์ „๋‹ฌ โ‘ข Answer ์ „์†ก โ‘ฃ Answer ์ „๋‹ฌ ICE Candidate ๋„คํŠธ์›Œํฌ ์ •๋ณด ICE Candidate ๋„คํŠธ์›Œํฌ ์ •๋ณด โ‘ค ICE ๊ตํ™˜ โ‘ค ICE ๊ตํ™˜ P2P ์ง์ ‘ ์—ฐ๊ฒฐ ์™„๋ฃŒ! ์‹ค์‹œ๊ฐ„ ๋ฏธ๋””์–ด ์ŠคํŠธ๋ฆผ ์ „์†ก SDP ๊ตํ™˜ ICE Candidate ์ˆ˜์ง‘ P2P ์—ฐ๊ฒฐ

๐Ÿ“ SDP (Session Description Protocol)

SDP๋Š” ์„ธ์…˜ ์ •๋ณด๋ฅผ ๋‹ด์€ ํ…์ŠคํŠธ ํ˜•์‹์ด์•ผ. ์–ด๋–ค ์ฝ”๋ฑ์„ ์‚ฌ์šฉํ• ์ง€, ํ•ด์ƒ๋„๋Š” ์–ผ๋งˆ์ธ์ง€, ๋„คํŠธ์›Œํฌ ์ •๋ณด๋Š” ๋ญ”์ง€ ๋“ฑ์„ ๋‹ด๊ณ  ์žˆ์ง€. ๐Ÿ“„

์—ฐ๊ฒฐ ๊ณผ์ •์€ ์ด๋ ‡๊ฒŒ ์ง„ํ–‰๋ผ:
Step 1: Offer ์ƒ์„ฑ (ํ˜ธ์ถœ์ž)
์‚ฌ์šฉ์ž A๊ฐ€ ์—ฐ๊ฒฐ์„ ์‹œ์ž‘ํ•˜๋ฉด์„œ ์ž์‹ ์˜ SDP Offer๋ฅผ ์ƒ์„ฑํ•ด.
// Offer ์ƒ์„ฑ
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);

// ์‹œ๊ทธ๋„๋ง ์„œ๋ฒ„๋กœ ์ „์†ก
signalingServer.send({
    type: 'offer',
    sdp: offer
});
Step 2: Answer ์ƒ์„ฑ (์ˆ˜์‹ ์ž)
์‚ฌ์šฉ์ž B๊ฐ€ Offer๋ฅผ ๋ฐ›๊ณ  Answer๋ฅผ ์ƒ์„ฑํ•ด์„œ ์‘๋‹ตํ•ด.
// Offer ์ˆ˜์‹  ๋ฐ Answer ์ƒ์„ฑ
await peerConnection.setRemoteDescription(receivedOffer);
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);

// ์‹œ๊ทธ๋„๋ง ์„œ๋ฒ„๋กœ ์ „์†ก
signalingServer.send({
    type: 'answer',
    sdp: answer
});

๐ŸงŠ ICE (Interactive Connectivity Establishment)

ICE๋Š” ์ตœ์ ์˜ ๋„คํŠธ์›Œํฌ ๊ฒฝ๋กœ๋ฅผ ์ฐพ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. ์—ฌ๋Ÿฌ ๋„คํŠธ์›Œํฌ ์ธํ„ฐํŽ˜์ด์Šค(Wi-Fi, ์ด๋”๋„ท, ๋ชจ๋ฐ”์ผ ๋“ฑ)๋ฅผ ํƒ์ƒ‰ํ•˜๊ณ , ๊ฐ€์žฅ ๋น ๋ฅธ ๊ฒฝ๋กœ๋กœ ์—ฐ๊ฒฐ์„ ์‹œ๋„ํ•˜์ง€. ๐Ÿ”

ICE Candidate๋Š” ์—ฐ๊ฒฐ ๊ฐ€๋Šฅํ•œ ๋„คํŠธ์›Œํฌ ์ฃผ์†Œ ์ •๋ณด์•ผ. ์ด๊ฒƒ๋„ ์‹œ๊ทธ๋„๋ง์„ ํ†ตํ•ด ๊ตํ™˜ํ•ด์•ผ ํ•ด:
// ICE Candidate ์ˆ˜์ง‘
peerConnection.onicecandidate = (event) => {
    if (event.candidate) {
        signalingServer.send({
            type: 'ice-candidate',
            candidate: event.candidate
        });
    }
};

// ์ƒ๋Œ€๋ฐฉ์˜ ICE Candidate ์ถ”๊ฐ€
function addIceCandidate(candidate) {
    peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
}

๐Ÿ› ๏ธ ์‹ค์ „ ๊ตฌํ˜„: 1๋Œ€1 ํ™”์ƒ ํ†ตํ™” ๋งŒ๋“ค๊ธฐ

์ด๋ก ์€ ์ถฉ๋ถ„ํžˆ ๋ฐฐ์› ์œผ๋‹ˆ, ์ด์ œ ์‹ค์ œ๋กœ ๋™์ž‘ํ•˜๋Š” ํ™”์ƒ ํ†ตํ™” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค์–ด๋ณด์ž! ๐Ÿš€ ์‹œ๊ทธ๋„๋ง ์„œ๋ฒ„๋กœ๋Š” WebSocket์„ ์‚ฌ์šฉํ•  ๊ฑฐ์•ผ.

๐Ÿ–ฅ๏ธ ์„œ๋ฒ„ ์ธก ์ฝ”๋“œ (Node.js + Socket.io)

๋จผ์ € ์‹œ๊ทธ๋„๋ง ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•ด. Socket.io๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด:
// server.js
const express = require('express');
const app = express();
const server = require('http').createServer(app);
const io = require('socket.io')(server);

const rooms = new Map();

io.on('connection', (socket) => {
    console.log('์ƒˆ๋กœ์šด ์‚ฌ์šฉ์ž ์—ฐ๊ฒฐ:', socket.id);
    
    // ๋ฐฉ ์ž…์žฅ
    socket.on('join-room', (roomId) => {
        socket.join(roomId);
        
        if (!rooms.has(roomId)) {
            rooms.set(roomId, []);
        }
        
        const room = rooms.get(roomId);
        room.push(socket.id);
        
        // ๋ฐฉ์— ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๊ฐ€ ์žˆ์œผ๋ฉด ์•Œ๋ฆผ
        if (room.length > 1) {
            socket.to(roomId).emit('user-connected', socket.id);
        }
        
        console.log(`${socket.id}๊ฐ€ ${roomId} ๋ฐฉ์— ์ž…์žฅ`);
    });
    
    // Offer ์ „๋‹ฌ
    socket.on('offer', (data) => {
        socket.to(data.target).emit('offer', {
            sdp: data.sdp,
            sender: socket.id
        });
    });
    
    // Answer ์ „๋‹ฌ
    socket.on('answer', (data) => {
        socket.to(data.target).emit('answer', {
            sdp: data.sdp,
            sender: socket.id
        });
    });
    
    // ICE Candidate ์ „๋‹ฌ
    socket.on('ice-candidate', (data) => {
        socket.to(data.target).emit('ice-candidate', {
            candidate: data.candidate,
            sender: socket.id
        });
    });
    
    // ์—ฐ๊ฒฐ ํ•ด์ œ
    socket.on('disconnect', () => {
        console.log('์‚ฌ์šฉ์ž ์—ฐ๊ฒฐ ํ•ด์ œ:', socket.id);
        
        rooms.forEach((room, roomId) => {
            const index = room.indexOf(socket.id);
            if (index !== -1) {
                room.splice(index, 1);
                socket.to(roomId).emit('user-disconnected', socket.id);
            }
        });
    });
});

server.listen(3000, () => {
    console.log('์‹œ๊ทธ๋„๋ง ์„œ๋ฒ„ ์‹คํ–‰ ์ค‘: http://localhost:3000');
});

๐Ÿ’ป ํด๋ผ์ด์–ธํŠธ ์ธก ์ฝ”๋“œ (JavaScript)

์ด์ œ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹คํ–‰๋  ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž:
// client.js
const socket = io('http://localhost:3000');
const roomId = 'room-123';  // ์‹ค์ œ๋กœ๋Š” ๋™์ ์œผ๋กœ ์ƒ์„ฑ

let localStream;
let peerConnection;
let remoteSocketId;

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

// ์ดˆ๊ธฐํ™”
async function init() {
    try {
        // ๋กœ์ปฌ ๋ฏธ๋””์–ด ์ŠคํŠธ๋ฆผ ๊ฐ€์ ธ์˜ค๊ธฐ
        localStream = await navigator.mediaDevices.getUserMedia({
            video: { width: 1280, height: 720 },
            audio: true
        });
        
        document.getElementById('localVideo').srcObject = localStream;
        
        // ๋ฐฉ ์ž…์žฅ
        socket.emit('join-room', roomId);
        
    } catch (error) {
        console.error('๋ฏธ๋””์–ด ์ ‘๊ทผ ์˜ค๋ฅ˜:', error);
        alert('์นด๋ฉ”๋ผ/๋งˆ์ดํฌ ์ ‘๊ทผ ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค!');
    }
}

// PeerConnection ์ƒ์„ฑ
function createPeerConnection() {
    peerConnection = new RTCPeerConnection(configuration);
    
    // ๋กœ์ปฌ ์ŠคํŠธ๋ฆผ ์ถ”๊ฐ€
    localStream.getTracks().forEach(track => {
        peerConnection.addTrack(track, localStream);
    });
    
    // ์›๊ฒฉ ์ŠคํŠธ๋ฆผ ์ˆ˜์‹ 
    peerConnection.ontrack = (event) => {
        const remoteVideo = document.getElementById('remoteVideo');
        remoteVideo.srcObject = event.streams[0];
        console.log('์›๊ฒฉ ์ŠคํŠธ๋ฆผ ์ˆ˜์‹  ์™„๋ฃŒ! ๐ŸŽ‰');
    };
    
    // ICE Candidate ์ฒ˜๋ฆฌ
    peerConnection.onicecandidate = (event) => {
        if (event.candidate) {
            socket.emit('ice-candidate', {
                target: remoteSocketId,
                candidate: event.candidate
            });
        }
    };
    
    // ์—ฐ๊ฒฐ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง
    peerConnection.onconnectionstatechange = () => {
        console.log('์—ฐ๊ฒฐ ์ƒํƒœ:', peerConnection.connectionState);
        
        if (peerConnection.connectionState === 'connected') {
            console.log('P2P ์—ฐ๊ฒฐ ์„ฑ๊ณต! โœ…');
        }
    };
    
    return peerConnection;
}

// ์ƒˆ ์‚ฌ์šฉ์ž ์—ฐ๊ฒฐ ์‹œ
socket.on('user-connected', async (socketId) => {
    console.log('์ƒˆ ์‚ฌ์šฉ์ž ์ž…์žฅ:', socketId);
    remoteSocketId = socketId;
    
    // Offer ์ƒ์„ฑ ๋ฐ ์ „์†ก
    const pc = createPeerConnection();
    const offer = await pc.createOffer();
    await pc.setLocalDescription(offer);
    
    socket.emit('offer', {
        target: socketId,
        sdp: offer
    });
});

// Offer ์ˆ˜์‹ 
socket.on('offer', async (data) => {
    console.log('Offer ์ˆ˜์‹ :', data.sender);
    remoteSocketId = data.sender;
    
    const pc = createPeerConnection();
    await pc.setRemoteDescription(new RTCSessionDescription(data.sdp));
    
    // Answer ์ƒ์„ฑ ๋ฐ ์ „์†ก
    const answer = await pc.createAnswer();
    await pc.setLocalDescription(answer);
    
    socket.emit('answer', {
        target: data.sender,
        sdp: answer
    });
});

// Answer ์ˆ˜์‹ 
socket.on('answer', async (data) => {
    console.log('Answer ์ˆ˜์‹ :', data.sender);
    await peerConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
});

// ICE Candidate ์ˆ˜์‹ 
socket.on('ice-candidate', async (data) => {
    try {
        await peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate));
    } catch (error) {
        console.error('ICE Candidate ์ถ”๊ฐ€ ์˜ค๋ฅ˜:', error);
    }
});

// ์‚ฌ์šฉ์ž ์—ฐ๊ฒฐ ํ•ด์ œ
socket.on('user-disconnected', (socketId) => {
    console.log('์‚ฌ์šฉ์ž ํ‡ด์žฅ:', socketId);
    if (peerConnection) {
        peerConnection.close();
        peerConnection = null;
    }
    document.getElementById('remoteVideo').srcObject = null;
});

// ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ดˆ๊ธฐํ™”
window.addEventListener('load', init);

๐ŸŽจ HTML ๊ตฌ์กฐ

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebRTC ํ™”์ƒ ํ†ตํ™”</title>
    <style>
        .video-container {
            display: flex;
            gap: 20px;
            padding: 20px;
        }
        video {
            width: 45%;
            max-width: 600px;
            border-radius: 10px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.2);
        }
        #localVideo {
            transform: scaleX(-1); /* ๊ฑฐ์šธ ํšจ๊ณผ */
        }
    </style>
</head>
<body>
    <h1>WebRTC ํ™”์ƒ ํ†ตํ™” ๋ฐ๋ชจ</h1>
    
    <div class="video-container">
        <div>
            <h3>๋‚ด ํ™”๋ฉด</h3>
            <video id="localVideo" autoplay muted playsinline></video>
        </div>
        
        <div>
            <h3>์ƒ๋Œ€๋ฐฉ ํ™”๋ฉด</h3>
            <video id="remoteVideo" autoplay playsinline></video>
        </div>
    </div>
    
    <script src="/socket.io/socket.io.js"></script>
    <script src="client.js"></script>
</body>
</html>
๐ŸŽฏ ์ฃผ์˜์‚ฌํ•ญ!
โ€ข autoplay ์†์„ฑ: ๋น„๋””์˜ค ์ž๋™ ์žฌ์ƒ
โ€ข muted ์†์„ฑ: ๋กœ์ปฌ ๋น„๋””์˜ค๋Š” ์Œ์†Œ๊ฑฐ (์—์ฝ” ๋ฐฉ์ง€)
โ€ข playsinline ์†์„ฑ: iOS์—์„œ ์ „์ฒดํ™”๋ฉด ๋ฐฉ์ง€
โ€ข ๋กœ์ปฌ ๋น„๋””์˜ค์— scaleX(-1) ์ ์šฉํ•˜๋ฉด ๊ฑฐ์šธ์ฒ˜๋Ÿผ ๋ณด์—ฌ์„œ ์ž์—ฐ์Šค๋Ÿฌ์›Œ! ๐Ÿชž

๐ŸŽ›๏ธ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ

๐Ÿ”‡ ์Œ์†Œ๊ฑฐ / ๋น„๋””์˜ค ๋„๊ธฐ

ํ™”์ƒ ํšŒ์˜์—์„œ ํ•„์ˆ˜ ๊ธฐ๋Šฅ์ด์ง€! ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด:
// ์˜ค๋””์˜ค ํ† ๊ธ€
function toggleAudio() {
    const audioTrack = localStream.getAudioTracks()[0];
    if (audioTrack) {
        audioTrack.enabled = !audioTrack.enabled;
        console.log('์˜ค๋””์˜ค:', audioTrack.enabled ? '์ผœ์ง ๐Ÿ”Š' : '๊บผ์ง ๐Ÿ”‡');
        return audioTrack.enabled;
    }
}

// ๋น„๋””์˜ค ํ† ๊ธ€
function toggleVideo() {
    const videoTrack = localStream.getVideoTracks()[0];
    if (videoTrack) {
        videoTrack.enabled = !videoTrack.enabled;
        console.log('๋น„๋””์˜ค:', videoTrack.enabled ? '์ผœ์ง ๐Ÿ“น' : '๊บผ์ง ๐Ÿ“ด');
        return videoTrack.enabled;
    }
}

// ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ
document.getElementById('muteBtn').addEventListener('click', () => {
    const isEnabled = toggleAudio();
    // ๋ฒ„ํŠผ UI ์—…๋ฐ์ดํŠธ
    document.getElementById('muteBtn').textContent = isEnabled ? '๐Ÿ”Š' : '๐Ÿ”‡';
});

document.getElementById('videoBtn').addEventListener('click', () => {
    const isEnabled = toggleVideo();
    document.getElementById('videoBtn').textContent = isEnabled ? '๐Ÿ“น' : '๐Ÿ“ด';
});

๐Ÿ–ฅ๏ธ ํ™”๋ฉด ๊ณต์œ 

ํ”„๋ ˆ์  ํ…Œ์ด์…˜์ด๋‚˜ ์›๊ฒฉ ์ง€์›์— ์œ ์šฉํ•œ ํ™”๋ฉด ๊ณต์œ  ๊ธฐ๋Šฅ! getDisplayMedia API๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ผ:
async function startScreenShare() {
    try {
        const screenStream = await navigator.mediaDevices.getDisplayMedia({
            video: {
                cursor: 'always'  // ๋งˆ์šฐ์Šค ์ปค์„œ ํ‘œ์‹œ
            },
            audio: false
        });
        
        // ๊ธฐ์กด ๋น„๋””์˜ค ํŠธ๋ž™ ๊ต์ฒด
        const videoTrack = screenStream.getVideoTracks()[0];
        const sender = peerConnection.getSenders().find(s => 
            s.track && s.track.kind === 'video'
        );
        
        if (sender) {
            sender.replaceTrack(videoTrack);
        }
        
        // ๋กœ์ปฌ ๋น„๋””์˜ค์—๋„ ํ‘œ์‹œ
        document.getElementById('localVideo').srcObject = screenStream;
        
        // ํ™”๋ฉด ๊ณต์œ  ์ค‘๋‹จ ์‹œ ์ฒ˜๋ฆฌ
        videoTrack.onended = () => {
            stopScreenShare();
        };
        
        console.log('ํ™”๋ฉด ๊ณต์œ  ์‹œ์ž‘! ๐Ÿ–ฅ๏ธ');
        
    } catch (error) {
        console.error('ํ™”๋ฉด ๊ณต์œ  ์˜ค๋ฅ˜:', error);
    }
}

function stopScreenShare() {
    // ์›๋ž˜ ์นด๋ฉ”๋ผ๋กœ ๋ณต๊ตฌ
    const videoTrack = localStream.getVideoTracks()[0];
    const sender = peerConnection.getSenders().find(s => 
        s.track && s.track.kind === 'video'
    );
    
    if (sender) {
        sender.replaceTrack(videoTrack);
    }
    
    document.getElementById('localVideo').srcObject = localStream;
    console.log('ํ™”๋ฉด ๊ณต์œ  ์ข…๋ฃŒ! ๐Ÿ“น');
}

๐Ÿ’ฌ ์ฑ„ํŒ… ๊ธฐ๋Šฅ ์ถ”๊ฐ€

DataChannel์„ ํ™œ์šฉํ•œ ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… ๊ตฌํ˜„:
let dataChannel;

// PeerConnection ์ƒ์„ฑ ์‹œ DataChannel ์ถ”๊ฐ€
function createPeerConnection() {
    const pc = new RTCPeerConnection(configuration);
    
    // ... ๊ธฐ์กด ์ฝ”๋“œ ...
    
    // DataChannel ์ƒ์„ฑ (Offer ์ธก์—์„œ๋งŒ)
    dataChannel = pc.createDataChannel('chat');
    setupDataChannel(dataChannel);
    
    // DataChannel ์ˆ˜์‹  (Answer ์ธก)
    pc.ondatachannel = (event) => {
        dataChannel = event.channel;
        setupDataChannel(dataChannel);
    };
    
    return pc;
}

function setupDataChannel(channel) {
    channel.onopen = () => {
        console.log('์ฑ„ํŒ… ์ฑ„๋„ ์—ฐ๊ฒฐ๋จ! ๐Ÿ’ฌ');
        document.getElementById('chatInput').disabled = false;
    };
    
    channel.onmessage = (event) => {
        displayMessage(event.data, 'received');
    };
    
    channel.onclose = () => {
        console.log('์ฑ„ํŒ… ์ฑ„๋„ ์ข…๋ฃŒ๋จ! โŒ');
        document.getElementById('chatInput').disabled = true;
    };
}

function sendMessage() {
    const input = document.getElementById('chatInput');
    const message = input.value.trim();
    
    if (message && dataChannel && dataChannel.readyState === 'open') {
        dataChannel.send(message);
        displayMessage(message, 'sent');
        input.value = '';
    }
}

function displayMessage(text, type) {
    const chatBox = document.getElementById('chatBox');
    const messageDiv = document.createElement('div');
    messageDiv.className = `message ${type}`;
    messageDiv.textContent = text;
    chatBox.appendChild(messageDiv);
    chatBox.scrollTop = chatBox.scrollHeight;
}

๐Ÿ“Š ํ†ต๊ณ„ ๋ฐ ํ’ˆ์งˆ ๋ชจ๋‹ˆํ„ฐ๋ง

์—ฐ๊ฒฐ ํ’ˆ์งˆ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ™•์ธํ•˜๋Š” ๊ธฐ๋Šฅ๋„ ์ค‘์š”ํ•ด:
async function getConnectionStats() {
    if (!peerConnection) return;
    
    const stats = await peerConnection.getStats();
    let statsOutput = '';
    
    stats.forEach(report => {
        if (report.type === 'inbound-rtp' && report.kind === 'video') {
            statsOutput += `
                ๐Ÿ“ฅ ์ˆ˜์‹  ๋น„๋””์˜ค:
                - ํ•ด์ƒ๋„: ${report.frameWidth}x${report.frameHeight}
                - FPS: ${report.framesPerSecond}
                - ํŒจํ‚ท ์†์‹ค: ${report.packetsLost}
                - ์ง€ํ„ฐ: ${report.jitter}
            `;
        }
        
        if (report.type === 'outbound-rtp' && report.kind === 'video') {
            statsOutput += `
                ๐Ÿ“ค ์†ก์‹  ๋น„๋””์˜ค:
                - ๋น„ํŠธ๋ ˆ์ดํŠธ: ${(report.bytesSent * 8 / 1000).toFixed(2)} kbps
                - ์ „์†ก ํŒจํ‚ท: ${report.packetsSent}
            `;
        }
        
        if (report.type === 'candidate-pair' && report.state === 'succeeded') {
            statsOutput += `
                ๐ŸŒ ๋„คํŠธ์›Œํฌ:
                - RTT: ${report.currentRoundTripTime * 1000}ms
                - ์—ฐ๊ฒฐ ํƒ€์ž…: ${report.localCandidateType} โ†’ ${report.remoteCandidateType}
            `;
        }
    });
    
    console.log(statsOutput);
    return statsOutput;
}

// 5์ดˆ๋งˆ๋‹ค ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ
setInterval(() => {
    getConnectionStats();
}, 5000);

๐ŸŽญ ๋‹ค์ž๊ฐ„ ํ™”์ƒ ํšŒ์˜ ๊ตฌํ˜„ (SFU ๋ฐฉ์‹)

1๋Œ€1์€ P2P๋กœ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ, ์—ฌ๋Ÿฌ ๋ช…์ด ์ฐธ์—ฌํ•˜๋Š” ํšŒ์˜๋Š” ์–ด๋–ป๊ฒŒ ํ• ๊นŒ? ๐Ÿค”

์„ธ ๊ฐ€์ง€ ๋ฐฉ์‹์ด ์žˆ์–ด:
๋ฐฉ์‹ ์„ค๋ช… ์žฅ์  ๋‹จ์ 
Mesh (P2P) ๋ชจ๋“  ์ฐธ๊ฐ€์ž๊ฐ€ ์„œ๋กœ ์ง์ ‘ ์—ฐ๊ฒฐ ์ง€์—ฐ ์‹œ๊ฐ„ ์ตœ์†Œ, ์„œ๋ฒ„ ๋ถ€๋‹ด ์—†์Œ ์ฐธ๊ฐ€์ž ์ฆ๊ฐ€ ์‹œ ๋Œ€์—ญํญ ํญ์ฆ (3๋ช…๊นŒ์ง€ ๊ถŒ์žฅ)
SFU ์„œ๋ฒ„๊ฐ€ ์ŠคํŠธ๋ฆผ ์ค‘๊ณ„ (๋ฏน์‹ฑ ์—†์Œ) ํ™•์žฅ์„ฑ ์ข‹์Œ, ํ’ˆ์งˆ ์œ ์ง€ ์„œ๋ฒ„ ๋Œ€์—ญํญ ํ•„์š”
MCU ์„œ๋ฒ„๊ฐ€ ์ŠคํŠธ๋ฆผ ๋ฏน์‹ฑ ํด๋ผ์ด์–ธํŠธ ๋ถ€๋‹ด ์ตœ์†Œ ์„œ๋ฒ„ CPU ๋ถ€๋‹ด ํผ, ์ง€์—ฐ ๋ฐœ์ƒ
์‹ค๋ฌด์—์„œ๋Š” SFU(Selective Forwarding Unit) ๋ฐฉ์‹์„ ๊ฐ€์žฅ ๋งŽ์ด ์จ. ๋Œ€ํ‘œ์ ์ธ ์˜คํ”ˆ์†Œ์Šค SFU ์„œ๋ฒ„๋กœ๋Š” Mediasoup, Janus, Jitsi ๋“ฑ์ด ์žˆ์–ด. ๐ŸŽฏ

๊ฐ„๋‹จํ•œ SFU ๊ฐœ๋… ์ฝ”๋“œ๋ฅผ ๋ณด์ž:
// SFU ์„œ๋ฒ„ ์ธก (๊ฐ„์†Œํ™”๋œ ์˜ˆ์ œ)
class SFURoom {
    constructor(roomId) {
        this.roomId = roomId;
        this.participants = new Map();
    }
    
    addParticipant(socketId, peerConnection) {
        this.participants.set(socketId, peerConnection);
        
        // ์ƒˆ ์ฐธ๊ฐ€์ž์—๊ฒŒ ๊ธฐ์กด ์ฐธ๊ฐ€์ž๋“ค์˜ ์ŠคํŠธ๋ฆผ ์ „๋‹ฌ
        this.participants.forEach((pc, id) => {
            if (id !== socketId) {
                // ๊ธฐ์กด ์ฐธ๊ฐ€์ž์˜ ํŠธ๋ž™์„ ์ƒˆ ์ฐธ๊ฐ€์ž์—๊ฒŒ ์ „๋‹ฌ
                pc.getSenders().forEach(sender => {
                    if (sender.track) {
                        peerConnection.addTrack(sender.track);
                    }
                });
            }
        });
    }
    
    removeParticipant(socketId) {
        this.participants.delete(socketId);
        
        // ๋‹ค๋ฅธ ์ฐธ๊ฐ€์ž๋“ค์—๊ฒŒ ์•Œ๋ฆผ
        this.participants.forEach((pc, id) => {
            // ํ•ด๋‹น ์ฐธ๊ฐ€์ž์˜ ํŠธ๋ž™ ์ œ๊ฑฐ ๋กœ์ง
        });
    }
}

// ํด๋ผ์ด์–ธํŠธ ์ธก
class MultiPartyCall {
    constructor() {
        this.peerConnections = new Map();
        this.remoteStreams = new Map();
    }
    
    async addParticipant(participantId) {
        const pc = new RTCPeerConnection(configuration);
        
        // ๋กœ์ปฌ ์ŠคํŠธ๋ฆผ ์ถ”๊ฐ€
        localStream.getTracks().forEach(track => {
            pc.addTrack(track, localStream);
        });
        
        // ์›๊ฒฉ ์ŠคํŠธ๋ฆผ ์ˆ˜์‹ 
        pc.ontrack = (event) => {
            this.remoteStreams.set(participantId, event.streams[0]);
            this.displayRemoteVideo(participantId, event.streams[0]);
        };
        
        this.peerConnections.set(participantId, pc);
        return pc;
    }
    
    displayRemoteVideo(participantId, stream) {
        let videoElement = document.getElementById(`video-${participantId}`);
        
        if (!videoElement) {
            videoElement = document.createElement('video');
            videoElement.id = `video-${participantId}`;
            videoElement.autoplay = true;
            videoElement.playsinline = true;
            document.getElementById('remoteVideos').appendChild(videoElement);
        }
        
        videoElement.srcObject = stream;
    }
    
    removeParticipant(participantId) {
        const pc = this.peerConnections.get(participantId);
        if (pc) {
            pc.close();
            this.peerConnections.delete(participantId);
        }
        
        const videoElement = document.getElementById(`video-${participantId}`);
        if (videoElement) {
            videoElement.remove();
        }
        
        this.remoteStreams.delete(participantId);
    }
}

๐Ÿ” ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ

์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์—์„œ ๋ณด์•ˆ์€ ์ •๋ง ์ค‘์š”ํ•ด! WebRTC๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ณด์•ˆ ๊ธฐ๋Šฅ์ด ๋‚ด์žฅ๋˜์–ด ์žˆ์ง€๋งŒ, ์ถ”๊ฐ€๋กœ ์‹ ๊ฒฝ ์จ์•ผ ํ•  ๋ถ€๋ถ„๋“ค์ด ์žˆ์–ด. ๐Ÿ”’

๐Ÿ›ก๏ธ ๊ธฐ๋ณธ ๋ณด์•ˆ ๊ธฐ๋Šฅ

WebRTC์˜ ๋‚ด์žฅ ๋ณด์•ˆ

โœ… DTLS (Datagram Transport Layer Security)
โ†’ ๋ฏธ๋””์–ด ์ŠคํŠธ๋ฆผ ์•”ํ˜ธํ™”

โœ… SRTP (Secure Real-time Transport Protocol)
โ†’ RTP ํŒจํ‚ท ์•”ํ˜ธํ™” ๋ฐ ์ธ์ฆ

โœ… ๊ฐ•์ œ ์•”ํ˜ธํ™”
โ†’ ์•”ํ˜ธํ™” ์—†์ด๋Š” ์—ฐ๊ฒฐ ๋ถˆ๊ฐ€

โš ๏ธ ์ถ”๊ฐ€ ๋ณด์•ˆ ์กฐ์น˜

๐Ÿšจ ์ฃผ์˜ํ•ด์•ผ ํ•  ๋ณด์•ˆ ์ด์Šˆ

1. HTTPS ํ•„์ˆ˜
getUserMedia๋Š” HTTPS ํ™˜๊ฒฝ์—์„œ๋งŒ ์ž‘๋™ํ•ด. HTTP์—์„œ๋Š” ๊ถŒํ•œ ์š”์ฒญ์กฐ์ฐจ ์•ˆ ๋ผ!

2. ์‹œ๊ทธ๋„๋ง ์„œ๋ฒ„ ๋ณด์•ˆ
WebRTC ์ž์ฒด๋Š” ์•ˆ์ „ํ•˜์ง€๋งŒ, ์‹œ๊ทธ๋„๋ง ์„œ๋ฒ„๋Š” ๋ณ„๋„๋กœ ๋ณด์•ˆ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•ด. WebSocket๋„ WSS(์•”ํ˜ธํ™”)๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด.

3. TURN ์„œ๋ฒ„ ์ธ์ฆ
TURN ์„œ๋ฒ„๋Š” ํŠธ๋ž˜ํ”ฝ ๋น„์šฉ์ด ๋“ค๊ธฐ ๋•Œ๋ฌธ์— ์ธ์ฆ ์—†์ด ๊ณต๊ฐœํ•˜๋ฉด ์•…์šฉ๋  ์ˆ˜ ์žˆ์–ด.
// TURN ์„œ๋ฒ„ ์ธ์ฆ ์„ค์ •
const configuration = {
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
        {
            urls: 'turn:your-turn-server.com:3478',
            username: 'user123',
            credential: 'password123',
            credentialType: 'password'
        }
    ]
};

// ๋™์  ์ธ์ฆ ํ† ํฐ ์‚ฌ์šฉ (๊ถŒ์žฅ)
async function getTurnCredentials() {
    const response = await fetch('/api/turn-credentials', {
        headers: {
            'Authorization': `Bearer ${userToken}`
        }
    });
    
    const { username, credential } = await response.json();
    
    return {
        urls: 'turn:your-turn-server.com:3478',
        username: username,
        credential: credential,
        credentialType: 'password'
    };
}

๐Ÿ”‘ ๋ฐฉ ์ ‘๊ทผ ์ œ์–ด

// ์„œ๋ฒ„ ์ธก: ๋ฐฉ ์ ‘๊ทผ ์ธ์ฆ
io.on('connection', (socket) => {
    socket.on('join-room', async (data) => {
        const { roomId, token } = data;
        
        // ํ† ํฐ ๊ฒ€์ฆ
        const isValid = await verifyToken(token);
        if (!isValid) {
            socket.emit('error', { message: '์ธ์ฆ ์‹คํŒจ' });
            return;
        }
        
        // ๋ฐฉ ์ธ์› ์ œํ•œ ํ™•์ธ
        const room = io.sockets.adapter.rooms.get(roomId);
        if (room && room.size >= MAX_PARTICIPANTS) {
            socket.emit('error', { message: '๋ฐฉ์ด ๊ฐ€๋“ ์ฐผ์Šต๋‹ˆ๋‹ค' });
            return;
        }
        
        socket.join(roomId);
        socket.emit('joined', { roomId });
    });
});

๐Ÿ› ๋””๋ฒ„๊น… ๋ฐ ๋ฌธ์ œ ํ•ด๊ฒฐ

WebRTC ๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๋ฌธ์ œ๋ฅผ ๋งŒ๋‚˜๊ฒŒ ๋ผ. ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ์ด์Šˆ๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž! ๐Ÿ”ง

โŒ ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜๋“ค

1. "NotAllowedError: Permission denied"
์‚ฌ์šฉ์ž๊ฐ€ ์นด๋ฉ”๋ผ/๋งˆ์ดํฌ ๊ถŒํ•œ์„ ๊ฑฐ๋ถ€ํ–ˆ์„ ๋•Œ ๋ฐœ์ƒํ•ด.
async function requestMediaPermission() {
    try {
        const stream = await navigator.mediaDevices.getUserMedia({
            video: true,
            audio: true
        });
        return stream;
    } catch (error) {
        if (error.name === 'NotAllowedError') {
            alert('์นด๋ฉ”๋ผ์™€ ๋งˆ์ดํฌ ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋ธŒ๋ผ์šฐ์ € ์„ค์ •์—์„œ ๊ถŒํ•œ์„ ํ—ˆ์šฉํ•ด์ฃผ์„ธ์š”.');
        } else if (error.name === 'NotFoundError') {
            alert('์นด๋ฉ”๋ผ๋‚˜ ๋งˆ์ดํฌ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์žฅ์น˜๊ฐ€ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”.');
        } else {
            alert('๋ฏธ๋””์–ด ์ ‘๊ทผ ์˜ค๋ฅ˜: ' + error.message);
        }
        throw error;
    }
}
2. ICE ์—ฐ๊ฒฐ ์‹คํŒจ
๋ฐฉํ™”๋ฒฝ์ด๋‚˜ NAT ๋•Œ๋ฌธ์— P2P ์—ฐ๊ฒฐ์ด ์•ˆ ๋  ๋•Œ๊ฐ€ ์žˆ์–ด.
// ICE ์—ฐ๊ฒฐ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง
peerConnection.oniceconnectionstatechange = () => {
    console.log('ICE ์—ฐ๊ฒฐ ์ƒํƒœ:', peerConnection.iceConnectionState);
    
    switch (peerConnection.iceConnectionState) {
        case 'checking':
            console.log('์—ฐ๊ฒฐ ๊ฒฝ๋กœ ํƒ์ƒ‰ ์ค‘...');
            break;
        case 'connected':
            console.log('P2P ์—ฐ๊ฒฐ ์„ฑ๊ณต! โœ…');
            break;
        case 'failed':
            console.error('์—ฐ๊ฒฐ ์‹คํŒจ! TURN ์„œ๋ฒ„ ํ•„์š” โŒ');
            // TURN ์„œ๋ฒ„ ์žฌ์‹œ๋„ ๋กœ์ง
            restartIceWithTurn();
            break;
        case 'disconnected':
            console.warn('์—ฐ๊ฒฐ ๋Š๊น€. ์žฌ์—ฐ๊ฒฐ ์‹œ๋„ ์ค‘...');
            break;
        case 'closed':
            console.log('์—ฐ๊ฒฐ ์ข…๋ฃŒ๋จ');
            break;
    }
};

async function restartIceWithTurn() {
    // TURN ์„œ๋ฒ„ ์ถ”๊ฐ€ ํ›„ ICE ์žฌ์‹œ์ž‘
    const turnConfig = await getTurnCredentials();
    peerConnection.setConfiguration({
        iceServers: [
            { urls: 'stun:stun.l.google.com:19302' },
            turnConfig
        ]
    });
    
    // ICE ์žฌ์‹œ์ž‘
    const offer = await peerConnection.createOffer({ iceRestart: true });
    await peerConnection.setLocalDescription(offer);
    // Offer ์ „์†ก...
}
3. ์—์ฝ”(Echo) ๋ฌธ์ œ
์ž๊ธฐ ๋ชฉ์†Œ๋ฆฌ๊ฐ€ ๋‹ค์‹œ ๋“ค๋ฆฌ๋Š” ํ˜„์ƒ.
// ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• 1: ๋กœ์ปฌ ๋น„๋””์˜ค ์Œ์†Œ๊ฑฐ
document.getElementById('localVideo').muted = true;

// ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• 2: ์—์ฝ” ์ œ๊ฑฐ ์˜ต์…˜ ํ™œ์„ฑํ™”
const stream = await navigator.mediaDevices.getUserMedia({
    audio: {
        echoCancellation: true,
        noiseSuppression: true,
        autoGainControl: true
    },
    video: true
});

๐Ÿ” ๋””๋ฒ„๊น… ๋„๊ตฌ

ํฌ๋กฌ ๋ธŒ๋ผ์šฐ์ €์—๋Š” WebRTC ์ „์šฉ ๋””๋ฒ„๊น… ํŽ˜์ด์ง€๊ฐ€ ์žˆ์–ด! ๐Ÿ“Š
๐Ÿ› ๏ธ chrome://webrtc-internals

ํฌ๋กฌ ์ฃผ์†Œ์ฐฝ์— chrome://webrtc-internals๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ๋ชจ๋“  WebRTC ์—ฐ๊ฒฐ ์ •๋ณด๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์–ด:
โ€ข ์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„ ๊ทธ๋ž˜ํ”„
โ€ข ICE Candidate ๋ชฉ๋ก
โ€ข SDP ๋‚ด์šฉ
โ€ข ๋น„ํŠธ๋ ˆ์ดํŠธ, ํŒจํ‚ท ์†์‹ค๋ฅ  ๋“ฑ
โ€ข ์—ฐ๊ฒฐ ํƒ€์ž„๋ผ์ธ

๊ฐœ๋ฐœํ•  ๋•Œ ์ด ํŽ˜์ด์ง€๋ฅผ ์—ด์–ด๋‘๋ฉด ์—„์ฒญ ์œ ์šฉํ•ด! ๐ŸŽฏ

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ

์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์€ ์„ฑ๋Šฅ์ด ์ƒ๋ช…์ด์•ผ! ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ค๋Š” ์ตœ์ ํ™” ๊ธฐ๋ฒ•๋“ค์„ ์•Œ์•„๋ณด์ž. ๐Ÿš€

๐Ÿ“น ๋น„๋””์˜ค ํ’ˆ์งˆ ์กฐ์ ˆ

// ๋„คํŠธ์›Œํฌ ์ƒํ™ฉ์— ๋”ฐ๋ผ ๋™์ ์œผ๋กœ ๋น„ํŠธ๋ ˆ์ดํŠธ ์กฐ์ ˆ
async function adjustVideoBitrate(bitrate) {
    const sender = peerConnection.getSenders().find(s => 
        s.track && s.track.kind === 'video'
    );
    
    if (!sender) return;
    
    const parameters = sender.getParameters();
    
    if (!parameters.encodings) {
        parameters.encodings = [{}];
    }
    
    parameters.encodings[0].maxBitrate = bitrate * 1000; // kbps to bps
    
    await sender.setParameters(parameters);
    console.log(`๋น„ํŠธ๋ ˆ์ดํŠธ ์กฐ์ ˆ: ${bitrate} kbps`);
}

// ๋„คํŠธ์›Œํฌ ์ƒํƒœ ๊ฐ์ง€
let lastBytesReceived = 0;
let lastTimestamp = Date.now();

setInterval(async () => {
    const stats = await peerConnection.getStats();
    
    stats.forEach(report => {
        if (report.type === 'inbound-rtp' && report.kind === 'video') {
            const now = Date.now();
            const bytes = report.bytesReceived;
            const bitrate = (bytes - lastBytesReceived) * 8 / (now - lastTimestamp);
            
            // ๋„คํŠธ์›Œํฌ๊ฐ€ ๋А๋ฆฌ๋ฉด ํ’ˆ์งˆ ๋‚ฎ์ถค
            if (bitrate < 500) {  // 500 kbps ์ดํ•˜
                adjustVideoBitrate(300);
            } else if (bitrate > 2000) {  // 2 Mbps ์ด์ƒ
                adjustVideoBitrate(1500);
            }
            
            lastBytesReceived = bytes;
            lastTimestamp = now;
        }
    });
}, 3000);

๐ŸŽจ Simulcast (๋‹ค์ค‘ ํ•ด์ƒ๋„ ์ „์†ก)

Simulcast๋Š” ๋™์‹œ์— ์—ฌ๋Ÿฌ ํ•ด์ƒ๋„์˜ ์˜์ƒ์„ ์ „์†กํ•˜๋Š” ๊ธฐ์ˆ ์ด์•ผ. SFU ์„œ๋ฒ„๊ฐ€ ๊ฐ ์ˆ˜์‹ ์ž์˜ ๋„คํŠธ์›Œํฌ ์ƒํ™ฉ์— ๋งž๋Š” ํ•ด์ƒ๋„๋ฅผ ์„ ํƒํ•ด์„œ ๋ณด๋‚ด์ค„ ์ˆ˜ ์žˆ์ง€! ๐ŸŽฏ
// Simulcast ํ™œ์„ฑํ™”
async function enableSimulcast() {
    const sender = peerConnection.getSenders().find(s => 
        s.track && s.track.kind === 'video'
    );
    
    const parameters = sender.getParameters();
    
    if (!parameters.encodings) {
        parameters.encodings = [
            { rid: 'h', maxBitrate: 1500000 },  // ๊ณ ํ™”์งˆ
            { rid: 'm', maxBitrate: 600000, scaleResolutionDownBy: 2 },  // ์ค‘ํ™”์งˆ
            { rid: 'l', maxBitrate: 200000, scaleResolutionDownBy: 4 }   // ์ €ํ™”์งˆ
        ];
    }
    
    await sender.setParameters(parameters);
    console.log('Simulcast ํ™œ์„ฑํ™”! ๐Ÿ“ก');
}

๐Ÿ”„ ์ž๋™ ์žฌ์—ฐ๊ฒฐ

class RobustPeerConnection {
    constructor(config) {
        this.config = config;
        this.pc = null;
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = 5;
        this.init();
    }
    
    init() {
        this.pc = new RTCPeerConnection(this.config);
        this.setupEventHandlers();
    }
    
    setupEventHandlers() {
        this.pc.oniceconnectionstatechange = () => {
            if (this.pc.iceConnectionState === 'failed') {
                this.handleConnectionFailure();
            } else if (this.pc.iceConnectionState === 'connected') {
                this.reconnectAttempts = 0;
                console.log('์—ฐ๊ฒฐ ๋ณต๊ตฌ ์„ฑ๊ณต! โœ…');
            }
        };
    }
    
    async handleConnectionFailure() {
        if (this.reconnectAttempts >= this.maxReconnectAttempts) {
            console.error('์ตœ๋Œ€ ์žฌ์—ฐ๊ฒฐ ์‹œ๋„ ํšŸ์ˆ˜ ์ดˆ๊ณผ โŒ');
            this.onMaxReconnectFailed();
            return;
        }
        
        this.reconnectAttempts++;
        console.log(`์žฌ์—ฐ๊ฒฐ ์‹œ๋„ ${this.reconnectAttempts}/${this.maxReconnectAttempts}...`);
        
        // ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ (exponential backoff)
        const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 10000);
        await new Promise(resolve => setTimeout(resolve, delay));
        
        // ICE ์žฌ์‹œ์ž‘
        const offer = await this.pc.createOffer({ iceRestart: true });
        await this.pc.setLocalDescription(offer);
        // Offer ์ „์†ก ๋กœ์ง...
    }
    
    onMaxReconnectFailed() {
        // ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ
        alert('์—ฐ๊ฒฐ์ด ๋ถˆ์•ˆ์ •ํ•ฉ๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•ด์ฃผ์„ธ์š”.');
    }
}

๐ŸŒŸ ์‹ค์ „ ํ”„๋กœ์ ํŠธ ์•„์ด๋””์–ด

WebRTC๋ฅผ ๋ฐฐ์› ์œผ๋‹ˆ ์ด์ œ ๋ญ˜ ๋งŒ๋“ค ์ˆ˜ ์žˆ์„๊นŒ? ์žฌ๋ฏธ์žˆ๋Š” ํ”„๋กœ์ ํŠธ ์•„์ด๋””์–ด๋“ค์„ ์†Œ๊ฐœํ• ๊ฒŒ! ๐Ÿ’ก

๋งŒ์•ฝ ์ด๋Ÿฐ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ๋„์›€์ด ํ•„์š”ํ•˜๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ WebRTC ์ „๋ฌธ๊ฐ€๋ฅผ ์ฐพ์•„๋ณผ ์ˆ˜๋„ ์žˆ์–ด. ์‹ค๋ฌด ๊ฒฝํ—˜์ด ํ’๋ถ€ํ•œ ๊ฐœ๋ฐœ์ž๋“ค์ด ๋งŽ์ด ํ™œ๋™ํ•˜๊ณ  ์žˆ๊ฑฐ๋“ ! ๐Ÿ˜Š

1๏ธโƒฃ ์˜จ๋ผ์ธ ์Šคํ„ฐ๋”” ๋ฃธ

โ€ข ํ™”์ƒ ํ†ตํ™” + ํ™”๋ฉด ๊ณต์œ 
โ€ข ํƒ€์ด๋จธ ๋ฐ ์ง‘์ค‘ ์‹œ๊ฐ„ ์ธก์ •
โ€ข ๊ณต๋ถ€ ์‹œ๊ฐ„ ํ†ต๊ณ„
โ€ข ๋ฐฐ๊ฒฝ ์Œ์•… ๊ณต์œ  ๊ธฐ๋Šฅ

2๏ธโƒฃ ์›๊ฒฉ ์˜๋ฃŒ ์ƒ๋‹ด ํ”Œ๋žซํผ

โ€ข ์˜์‚ฌ-ํ™˜์ž 1๋Œ€1 ํ™”์ƒ ์ƒ๋‹ด
โ€ข ์˜๋ฃŒ ๊ธฐ๋ก ํ™”๋ฉด ๊ณต์œ 
โ€ข ์ฑ„ํŒ…์œผ๋กœ ์ฒ˜๋ฐฉ์ „ ์ „๋‹ฌ
โ€ข ์˜ˆ์•ฝ ์‹œ์Šคํ…œ ์—ฐ๋™

3๏ธโƒฃ ์‹ค์‹œ๊ฐ„ ํ˜‘์—… ํ™”์ดํŠธ๋ณด๋“œ

โ€ข ํ™”์ƒ ํ†ตํ™” + Canvas API
โ€ข ์‹ค์‹œ๊ฐ„ ๊ทธ๋ฆผ ๊ณต์œ  (DataChannel)
โ€ข ๋‹ค์ค‘ ์‚ฌ์šฉ์ž ๋™์‹œ ํŽธ์ง‘
โ€ข ์Šคํฌ๋ฆฐ์ƒท ์ €์žฅ ๊ธฐ๋Šฅ

4๏ธโƒฃ ๋žœ๋ค ์ฑ„ํŒ… ๋ฃฐ๋ ›

โ€ข ๋ฌด์ž‘์œ„ ์‚ฌ์šฉ์ž ๋งค์นญ
โ€ข ๊ด€์‹ฌ์‚ฌ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง
โ€ข ์‹ ๊ณ  ๋ฐ ์ฐจ๋‹จ ์‹œ์Šคํ…œ
โ€ข ์ต๋ช…์„ฑ ๋ณด์žฅ

5๏ธโƒฃ ์˜จ๋ผ์ธ ๋ฉด์ ‘ ์‹œ์Šคํ…œ

โ€ข ๋ฉด์ ‘๊ด€-์ง€์›์ž ํ™”์ƒ ๋ฉด์ ‘
โ€ข ํ™”๋ฉด ๋…นํ™” ๊ธฐ๋Šฅ
โ€ข ์ฝ”๋”ฉ ํ…Œ์ŠคํŠธ ํ™”๋ฉด ๊ณต์œ 
โ€ข ํ‰๊ฐ€ ๋ฉ”๋ชจ ๊ธฐ๋Šฅ
WebRTC ํ™œ์šฉ ๋ถ„์•ผ ์›๊ฒฉ ๊ต์œก ์˜จ๋ผ์ธ ์ˆ˜์—… ์›๊ฒฉ ์˜๋ฃŒ ํ™”์ƒ ์ง„๋ฃŒ ๊ฒŒ์ž„ ์‹ค์‹œ๊ฐ„ ๋ฉ€ํ‹ฐํ”Œ๋ ˆ์ด IoT ์Šค๋งˆํŠธํ™ˆ ๋ชจ๋‹ˆํ„ฐ๋ง ์†Œ์…œ ํ™”์ƒ ์ฑ„ํŒ… ๋น„์ฆˆ๋‹ˆ์Šค ํ™”์ƒ ํšŒ์˜ ์—”ํ„ฐํ…Œ์ธ๋จผํŠธ ๋ผ์ด๋ธŒ ์ŠคํŠธ๋ฆฌ๋ฐ ๋ฌดํ•œํ•œ ๊ฐ€๋Šฅ์„ฑ์˜ WebRTC! ๐Ÿš€

๐Ÿ“š ์œ ์šฉํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฐ ํ”„๋ ˆ์ž„์›Œํฌ

์ฒ˜์Œ๋ถ€ํ„ฐ ๋ชจ๋“  ๊ฑธ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๋Š” ๊ฑด ํž˜๋“ค์–ด. ๊ฒ€์ฆ๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•˜๋ฉด ํ›จ์”ฌ ๋น ๋ฅด๊ฒŒ ๊ฐœ๋ฐœํ•  ์ˆ˜ ์žˆ์ง€! ๐Ÿ› ๏ธ

๐ŸŽฏ ์ถ”์ฒœ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

1. Simple-Peer
โ†’ WebRTC๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋ž˜ํ•‘ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ. ์ดˆ๋ณด์ž์—๊ฒŒ ์ตœ๊ณ !
โ†’ npm: npm install simple-peer

2. PeerJS
โ†’ P2P ์—ฐ๊ฒฐ์„ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ. ๋ฌด๋ฃŒ ์‹œ๊ทธ๋„๋ง ์„œ๋ฒ„ ์ œ๊ณต!
โ†’ npm: npm install peerjs

3. Mediasoup
โ†’ ํ”„๋กœ๋•์…˜๊ธ‰ SFU ์„œ๋ฒ„. ๋‹ค์ž๊ฐ„ ํšŒ์˜์— ์ตœ์ ํ™”.
โ†’ npm: npm install mediasoup

4. Daily.co
โ†’ WebRTC๋ฅผ ์„œ๋น„์Šค๋กœ ์ œ๊ณต. API๋งŒ ํ˜ธ์ถœํ•˜๋ฉด ๋!
โ†’ ๋ฌด๋ฃŒ ํ”Œ๋žœ๋„ ์žˆ์–ด์„œ ํ”„๋กœํ† ํƒ€์ž… ๋งŒ๋“ค๊ธฐ ์ข‹์•„.

5. Agora SDK
โ†’ ์ƒ์šฉ WebRTC ํ”Œ๋žซํผ. ์•ˆ์ •์„ฑ๊ณผ ํ’ˆ์งˆ์ด ๋›ฐ์–ด๋‚˜.
โ†’ ๋Œ€๊ทœ๋ชจ ์„œ๋น„์Šค์— ์ ํ•ฉ.

๐Ÿ’ป Simple-Peer ์˜ˆ์ œ

// Simple-Peer๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„ํ•˜๊ธฐ
const SimplePeer = require('simple-peer');

// Initiator (ํ˜ธ์ถœ์ž)
const peer1 = new SimplePeer({ initiator: true, stream: localStream });

peer1.on('signal', (data) => {
    // ์‹œ๊ทธ๋„ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ๋Œ€๋ฐฉ์—๊ฒŒ ์ „์†ก
    socket.emit('signal', data);
});

peer1.on('stream', (remoteStream) => {
    // ์›๊ฒฉ ์ŠคํŠธ๋ฆผ ์ˆ˜์‹ 
    remoteVideo.srcObject = remoteStream;
});

// Receiver (์ˆ˜์‹ ์ž)
const peer2 = new SimplePeer({ stream: localStream });

peer2.on('signal', (data) => {
    socket.emit('signal', data);
});

peer2.on('stream', (remoteStream) => {
    remoteVideo.srcObject = remoteStream;
});

// ์ƒ๋Œ€๋ฐฉ์˜ ์‹œ๊ทธ๋„ ์ˆ˜์‹ 
socket.on('signal', (data) => {
    peer1.signal(data);  // ๋˜๋Š” peer2.signal(data)
});

// ์ด๊ฒŒ ๋! ์—„์ฒญ ๊ฐ„๋‹จํ•˜์ง€? ๐Ÿ˜Ž

๐ŸŽ“ ํ•™์Šต ๋ฆฌ์†Œ์Šค ๋ฐ ์ปค๋ฎค๋‹ˆํ‹ฐ

WebRTC๋ฅผ ๋” ๊นŠ์ด ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด๋Ÿฐ ์ž๋ฃŒ๋“ค์„ ์ฐธ๊ณ ํ•ด๋ด! ๐Ÿ“–
๐Ÿ“– ๊ณต์‹ ๋ฌธ์„œ ๋ฐ ์ŠคํŽ™
โ€ข MDN Web Docs - WebRTC API
โ€ข W3C WebRTC 1.0 Specification
โ€ข webrtc.org (๊ณต์‹ ์‚ฌ์ดํŠธ)

๐ŸŽฅ ์˜จ๋ผ์ธ ๊ฐ•์˜
โ€ข Udemy - WebRTC ์™„๋ฒฝ ๊ฐ€์ด๋“œ
โ€ข YouTube - WebRTC ํŠœํ† ๋ฆฌ์–ผ ์‹œ๋ฆฌ์ฆˆ
โ€ข Coursera - Real-Time Communication

๐Ÿ’ฌ ์ปค๋ฎค๋‹ˆํ‹ฐ
โ€ข WebRTC GitHub Discussions
โ€ข Stack Overflow - webrtc ํƒœ๊ทธ
โ€ข Reddit - r/webrtc
โ€ข Discuss WebRTC (Google Groups)

๐Ÿ”ง ์‹ค์Šต ์‚ฌ์ดํŠธ
โ€ข webrtc.github.io/samples (๊ณต์‹ ์ƒ˜ํ”Œ)
โ€ข appr.tc (Google์˜ ๋ฐ๋ชจ ์•ฑ)
โ€ข appear.in (์‹ค์ œ ์„œ๋น„์Šค ์˜ˆ์ œ)
๊ทธ๋ฆฌ๊ณ  ์‹ค๋ฌด ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท์—์„œ WebRTC ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ๊ฒฝํ—˜ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋‚˜ ์ปจ์„คํŒ…์„ ์ œ๊ณตํ•˜๊ณ  ์žˆ๊ฑฐ๋“ ! ๐Ÿค

๐ŸŽฌ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€! ์—ฌ๊ธฐ๊นŒ์ง€ ์ฝ์—ˆ๋‹ค๋ฉด ์ •๋ง ๋Œ€๋‹จํ•ด! ๐Ÿ‘ WebRTC๋Š” ์ฒ˜์Œ์—” ๋ณต์žกํ•ด ๋ณด์ด์ง€๋งŒ, ํ•ต์‹ฌ ๊ฐœ๋…๋งŒ ์ดํ•ดํ•˜๋ฉด ์ƒ๊ฐ๋ณด๋‹ค ์žฌ๋ฏธ์žˆ๋Š” ๊ธฐ์ˆ ์ด์•ผ.

์šฐ๋ฆฌ๊ฐ€ ๋ฐฐ์šด ๋‚ด์šฉ์„ ์ •๋ฆฌํ•ด๋ณผ๊นŒ?

โœ… WebRTC์˜ ๊ธฐ๋ณธ ๊ฐœ๋…: P2P ์‹ค์‹œ๊ฐ„ ํ†ต์‹ 
โœ… ํ•ต์‹ฌ API 3๊ฐ€์ง€: MediaStream, RTCPeerConnection, RTCDataChannel
โœ… ์‹œ๊ทธ๋„๋ง ํ”„๋กœ์„ธ์Šค: SDP์™€ ICE Candidate ๊ตํ™˜
โœ… ์‹ค์ „ ๊ตฌํ˜„: 1๋Œ€1 ํ™”์ƒ ํ†ตํ™” ๋งŒ๋“ค๊ธฐ
โœ… ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ: ํ™”๋ฉด ๊ณต์œ , ์ฑ„ํŒ…, ๋‹ค์ž๊ฐ„ ํšŒ์˜
โœ… ๋ณด์•ˆ๊ณผ ์ตœ์ ํ™”: HTTPS, TURN ์ธ์ฆ, ๋น„ํŠธ๋ ˆ์ดํŠธ ์กฐ์ ˆ
โœ… ๋””๋ฒ„๊น… ๋ฐฉ๋ฒ•: chrome://webrtc-internals ํ™œ์šฉ

์ด์ œ ๋„ˆ๋„ WebRTC ๊ฐœ๋ฐœ์ž์•ผ! ๐ŸŽ‰ ์ž‘์€ ํ”„๋กœ์ ํŠธ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์ ์  ๋ณต์žกํ•œ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด๋ณด์ž. ์‹ค์ œ๋กœ ๋งŒ๋“ค์–ด๋ณด๋ฉด์„œ ๋ฐฐ์šฐ๋Š” ๊ฒŒ ๊ฐ€์žฅ ๋น ๋ฅธ ๋ฐฉ๋ฒ•์ด์•ผ.

ํ˜น์‹œ ๊ฐœ๋ฐœํ•˜๋‹ค๊ฐ€ ์–ด๋ ค์šด ๋ถ€๋ถ„์ด ์žˆ์œผ๋ฉด ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•˜๊ฑฐ๋‚˜, ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์•„. ํ˜ผ์ž ๊ณ ๋ฏผํ•˜์ง€ ๋ง๊ณ  ์ ๊ทน์ ์œผ๋กœ ๋„์›€์„ ์š”์ฒญํ•˜์ž! ๐Ÿ’ช

WebRTC๋Š” ๊ณ„์† ๋ฐœ์ „ํ•˜๊ณ  ์žˆ๋Š” ๊ธฐ์ˆ ์ด์•ผ. ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ๋“ค์ด ์ถ”๊ฐ€๋˜๊ณ , ๋ธŒ๋ผ์šฐ์ € ์ง€์›๋„ ์ ์  ์ข‹์•„์ง€๊ณ  ์žˆ์ง€. ์•ž์œผ๋กœ ๋” ๋งŽ์€ ์„œ๋น„์Šค๋“ค์ด WebRTC๋ฅผ ํ™œ์šฉํ•  ๊ฑฐ์•ผ. ์ง€๊ธˆ ๋ฐฐ์›Œ๋‘๋ฉด ์ •๋ง ์œ ์šฉํ•  ๊ฑฐ์•ผ! ๐Ÿš€

์ž, ์ด์ œ ์ฝ”๋“œ ์—๋””ํ„ฐ๋ฅผ ์—ด๊ณ  ์ฒซ ๋ฒˆ์งธ WebRTC ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•ด๋ณด์ž! ํ™”์ดํŒ…! ๐Ÿ’ปโœจ
Happy Coding! ๐ŸŽ‰ WebRTC๋กœ ๋ฉ‹์ง„ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”! ๐Ÿ“น ๐Ÿš€ ๐Ÿ’ก
๐ŸŒŸ ์ด ๊ธ€์ด ๋„์›€์ด ๋˜์—ˆ๋‚˜์š”?

WebRTC ๊ฐœ๋ฐœ์„ ์‹œ์ž‘ํ•˜๋Š” ๋ชจ๋“  ๋ถ„๋“ค์„ ์‘์›ํ•ฉ๋‹ˆ๋‹ค! ๐Ÿ’ช
๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๋‹ค๋ฉด ์–ธ์ œ๋“  ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•ด๋ณด์„ธ์š”.
ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜์–ด์š”! ๐ŸŒฑ
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.