Ver3.0 ๐ฅ ์น ๋น๋์ค์ WebRTC๋ก ๋ง๋๋ ์ค์๊ฐ ํ์ ํต์ ์ ๋ชจ๋ ๊ฒ

๐ฅ ์น ๋น๋์ค์ WebRTC๋ก ๋ง๋๋ ์ค์๊ฐ ํ์ ํต์ ์ ๋ชจ๋ ๊ฒ
์น๊ตฌ์ฒ๋ผ ์ฝ๊ฒ ๋ฐฐ์ฐ๋ ์ค์ ํ์ ํต์ ๊ตฌํ ๊ฐ์ด๋
์๋ ! ๐ ์์ฆ ํ์ ํ์, ์จ๋ผ์ธ ์์ , ์๊ฒฉ ์ง๋ฃ ๋ฑ ์ค์๊ฐ ์์ ํต์ ์ด ์ ๋ง ๋ง์ด ์ฐ์ด๊ณ ์์ง? ์ค(Zoom), ๊ตฌ๊ธ ๋ฏธํธ(Google Meet), ๋์ค์ฝ๋(Discord) ๊ฐ์ ์๋น์ค๋ค์ด ์ด๋ป๊ฒ ์ด๋ ๊ฒ ๋ถ๋๋ฝ๊ฒ ์ค์๊ฐ์ผ๋ก ์์๊ณผ ์์ฑ์ ์ฃผ๊ณ ๋ฐ์ ์ ์๋์ง ๊ถ๊ธํ๋ ์ ์์ด?
์ค๋์ ๋ฐ๋ก ๊ทธ ๋น๋ฐ์ ํํค์ณ๋ณผ ๊ฑฐ์ผ! ์น ๋น๋์ค์ WebRTC(Web Real-Time Communication)๋ฅผ ํ์ฉํด์ ์ค์๊ฐ ํ์ ํต์ ์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ์น๊ตฌ์ฒ๋ผ ํธํ๊ฒ ์ค๋ช ํด์ค๊ฒ. ํ๋ก๊ทธ๋๋ฐ ์ด๋ณด์๋ ์ดํดํ ์ ์๋๋ก ์ฐจ๊ทผ์ฐจ๊ทผ ์๋ ค์ค ํ ๋๊น ๊ฑฑ์ ๋ง! ๐
๐ฌ ์น ๋น๋์ค์ ๊ธฐ์ด: HTML5 Video ํ๊ทธ๋ถํฐ ์์ํ๊ธฐ
์ค์๊ฐ ํ์ ํต์ ์ ์ดํดํ๋ ค๋ฉด ๋จผ์ ์น์์ ๋น๋์ค๊ฐ ์ด๋ป๊ฒ ์๋ํ๋์ง ์์์ผ ํด. HTML5๊ฐ ๋ฑ์ฅํ๋ฉด์ ์น ๋ธ๋ผ์ฐ์ ์์ ํ๋ฌ๊ทธ์ธ ์์ด๋ ๋น๋์ค๋ฅผ ์ฌ์ํ ์ ์๊ฒ ๋๊ฑฐ๋ !
๊ฐ์ฅ ๊ฐ๋จํ ๋น๋์ค ์ฌ์ ๋ฐฉ๋ฒ์
<video> ํ๊ทธ๋ฅผ ์ฌ์ฉํ๋ ๊ฑฐ์ผ. ์ด๋ ๊ฒ ์๊ฒผ์ด:
<video width="640" height="480" controls>
<source src="movie.mp4" type="video/mp4">
<source src="movie.webm" type="video/webm">
Your browser does not support the video tag.
</video>
์ด ์ฝ๋๋ ์ ์ฅ๋ ๋น๋์ค ํ์ผ์ ์ฌ์ํ๋ ๊ฑฐ์ผ. ํ์ง๋ง ์ฐ๋ฆฌ๊ฐ ๋ง๋ค๊ณ ์ถ์ ๊ฑด ์ค์๊ฐ ํ์ ํต์ ์ด์์? ๊ทธ๋ฌ๋ ค๋ฉด ์ฌ์ฉ์์ ์น์บ ๊ณผ ๋ง์ดํฌ์ ์ ๊ทผํด์ผ ํด. ์ฌ๊ธฐ์ ๋ฑ์ฅํ๋ ๊ฒ ๋ฐ๋ก MediaStream API์ผ! ๐ฅ
navigator.mediaDevices.getUserMedia() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ฉด ์ฌ์ฉ์์ ์นด๋ฉ๋ผ์ ๋ง์ดํฌ์ ์ ๊ทผํ ์ ์์ด. ๋ฌผ๋ก ์ฌ์ฉ์์ ํ๋ฝ์ ๋ฐ์์ผ ํ์ง!
// ์นด๋ฉ๋ผ์ ๋ง์ดํฌ ์ ๊ทผ ์์ฒญ
navigator.mediaDevices.getUserMedia({
video: true,
audio: true
})
.then(function(stream) {
// ์ฑ๊ณต! stream์ video ์์์ ์ฐ๊ฒฐ
const videoElement = document.querySelector('video');
videoElement.srcObject = stream;
})
.catch(function(error) {
console.error('๋ฏธ๋์ด ์ ๊ทผ ์คํจ:', error);
});
์ด ์ฝ๋๋ฅผ ์คํํ๋ฉด ๋ธ๋ผ์ฐ์ ๊ฐ "์ด ์ฌ์ดํธ๊ฐ ์นด๋ฉ๋ผ์ ๋ง์ดํฌ๋ฅผ ์ฌ์ฉํ๋ ค๊ณ ํฉ๋๋ค"๋ผ๋ ํ์
์ ๋์. ์ฌ์ฉ์๊ฐ ํ๋ฝํ๋ฉด stream ๊ฐ์ฒด๋ฅผ ๋ฐ๊ฒ ๋๊ณ , ์ด๊ฑธ <video> ์์์ srcObject์ ์ฐ๊ฒฐํ๋ฉด ์๊ธฐ ์ผ๊ตด์ด ํ๋ฉด์ ๋ํ๋! ๐
video: { facingMode: "user" }๋ฅผ ์ค์ ํ๋ฉด ์ ๋ฉด ์นด๋ฉ๋ผ๋ฅผ, facingMode: "environment"๋ฅผ ์ค์ ํ๋ฉด ํ๋ฉด ์นด๋ฉ๋ผ๋ฅผ ์ฌ์ฉํ ์ ์์ด!
๐ WebRTC๋ ๋ฌด์์ธ๊ฐ? ์ค์๊ฐ ํต์ ์ ํต์ฌ
์, ์ด์ ๋ด ์นด๋ฉ๋ผ ์์์ ๋ด ํ๋ฉด์ ๋์ฐ๋ ๊ฑด ์ฑ๊ณตํ์ด. ๊ทธ๋ฐ๋ฐ ์ด๊ฑธ ์ด๋ป๊ฒ ๋ค๋ฅธ ์ฌ๋์๊ฒ ๋ณด๋ด์ง? ์ฌ๊ธฐ์ WebRTC(Web Real-Time Communication)๊ฐ ๋ฑ์ฅํด! ๐
โข P2P(Peer-to-Peer) ํต์ : ์๋ฒ๋ฅผ ๊ฑฐ์น์ง ์๊ณ ๋ธ๋ผ์ฐ์ ๋ผ๋ฆฌ ์ง์ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์
โข ์ ์ง์ฐ(Low Latency): ์ค์๊ฐ์ผ๋ก ํต์ ํ๊ธฐ ๋๋ฌธ์ ์ง์ฐ์ด ๊ฑฐ์ ์์ด
โข ์คํ์์ค: ๊ตฌ๊ธ์ด ์ฃผ๋ํ๊ณ W3C์ IETF๊ฐ ํ์คํํ ๋ฌด๋ฃ ๊ธฐ์ ์ด์ผ
โข ๋ณด์: ๊ธฐ๋ณธ์ ์ผ๋ก ์ํธํ(DTLS, SRTP)๊ฐ ์ ์ฉ๋ผ ์์ด
โข ๋ฉํฐ๋ฏธ๋์ด ์ง์: ์ค๋์ค, ๋น๋์ค, ๋ฐ์ดํฐ ์ฑ๋ ๋ชจ๋ ์ง์ํด
WebRTC๋ ์ธ ๊ฐ์ง ์ฃผ์ API๋ก ๊ตฌ์ฑ๋ผ ์์ด:
| API | ์ญํ | ์ค๋ช |
|---|---|---|
| MediaStream | ๋ฏธ๋์ด ์คํธ๋ฆผ ๊ด๋ฆฌ | ์นด๋ฉ๋ผ, ๋ง์ดํฌ ๋ฑ์ ๋ฏธ๋์ด ์์ค์ ์ ๊ทผํ๊ณ ์คํธ๋ฆผ์ ๊ด๋ฆฌํด |
| RTCPeerConnection | P2P ์ฐ๊ฒฐ ๊ด๋ฆฌ | ๋ ๋ธ๋ผ์ฐ์ ๊ฐ์ ์ค๋์ค/๋น๋์ค ์ฐ๊ฒฐ์ ์ค์ ํ๊ณ ๊ด๋ฆฌํด |
| RTCDataChannel | ๋ฐ์ดํฐ ์ ์ก | ํ ์คํธ, ํ์ผ ๋ฑ ์์์ ๋ฐ์ดํฐ๋ฅผ P2P๋ก ์ ์กํ ์ ์์ด |
์ด ์ค์์ ํ์ ํต์ ์ ๊ฐ์ฅ ์ค์ํ ๊ฑด RTCPeerConnection์ด์ผ. ์ด๊ฒ ์ค์ ๋ก ๋ ์ฌ๋์ ์ฐ๊ฒฐํด์ฃผ๋ ํต์ฌ ์ญํ ์ ํ๊ฑฐ๋ ! ๐
๐ค ์๊ทธ๋๋ง(Signaling): ๋ ๋ธ๋ผ์ฐ์ ๋ฅผ ์ฐ๊ฒฐํ๋ ์ค๋งค์์ด
WebRTC๋ P2P ํต์ ์ด๋ผ๊ณ ํ์ง? ๊ทธ๋ฐ๋ฐ ์ฒ์๋ถํฐ ๋ ๋ธ๋ผ์ฐ์ ๊ฐ ์๋ก๋ฅผ ์ด๋ป๊ฒ ์ฐพ์? ๐ค ์ฌ๊ธฐ์ ํ์ํ ๊ฒ ๋ฐ๋ก ์๊ทธ๋๋ง(Signaling)์ด์ผ!
์๊ทธ๋๋ง์ ๋ ํผ์ด(peer)๊ฐ ์๋ก๋ฅผ ์ฐพ๊ณ ์ฐ๊ฒฐ ์ ๋ณด๋ฅผ ๊ตํํ๋ ๊ณผ์ ์ด์ผ. ๋ง์น ์๊ฐํ ์ฑ์์ ์๋ก์ ์ฐ๋ฝ์ฒ๋ฅผ ๊ตํํ๋ ๊ฒ๊ณผ ๋น์ทํด! ํ๋ฒ ์ฐ๋ฝ์ฒ๋ฅผ ๊ตํํ๋ฉด ๊ทธ ๋ค์๋ถํฐ๋ ์ง์ ํตํํ๋ ๊ฑฐ์ง.
์๊ทธ๋๋ง ๊ณผ์ ์์ ๊ตํ๋๋ ์ ๋ณด๋ ํฌ๊ฒ ๋ ๊ฐ์ง์ผ:
SDP๋ ๋ฏธ๋์ด ์ธ์ ์ ๋ํ ์ ๋ณด๋ฅผ ๋ด๊ณ ์์ด. ์ด๋ค ์ฝ๋ฑ์ ์ง์ํ๋์ง, ์ด๋ค ํด์๋๋ฅผ ์ฌ์ฉํ ์ง ๋ฑ์ ์ ๋ณด๊ฐ ๋ค์ด์์ง. ํ์ชฝ์ด Offer๋ฅผ ๋ง๋ค๋ฉด, ๋ค๋ฅธ ์ชฝ์ด Answer๋ก ์๋ตํ๋ ๋ฐฉ์์ด์ผ.
// Peer A: Offer ์์ฑ
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
// ์ด offer๋ฅผ ์๊ทธ๋๋ง ์๋ฒ๋ฅผ ํตํด Peer B์๊ฒ ์ ์ก
// Peer B: Offer ๋ฐ๊ณ Answer ์์ฑ
await peerConnection.setRemoteDescription(receivedOffer);
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
// ์ด answer๋ฅผ Peer A์๊ฒ ์ ์ก
ICE ํ๋ณด๋ ๋คํธ์ํฌ ๊ฒฝ๋ก ์ ๋ณด์ผ. ๊ฐ ํผ์ด๊ฐ ์์ ์๊ฒ ๋๋ฌํ ์ ์๋ ๋ชจ๋ ๊ฐ๋ฅํ ๋คํธ์ํฌ ๊ฒฝ๋ก(IP ์ฃผ์์ ํฌํธ)๋ฅผ ์์งํด์ ์๋๋ฐฉ์๊ฒ ๋ณด๋ด. ๊ทธ๋ฌ๋ฉด ์๋ก ์ฐ๊ฒฐ ๊ฐ๋ฅํ ์ต์ ์ ๊ฒฝ๋ก๋ฅผ ์ฐพ์๋ด๋ ๊ฑฐ์ง! ๐บ๏ธ
// ICE candidate ์ด๋ฒคํธ ๋ฆฌ์ค๋
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
// ์ด candidate๋ฅผ ์๊ทธ๋๋ง ์๋ฒ๋ฅผ ํตํด ์๋๋ฐฉ์๊ฒ ์ ์ก
sendToSignalingServer({
type: 'ice-candidate',
candidate: event.candidate
});
}
};
์๊ทธ๋๋ง ์๋ฒ๋ ๋จ์ํ ๋ฉ์์ง๋ฅผ ์ค๊ณํ๋ ์ญํ ๋ง ํด. ์ค์ ๋ฏธ๋์ด ๋ฐ์ดํฐ๋ ์๊ทธ๋๋ง ์๋ฒ๋ฅผ ๊ฑฐ์น์ง ์๊ณ P2P๋ก ์ง์ ์ ์ก๋ผ! ์ด๊ฒ WebRTC์ ํฐ ์ฅ์ ์ด์ผ. ์๋ฒ ๋ถํ๊ฐ ํจ์ฌ ์ ๊ฑฐ๋ . ๐ช
๐๏ธ ์ค์ ๊ตฌํ: ๊ฐ๋จํ 1:1 ํ์ ํตํ ๋ง๋ค๊ธฐ
์ด๋ก ์ ์ถฉ๋ถํ ๋ฐฐ์ ์ผ๋ ์ด์ ์ค์ ๋ก ๋ง๋ค์ด๋ณผ๊น? ๊ฐ๋จํ 1:1 ํ์ ํตํ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋จ๊ณ๋ณ๋ก ๊ตฌํํด๋ณด์! ๐ ๏ธ
โข ํ๋ก ํธ์๋: HTML, CSS, JavaScript (๋ฐ๋๋ผ JS ๋๋ React/Vue ๋ฑ)
โข ์๊ทธ๋๋ง ์๋ฒ: Node.js + Socket.io (๋๋ WebSocket)
โข STUN/TURN ์๋ฒ: NAT ํต๊ณผ๋ฅผ ์ํ ์๋ฒ (Google์ ๋ฌด๋ฃ STUN ์๋ฒ ์ฌ์ฉ ๊ฐ๋ฅ)
Step 1: HTML ๊ตฌ์กฐ ๋ง๋ค๊ธฐ
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>WebRTC ํ์ ํตํ</title>
</head>
<body>
<div class="video-container">
<video id="localVideo" autoplay muted></video>
<video id="remoteVideo" autoplay></video>
</div>
<div class="controls">
<button id="startButton">ํตํ ์์</button>
<button id="hangupButton">ํตํ ์ข
๋ฃ</button>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="main.js"></script>
</body>
</html>
localVideo๋ ๋ด ์์์, remoteVideo๋ ์๋๋ฐฉ ์์์ ๋ณด์ฌ์ค ๊ฑฐ์ผ. muted ์์ฑ์ ๋ด ๋ชฉ์๋ฆฌ๊ฐ ์คํผ์ปค๋ก ๋์์ ์์ฝ๊ฐ ์๊ธฐ๋ ๊ฑธ ๋ฐฉ์งํด์ค!
Step 2: JavaScript๋ก WebRTC ๋ก์ง ๊ตฌํํ๊ธฐ
// main.js
const socket = io();
let localStream;
let remoteStream;
let peerConnection;
// STUN ์๋ฒ ์ค์ (Google์ ๋ฌด๋ฃ STUN ์๋ฒ)
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
};
// ๋ก์ปฌ ๋ฏธ๋์ด ์คํธ๋ฆผ ๊ฐ์ ธ์ค๊ธฐ
async function startLocalStream() {
try {
localStream = await navigator.mediaDevices.getUserMedia({
video: { width: 1280, height: 720 },
audio: true
});
document.getElementById('localVideo').srcObject = localStream;
console.log('๋ก์ปฌ ์คํธ๋ฆผ ์์ ์ฑ๊ณต!');
} catch (error) {
console.error('๋ฏธ๋์ด ์ ๊ทผ ์ค๋ฅ:', error);
}
}
// PeerConnection ์์ฑ
function createPeerConnection() {
peerConnection = new RTCPeerConnection(configuration);
// ๋ก์ปฌ ์คํธ๋ฆผ์ ํธ๋๋ค์ PeerConnection์ ์ถ๊ฐ
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream);
});
// ์๊ฒฉ ์คํธ๋ฆผ ์์ ์ด๋ฒคํธ
peerConnection.ontrack = (event) => {
if (!remoteStream) {
remoteStream = new MediaStream();
document.getElementById('remoteVideo').srcObject = remoteStream;
}
remoteStream.addTrack(event.track);
};
// ICE candidate ์ด๋ฒคํธ
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
socket.emit('ice-candidate', event.candidate);
}
};
// ์ฐ๊ฒฐ ์ํ ๋ณ๊ฒฝ ์ด๋ฒคํธ
peerConnection.onconnectionstatechange = () => {
console.log('์ฐ๊ฒฐ ์ํ:', peerConnection.connectionState);
};
}
// Offer ์์ฑ ๋ฐ ์ ์ก (๋ฐ์ ์)
async function createOffer() {
createPeerConnection();
try {
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
socket.emit('offer', offer);
console.log('Offer ์ ์ก ์๋ฃ');
} catch (error) {
console.error('Offer ์์ฑ ์ค๋ฅ:', error);
}
}
// Offer ์์ ๋ฐ Answer ์์ฑ (์์ ์)
socket.on('offer', async (offer) => {
createPeerConnection();
try {
await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
socket.emit('answer', answer);
console.log('Answer ์ ์ก ์๋ฃ');
} catch (error) {
console.error('Answer ์์ฑ ์ค๋ฅ:', error);
}
});
// Answer ์์
socket.on('answer', async (answer) => {
try {
await peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
console.log('Answer ์์ ์๋ฃ');
} catch (error) {
console.error('Answer ์ค์ ์ค๋ฅ:', error);
}
});
// ICE candidate ์์
socket.on('ice-candidate', async (candidate) => {
try {
await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
console.log('ICE candidate ์ถ๊ฐ ์๋ฃ');
} catch (error) {
console.error('ICE candidate ์ถ๊ฐ ์ค๋ฅ:', error);
}
});
// ๋ฒํผ ์ด๋ฒคํธ
document.getElementById('startButton').onclick = async () => {
await startLocalStream();
createOffer();
};
document.getElementById('hangupButton').onclick = () => {
if (peerConnection) {
peerConnection.close();
peerConnection = null;
}
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
}
socket.emit('hangup');
};
โข
getUserMedia()๋ก ์นด๋ฉ๋ผ/๋ง์ดํฌ ์ ๊ทผโข
RTCPeerConnection ๊ฐ์ฒด ์์ฑโข
addTrack()์ผ๋ก ๋ก์ปฌ ์คํธ๋ฆผ ์ถ๊ฐโข
ontrack ์ด๋ฒคํธ๋ก ์๊ฒฉ ์คํธ๋ฆผ ์์ โข Socket.io๋ก SDP์ ICE candidate ๊ตํ
Step 3: Node.js ์๊ทธ๋๋ง ์๋ฒ ๊ตฌํํ๊ธฐ
// server.js
const express = require('express');
const app = express();
const server = require('http').createServer(app);
const io = require('socket.io')(server);
app.use(express.static('public'));
io.on('connection', (socket) => {
console.log('์๋ก์ด ํด๋ผ์ด์ธํธ ์ฐ๊ฒฐ:', socket.id);
// Offer ์ค๊ณ
socket.on('offer', (offer) => {
socket.broadcast.emit('offer', offer);
});
// Answer ์ค๊ณ
socket.on('answer', (answer) => {
socket.broadcast.emit('answer', answer);
});
// ICE candidate ์ค๊ณ
socket.on('ice-candidate', (candidate) => {
socket.broadcast.emit('ice-candidate', candidate);
});
// ํตํ ์ข
๋ฃ
socket.on('hangup', () => {
socket.broadcast.emit('hangup');
});
socket.on('disconnect', () => {
console.log('ํด๋ผ์ด์ธํธ ์ฐ๊ฒฐ ํด์ :', socket.id);
});
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`์๋ฒ๊ฐ ํฌํธ ${PORT}์์ ์คํ ์ค์
๋๋ค.`);
});
์ด ์๋ฒ๋ ์ ๋ง ๊ฐ๋จํด! ํด๋ผ์ด์ธํธ๋ค ์ฌ์ด์์ ๋ฉ์์ง๋ฅผ ์ค๊ณํ๋ ์ญํ ๋ง ํด. socket.broadcast.emit()์ ์ฌ์ฉํ๋ฉด ๋ฉ์์ง๋ฅผ ๋ณด๋ธ ํด๋ผ์ด์ธํธ๋ฅผ ์ ์ธํ ๋ชจ๋ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฉ์์ง๋ฅผ ์ ๋ฌํ ์ ์์ด. ๐ก
1. Node.js์ npm ์ค์น
2.
npm install express socket.io ์คํ3.
node server.js๋ก ์๋ฒ ์์4. ๋ธ๋ผ์ฐ์ ๋ ๊ฐ๋ฅผ ์ด์ด์
localhost:3000 ์ ์5. ํ์ชฝ์์ "ํตํ ์์" ๋ฒํผ ํด๋ฆญ!
๐ NAT์ ๋ฐฉํ๋ฒฝ: STUN๊ณผ TURN ์๋ฒ์ ํ์์ฑ
์, ์ด์ ์ฝ๋๋ ๋ค ์งฐ๊ณ ์คํ๋ ํด๋ดค์ด. ๊ทธ๋ฐ๋ฐ ๊ฐ๋ ์ฐ๊ฒฐ์ด ์ ๋๋ ๊ฒฝ์ฐ๊ฐ ์์ด. ์ ๊ทธ๋ด๊น? ๐ค ๋ฐ๋ก NAT(Network Address Translation)์ ๋ฐฉํ๋ฒฝ ๋๋ฌธ์ด์ผ!
NAT๋ ๊ณต์ธ IP ์ฃผ์๋ฅผ ์ ์ฝํ๊ธฐ ์ํด ์ฌ์ค IP ์ฃผ์๋ฅผ ๊ณต์ธ IP ์ฃผ์๋ก ๋ณํํ๋ ๊ธฐ์ ์ด์ผ. ์ง์์ ๊ณต์ ๊ธฐ๋ฅผ ์ฐ๋ฉด ์ฌ๋ฌ ๊ธฐ๊ธฐ๊ฐ ํ๋์ ๊ณต์ธ IP๋ฅผ ๊ณต์ ํ์์? ๊ทธ๊ฒ NAT์ผ!
๋ฌธ์ ๋ NAT ๋ค์ ์๋ ๊ธฐ๊ธฐ๋ค์ ์ธ๋ถ์์ ์ง์ ์ ๊ทผํ ์ ์๋ค๋ ๊ฑฐ์ผ. ๊ทธ๋์ WebRTC๋ STUN๊ณผ TURN ์๋ฒ๋ฅผ ์ฌ์ฉํด!
STUN ์๋ฒ๋ ํด๋ผ์ด์ธํธ์๊ฒ "๋์ ๊ณต์ธ IP ์ฃผ์๋ ์ด๊ฑฐ์ผ!"๋ผ๊ณ ์๋ ค์ฃผ๋ ์ญํ ์ ํด. ํด๋ผ์ด์ธํธ๋ ์ด ์ ๋ณด๋ฅผ ์๋๋ฐฉ์๊ฒ ์ ๋ฌํ๊ณ , ์๋ก ์ง์ ์ฐ๊ฒฐ์ ์๋ํ์ง.
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' }
]
};
Google์ด ๋ฌด๋ฃ๋ก ์ ๊ณตํ๋ STUN ์๋ฒ๋ฅผ ์ฌ์ฉํ ์ ์์ด! ๋๋ถ๋ถ์ ๊ฒฝ์ฐ(์ฝ 80~90%) STUN ์๋ฒ๋ง์ผ๋ก๋ ์ฐ๊ฒฐ์ด ์ฑ๊ณตํด. ๐
TURN ์๋ฒ๋ ์ง์ ์ฐ๊ฒฐ์ด ๋ถ๊ฐ๋ฅํ ๋ ์ฌ์ฉํ๋ ์ค๊ณ ์๋ฒ์ผ. ๋ฏธ๋์ด ๋ฐ์ดํฐ๊ฐ TURN ์๋ฒ๋ฅผ ๊ฑฐ์ณ์ ์ ๋ฌ๋๊ธฐ ๋๋ฌธ์ ์๋ฒ ๋ถํ๊ฐ ํฌ๊ณ ๋น์ฉ์ด ๋ฐ์ํด.
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:your-turn-server.com:3478',
username: 'username',
credential: 'password'
}
]
};
TURN ์๋ฒ๋ ์ง์ ๊ตฌ์ถํ๊ฑฐ๋ Twilio, Xirsys ๊ฐ์ ์๋น์ค๋ฅผ ์ ๋ฃ๋ก ์ฌ์ฉํ ์ ์์ด. ๊ธฐ์ ์ฉ ์๋น์ค๋ผ๋ฉด TURN ์๋ฒ๋ ํ์์ผ!
| NAT ํ์ | STUN ์ฑ๊ณต๋ฅ | TURN ํ์์ฑ |
|---|---|---|
| Full Cone NAT | 100% | ๋ถํ์ |
| Restricted Cone NAT | 90% | ๋ฎ์ |
| Port Restricted Cone NAT | 70% | ์ค๊ฐ |
| Symmetric NAT | 10% | ๋์ |
๐จ ํ์ง๊ณผ ์ฑ๋ฅ ์ต์ ํ: ์ฝ๋ฑ๊ณผ ๋นํธ๋ ์ดํธ ์กฐ์
ํ์ ํต์ ์ ๊ตฌํํ๋ค๋ฉด ์ด์ ํ์ง์ ๋์ฌ๋ณผ ์ฐจ๋ก์ผ! ๋คํธ์ํฌ ์ํฉ์ ๋ฐ๋ผ ํ์ง๊ณผ ์์ง์ ์กฐ์ ํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์. ๐
โข VP8: Google์ด ๊ฐ๋ฐํ ์คํ์์ค ์ฝ๋ฑ, ๋ฌด๋ฃ๋ก ์ฌ์ฉ ๊ฐ๋ฅ
โข VP9: VP8์ ํ์์, ๋ ๋์ ์์ถ๋ฅ
โข H.264: ๊ฐ์ฅ ๋๋ฆฌ ์ฌ์ฉ๋๋ ์ฝ๋ฑ, ํ๋์จ์ด ๊ฐ์ ์ง์
โข H.265 (HEVC): H.264๋ณด๋ค 50% ํฅ์๋ ์์ถ๋ฅ
โข AV1: ์ฐจ์ธ๋ ์ฝ๋ฑ, ์ต๊ณ ์ ์์ถ๋ฅ ์ด์ง๋ง ์์ง ์ง์์ด ์ ํ์
// ๋น๋์ค ์ ์ฝ ์กฐ๊ฑด ์ค์
const videoConstraints = {
width: { ideal: 1920, max: 1920 },
height: { ideal: 1080, max: 1080 },
frameRate: { ideal: 30, max: 60 },
facingMode: 'user' // 'user' ๋๋ 'environment'
};
// ์ค๋์ค ์ ์ฝ ์กฐ๊ฑด ์ค์
const audioConstraints = {
echoCancellation: true, // ์์ฝ ์ ๊ฑฐ
noiseSuppression: true, // ๋
ธ์ด์ฆ ์ ๊ฑฐ
autoGainControl: true, // ์๋ ๋ณผ๋ฅจ ์กฐ์
sampleRate: 48000 // ์ํ๋ง ๋ ์ดํธ
};
const stream = await navigator.mediaDevices.getUserMedia({
video: videoConstraints,
audio: audioConstraints
});
๋คํธ์ํฌ ์ํฉ์ด ๋๋น ์ง๋ฉด ์๋์ผ๋ก ํ์ง์ ๋ฎ์ถฐ์ ๋๊น์ ๋ฐฉ์งํ ์ ์์ด!
// ๋นํธ๋ ์ดํธ ์ ํ ์ค์
const sender = peerConnection.getSenders().find(s => s.track.kind === 'video');
const parameters = sender.getParameters();
if (!parameters.encodings) {
parameters.encodings = [{}];
}
// ์ต๋ ๋นํธ๋ ์ดํธ ์ค์ (bps)
parameters.encodings[0].maxBitrate = 1000000; // 1 Mbps
await sender.setParameters(parameters);
// ๋คํธ์ํฌ ์ํ ๋ชจ๋ํฐ๋ง
peerConnection.getStats().then(stats => {
stats.forEach(report => {
if (report.type === 'inbound-rtp' && report.kind === 'video') {
console.log('์์ ๋นํธ๋ ์ดํธ:', report.bytesReceived);
console.log('ํจํท ์์ค:', report.packetsLost);
console.log('์งํฐ:', report.jitter);
}
});
});
โข ์ ํ์ง (480p): 500 Kbps
โข ์คํ์ง (720p): 1.5 Mbps
โข ๊ณ ํ์ง (1080p): 3 Mbps
โข ์ด๊ณ ํ์ง (4K): 10 Mbps ์ด์
ํ์ง๊ณผ ์ฑ๋ฅ์ ํธ๋ ์ด๋์คํ ๊ด๊ณ์ผ. ์ฌ์ฉ์์ ๋คํธ์ํฌ ํ๊ฒฝ์ ๊ณ ๋ คํด์ ์ ์ ํ ์ค์ ์ ์ ํํด์ผ ํด! ๐ฑ๐ป
๐ฅ ๋ค์๊ฐ ํ์ ํ์: SFU์ MCU ์ํคํ ์ฒ
์ง๊ธ๊น์ง๋ 1:1 ํตํ๋ฅผ ๋ค๋ค์ด. ๊ทธ๋ฐ๋ฐ ์ค(Zoom)์ฒ๋ผ ์ฌ๋ฌ ๋ช ์ด ๋์์ ํตํํ๋ ค๋ฉด ์ด๋ป๊ฒ ํด์ผ ํ ๊น? ๐ค ์ฌ๊ธฐ์ SFU์ MCU ์ํคํ ์ฒ๊ฐ ๋ฑ์ฅํด!
SFU๋ ๊ฐ ์ฐธ๊ฐ์์ ์คํธ๋ฆผ์ ๋ฐ์์ ๋ค๋ฅธ ์ฐธ๊ฐ์๋ค์๊ฒ ๊ทธ๋๋ก ์ ๋ฌํ๋ ๋ฐฉ์์ด์ผ. ์๋ฒ๋ ๋จ์ํ ๋ผ์ฐํ ๋ง ํ๊ณ , ์ธ์ฝ๋ฉ/๋์ฝ๋ฉ์ ๊ฐ ํด๋ผ์ด์ธํธ๊ฐ ๋ด๋นํด.
์ฅ์ :
โข ์ง์ฐ์๊ฐ์ด ์งง์ (๋ณดํต 100~300ms)โข ์๋ฒ CPU ๋ถํ๊ฐ ์ ์ด
โข ๊ฐ ์ฐธ๊ฐ์๊ฐ ์๋ณธ ํ์ง์ ๋ฐ์ ์ ์์ด
๋จ์ :
โข ํด๋ผ์ด์ธํธ๊ฐ ์ฌ๋ฌ ์คํธ๋ฆผ์ ๋์์ ๋์ฝ๋ฉํด์ผ ํดโข ํด๋ผ์ด์ธํธ์ ์ ๋ก๋ ๋์ญํญ์ด ๋ง์ด ํ์ํด
โข ๋ชจ๋ฐ์ผ ๊ธฐ๊ธฐ์์๋ ๋ฐฐํฐ๋ฆฌ ์๋ชจ๊ฐ ์ปค
๋ํ ์๋ฃจ์ : Janus, Mediasoup, Jitsi
MCU๋ ๋ชจ๋ ์ฐธ๊ฐ์์ ์คํธ๋ฆผ์ ์๋ฒ์์ ํ๋๋ก ํฉ์ณ์(๋ฏน์ฑ) ๊ฐ ์ฐธ๊ฐ์์๊ฒ ์ ๋ฌํ๋ ๋ฐฉ์์ด์ผ. ์๋ฒ๊ฐ ๋ชจ๋ ์ธ์ฝ๋ฉ/๋์ฝ๋ฉ ์์ ์ ์ฒ๋ฆฌํด.
์ฅ์ :
โข ํด๋ผ์ด์ธํธ ๋ถํ๊ฐ ์ ์ด (ํ๋์ ์คํธ๋ฆผ๋ง ๋ฐ์ผ๋ฉด ๋ผ)โข ํด๋ผ์ด์ธํธ์ ๋์ญํญ ์ฌ์ฉ๋์ด ์ผ์ ํด
โข ์ ์ฌ์ ๊ธฐ๊ธฐ์์๋ ์ ์๋ํด
๋จ์ :
โข ์๋ฒ CPU ๋ถํ๊ฐ ๋งค์ฐ ๋์โข ์ง์ฐ์๊ฐ์ด ๊ธธ์ด (๋ณดํต 500ms~1์ด)
โข ์๋ฒ ๋น์ฉ์ด ๋ง์ด ๋ค์ด
โข ํ์ง ์์ค์ด ๋ฐ์ํ ์ ์์ด
๋ํ ์๋ฃจ์ : Kurento, Licode
์์ฆ์ SFU์ Simulcast์ SVC(Scalable Video Coding)๋ฅผ ๊ฒฐํฉํ ํ์ด๋ธ๋ฆฌ๋ ๋ฐฉ์์ ๋ง์ด ์จ!
โข Simulcast: ํด๋ผ์ด์ธํธ๊ฐ ์ฌ๋ฌ ํ์ง์ ์คํธ๋ฆผ์ ๋์์ ์ ์กํ๊ณ , SFU๊ฐ ๊ฐ ์์ ์์ ๋คํธ์ํฌ ์ํฉ์ ๋ง์ถฐ ์ ์ ํ ํ์ง์ ์ ํํด์ ์ ๋ฌ
โข SVC: ํ๋์ ์คํธ๋ฆผ์ ์ฌ๋ฌ ๋ ์ด์ด๋ก ์ธ์ฝ๋ฉํด์ ๋คํธ์ํฌ ์ํฉ์ ๋ฐ๋ผ ๋ ์ด์ด๋ฅผ ์ถ๊ฐ/์ ๊ฑฐํ ์ ์์ด
// Simulcast ์ค์ ์์
const sender = peerConnection.addTransceiver('video', {
direction: 'sendrecv',
streams: [localStream],
sendEncodings: [
{ rid: 'high', maxBitrate: 3000000 }, // ๊ณ ํ์ง
{ rid: 'mid', maxBitrate: 1000000, scaleResolutionDownBy: 2 }, // ์คํ์ง
{ rid: 'low', maxBitrate: 300000, scaleResolutionDownBy: 4 } // ์ ํ์ง
]
});
๋๋ถ๋ถ์ ํ๋์ ์ธ ํ์ ํ์ ์๋น์ค(Zoom, Google Meet, Microsoft Teams)๋ SFU + Simulcast ๋ฐฉ์์ ์ฌ์ฉํด! ์ด๊ฒ ๊ฐ์ฅ ํจ์จ์ ์ด๊ฑฐ๋ . ๐ช
๐ก๏ธ ๋ณด์๊ณผ ํ๋ผ์ด๋ฒ์: ์์ ํ ํ์ ํต์ ๊ตฌํํ๊ธฐ
ํ์ ํต์ ์ ๋ฏผ๊ฐํ ์ ๋ณด๊ฐ ์ค๊ฐ๋ ๋งํผ ๋ณด์์ด ์ ๋ง ์ค์ํด! WebRTC๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ณด์ ๊ธฐ๋ฅ์ด ๋ด์ฅ๋์ด ์์ง๋ง, ์ถ๊ฐ๋ก ์ ๊ฒฝ ์จ์ผ ํ ๋ถ๋ถ๋ค์ด ์์ด. ๐
โข DTLS (Datagram Transport Layer Security): ๋ฐ์ดํฐ ์ฑ๋ ์ํธํ
โข SRTP (Secure Real-time Transport Protocol): ๋ฏธ๋์ด ์คํธ๋ฆผ ์ํธํ
โข ๊ฐ์ ์ํธํ: WebRTC๋ ์ํธํ ์์ด๋ ์๋ํ์ง ์์!
getUserMedia() API๋ ๋ณด์์์ ์ด์ ๋ก HTTPS ํ๊ฒฝ์์๋ง ์๋ํด (localhost๋ ์์ธ). ํ๋ก๋์
ํ๊ฒฝ์์๋ ๋ฐ๋์ SSL ์ธ์ฆ์๋ฅผ ์ค์นํด์ผ ํด!
// Let's Encrypt๋ก ๋ฌด๋ฃ SSL ์ธ์ฆ์ ๋ฐ๊ธ
// Certbot ์ฌ์ฉ ์์
sudo certbot --nginx -d yourdomain.com
// Node.js์์ HTTPS ์๋ฒ ์ค์
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('/path/to/private.key'),
cert: fs.readFileSync('/path/to/certificate.crt')
};
const server = https.createServer(options, app);
server.listen(443);
โข ์ค๊ฐ์ ๊ณต๊ฒฉ(MITM): ์๊ทธ๋๋ง ์๋ฒ๊ฐ ํดํน๋๋ฉด ์ฐ๊ฒฐ ์ ๋ณด๊ฐ ๋ ธ์ถ๋ ์ ์์ด
โข ๋์ฒญ: TURN ์๋ฒ๋ฅผ ์ฌ์ฉํ ๋๋ ์ ๋ขฐํ ์ ์๋ ์๋น์ค๋ฅผ ์ ํํด์ผ ํด
โข ๊ถํ ๊ด๋ฆฌ: ์นด๋ฉ๋ผ/๋ง์ดํฌ ์ ๊ทผ ๊ถํ์ ์ ์ ํ ๊ด๋ฆฌํด์ผ ํด
โข ๋ นํ ๋ฐฉ์ง: ํด๋ผ์ด์ธํธ ์ธก์์ ๋ นํ๋ฅผ ์์ ํ ๋ง์ ์๋ ์์ด
1. ๋ฐฉ ์ ์ฅ ์ธ์ฆ
// JWT ํ ํฐ์ ์ฌ์ฉํ ์ธ์ฆ
const jwt = require('jsonwebtoken');
// ํ ํฐ ์์ฑ
const token = jwt.sign(
{ userId: user.id, roomId: room.id },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
// ํ ํฐ ๊ฒ์ฆ
io.use((socket, next) => {
const token = socket.handshake.auth.token;
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
socket.userId = decoded.userId;
next();
} catch (err) {
next(new Error('Authentication error'));
}
});
WebRTC๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ํธํ๋์ง๋ง, ์๋ฒ ๊ด๋ฆฌ์๋ ๋ณผ ์ ์๊ฒ ํ๋ ค๋ฉด ์ถ๊ฐ ์ํธํ ๋ ์ด์ด๊ฐ ํ์ํด. Insertable Streams API๋ฅผ ์ฌ์ฉํ๋ฉด ๊ฐ๋ฅํด!
3. ๋๊ธฐ์ค(Waiting Room) ๊ธฐ๋ฅํธ์คํธ๊ฐ ์น์ธํด์ผ๋ง ์ ์ฅํ ์ ์๊ฒ ํด์ ๋ฌด๋จ ์นจ์ ์ ๋ฐฉ์งํ ์ ์์ด.
4. ํ๋ฉด ๊ณต์ ์ํฐ๋งํฌํ๋ฉด ๊ณต์ ์ ์ฐธ๊ฐ์ ์ ๋ณด๋ฅผ ์ํฐ๋งํฌ๋ก ํ์ํด์ ๋ฌด๋จ ๋ นํ๋ฅผ ์ต์ ํ ์ ์์ด.
โข ์ฌ์ฉ์์๊ฒ ์นด๋ฉ๋ผ/๋ง์ดํฌ ์ฌ์ฉ ๋ชฉ์ ์ ๋ช ํํ ๊ณ ์งํด
โข ๋ นํ ์์ ์ ์ ๋ชจ๋ ์ฐธ๊ฐ์์๊ฒ ์๋ ค์ค
โข ํตํ ์ข ๋ฃ ํ ๋ฏธ๋์ด ์คํธ๋ฆผ์ ์์ ํ ์ ๋ฆฌํด (
track.stop())โข ๊ฐ์ธ์ ๋ณด ์ฒ๋ฆฌ๋ฐฉ์นจ์ ๋ช ํํ ์์ฑํด
๐ฏ ์ค์ ํ์ฉ: ๋ค์ํ ๊ธฐ๋ฅ ์ถ๊ฐํ๊ธฐ
๊ธฐ๋ณธ์ ์ธ ํ์ ํต์ ์ ๊ตฌํํ์ผ๋, ์ด์ ์ฌ์ฉ์ ๊ฒฝํ์ ๋์ฌ์ค ๋ค์ํ ๊ธฐ๋ฅ๋ค์ ์ถ๊ฐํด๋ณด์! ๐
ํ๋ฉด ๊ณต์ ๋ ์๊ฒฉ ํ์, ์จ๋ผ์ธ ๊ต์ก, ๊ธฐ์ ์ง์ ๋ฑ์์ ํ์์ ์ธ ๊ธฐ๋ฅ์ด์ผ!
// ํ๋ฉด ๊ณต์ ์์
async function startScreenShare() {
try {
const screenStream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: 'always', // ์ปค์ ํ์
displaySurface: 'monitor' // 'monitor', 'window', 'application'
},
audio: true // ์์คํ
์ค๋์ค ํฌํจ (๋ธ๋ผ์ฐ์ ์ง์ ํ์)
});
// ๋น๋์ค ํธ๋ ๊ต์ฒด
const videoTrack = screenStream.getVideoTracks()[0];
const sender = peerConnection.getSenders().find(s => s.track.kind === 'video');
sender.replaceTrack(videoTrack);
// ํ๋ฉด ๊ณต์ ์ค๋จ ๊ฐ์ง
videoTrack.onended = () => {
console.log('ํ๋ฉด ๊ณต์ ๊ฐ ์ค๋จ๋์์ต๋๋ค');
// ์๋ ์นด๋ฉ๋ผ๋ก ๋ณต๊ท
const cameraTrack = localStream.getVideoTracks()[0];
sender.replaceTrack(cameraTrack);
};
} catch (error) {
console.error('ํ๋ฉด ๊ณต์ ์ค๋ฅ:', error);
}
}
์ฐธ๊ฐ์๊ฐ ์์ ์ ์ค๋์ค/๋น๋์ค๋ฅผ ์ ์ดํ ์ ์์ด์ผ ํด!
// ์ค๋์ค ์์๊ฑฐ/ํด์
function toggleAudio() {
const audioTrack = localStream.getAudioTracks()[0];
audioTrack.enabled = !audioTrack.enabled;
return audioTrack.enabled;
}
// ๋น๋์ค ๋๊ธฐ/์ผ๊ธฐ
function toggleVideo() {
const videoTrack = localStream.getVideoTracks()[0];
videoTrack.enabled = !videoTrack.enabled;
return videoTrack.enabled;
}
// ๋ฒํผ ์ด๋ฒคํธ
document.getElementById('muteButton').onclick = () => {
const isEnabled = toggleAudio();
// UI ์
๋ฐ์ดํธ
document.getElementById('muteButton').textContent =
isEnabled ? '๐ค ์์๊ฑฐ' : '๐ ์์๊ฑฐ ํด์ ';
};
document.getElementById('videoButton').onclick = () => {
const isEnabled = toggleVideo();
document.getElementById('videoButton').textContent =
isEnabled ? '๐น ๋น๋์ค ๋๊ธฐ' : '๐ต ๋น๋์ค ์ผ๊ธฐ';
};
RTCDataChannel์ ์ฌ์ฉํ๋ฉด ํ ์คํธ, ํ์ผ ๋ฑ์ P2P๋ก ์ ์กํ ์ ์์ด!
// Data Channel ์์ฑ
const dataChannel = peerConnection.createDataChannel('chat');
dataChannel.onopen = () => {
console.log('๋ฐ์ดํฐ ์ฑ๋ ์ฐ๊ฒฐ๋จ');
};
dataChannel.onmessage = (event) => {
const message = JSON.parse(event.data);
displayMessage(message);
};
// ๋ฉ์์ง ์ ์ก
function sendMessage(text) {
const message = {
sender: myName,
text: text,
timestamp: Date.now()
};
dataChannel.send(JSON.stringify(message));
displayMessage(message);
}
// ์์ ์ ์ธก์์ Data Channel ๋ฐ๊ธฐ
peerConnection.ondatachannel = (event) => {
const receiveChannel = event.channel;
receiveChannel.onmessage = (e) => {
const message = JSON.parse(e.data);
displayMessage(message);
};
};
TensorFlow.js์ BodyPix ๋ชจ๋ธ์ ์ฌ์ฉํ๋ฉด ๋ฐฐ๊ฒฝ์ ํ๋ฆฌ๊ฒ ํ๊ฑฐ๋ ๊ต์ฒดํ ์ ์์ด!
// TensorFlow.js์ BodyPix ๋ก๋
import * as bodyPix from '@tensorflow-models/body-pix';
async function applyVirtualBackground() {
// ๋ชจ๋ธ ๋ก๋
const net = await bodyPix.load();
// ์บ๋ฒ์ค ์ค์
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const video = document.getElementById('localVideo');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
// ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง ๋ก๋
const backgroundImage = new Image();
backgroundImage.src = 'background.jpg';
async function processFrame() {
// ์ฌ๋ ์ธ๊ทธ๋ฉํ
์ด์
const segmentation = await net.segmentPerson(video);
// ๋ฐฐ๊ฒฝ ๊ทธ๋ฆฌ๊ธฐ
ctx.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height);
// ์ฌ๋ ๋ถ๋ถ๋ง ์๋ณธ ๋น๋์ค์์ ๊ฐ์ ธ์ค๊ธฐ
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixel = imageData.data;
for (let i = 0; i < segmentation.data.length; i++) {
if (segmentation.data[i] === 1) {
// ์ฌ๋ ์์ญ์ ์๋ณธ ๋น๋์ค ํฝ์
์ฌ์ฉ
const videoPixel = getVideoPixel(video, i);
pixel[i * 4] = videoPixel.r;
pixel[i * 4 + 1] = videoPixel.g;
pixel[i * 4 + 2] = videoPixel.b;
}
}
ctx.putImageData(imageData, 0, 0);
requestAnimationFrame(processFrame);
}
processFrame();
// ์บ๋ฒ์ค ์คํธ๋ฆผ์ PeerConnection์ ์ถ๊ฐ
const stream = canvas.captureStream(30);
const videoTrack = stream.getVideoTracks()[0];
const sender = peerConnection.getSenders().find(s => s.track.kind === 'video');
sender.replaceTrack(videoTrack);
}
๊ฐ์ ๋ฐฐ๊ฒฝ์ CPU๋ฅผ ๋ง์ด ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ์ฑ๋ฅ ์ต์ ํ๊ฐ ์ค์ํด. ํด์๋๋ฅผ ๋ฎ์ถ๊ฑฐ๋ ํ๋ ์ ๋ ์ดํธ๋ฅผ ์กฐ์ ํด์ ๋ถํ๋ฅผ ์ค์ผ ์ ์์ด!
์ฌ์ฉ์์๊ฒ ํ์ฌ ์ฐ๊ฒฐ ์ํ๋ฅผ ์๋ ค์ฃผ๋ ๊ฑด ์ค์ํด!
// ๋คํธ์ํฌ ํต๊ณ ์์ง
setInterval(async () => {
const stats = await peerConnection.getStats();
let quality = 'good';
stats.forEach(report => {
if (report.type === 'inbound-rtp' && report.kind === 'video') {
const packetsLost = report.packetsLost || 0;
const packetsReceived = report.packetsReceived || 0;
const lossRate = packetsLost / (packetsLost + packetsReceived);
if (lossRate > 0.1) quality = 'poor';
else if (lossRate > 0.05) quality = 'fair';
// UI ์
๋ฐ์ดํธ
updateQualityIndicator(quality);
}
});
}, 2000);
๐ ๋๋ฒ๊น ๊ณผ ํธ๋ฌ๋ธ์ํ : ์์ฃผ ๋ฐ์ํ๋ ๋ฌธ์ ๋ค
WebRTC ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด ์ฌ๋ฌ ๊ฐ์ง ๋ฌธ์ ์ ๋ถ๋ชํ๊ฒ ๋ผ. ์์ฃผ ๋ฐ์ํ๋ ๋ฌธ์ ๋ค๊ณผ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์์๋ณด์! ๐ง
์์ธ: HTTPS๊ฐ ์๋ HTTP ํ๊ฒฝ์์ ์คํ
ํด๊ฒฐ: localhost๊ฐ ์๋๋ผ๋ฉด HTTPS๋ฅผ ์ฌ์ฉํด์ผ ํด. Let's Encrypt๋ก ๋ฌด๋ฃ SSL ์ธ์ฆ์๋ฅผ ๋ฐ๊ธ๋ฐ์!
์์ธ: ์ฌ๋ฌ ๊ฐ์ง ๊ฐ๋ฅ์ฑ์ด ์์ด
์ฒดํฌ๋ฆฌ์คํธ:
โข
autoplay ์์ฑ์ด ์๋์ง ํ์ธโข
srcObject๊ฐ ์ ๋๋ก ์ค์ ๋์๋์ง ํ์ธโข ๋ธ๋ผ์ฐ์ ์ฝ์์์ ์๋ฌ ๋ฉ์์ง ํ์ธ
โข ์นด๋ฉ๋ผ ๊ถํ์ด ํ์ฉ๋์๋์ง ํ์ธ
โข ๋ค๋ฅธ ์ฑ์ด ์นด๋ฉ๋ผ๋ฅผ ์ฌ์ฉ ์ค์ธ์ง ํ์ธ
์์ธ: NAT/๋ฐฉํ๋ฒฝ ๋ฌธ์ ๋๋ STUN/TURN ์๋ฒ ์ค์ ์ค๋ฅ
ํด๊ฒฐ:
โข STUN ์๋ฒ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ค์ ๋์๋์ง ํ์ธ
โข TURN ์๋ฒ๋ฅผ ์ถ๊ฐํด๋ณด๊ธฐ
โข
iceConnectionState๋ฅผ ๋ชจ๋ํฐ๋งํด์ ์ด๋ ๋จ๊ณ์์ ์คํจํ๋์ง ํ์ธ
peerConnection.oniceconnectionstatechange = () => {
console.log('ICE ์ฐ๊ฒฐ ์ํ:', peerConnection.iceConnectionState);
// 'new' -> 'checking' -> 'connected' -> 'completed'
// 'failed'๊ฐ ๋์ค๋ฉด ์ฐ๊ฒฐ ์คํจ
if (peerConnection.iceConnectionState === 'failed') {
// ICE ์ฌ์์ ์๋
peerConnection.restartIce();
}
};
์์ธ: ๋ก์ปฌ ๋น๋์ค์์
muted ์์ฑ์ด ๋น ์ก๊ฑฐ๋, ์์ฝ ์ ๊ฑฐ ๊ธฐ๋ฅ์ด ๊บผ์ ธ ์์ํด๊ฒฐ:
// ๋ก์ปฌ ๋น๋์ค๋ ๋ฐ๋์ muted
<video id="localVideo" autoplay muted></video>
// ์ค๋์ค ์ ์ฝ ์กฐ๊ฑด์์ ์์ฝ ์ ๊ฑฐ ํ์ฑํ
const audioConstraints = {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
};
์์ธ: ๋ชจ๋ฐ์ผ ๋ธ๋ผ์ฐ์ ์ ์ ์ฝ์ฌํญ
ํด๊ฒฐ:
โข iOS Safari๋ WebRTC ์ง์์ด ์ ํ์ ์ด์ผ (iOS 11 ์ด์ ํ์)
โข ๋ชจ๋ฐ์ผ์์๋ ํด์๋์ ํ๋ ์ ๋ ์ดํธ๋ฅผ ๋ฎ์ถฐ์ผ ํด
โข ๋ฐฐํฐ๋ฆฌ ์ ์ฝ์ ์ํด ๋ฐฑ๊ทธ๋ผ์ด๋์์๋ ๋น๋์ค๊ฐ ์ค๋จ๋ ์ ์์ด
โข
playsinline ์์ฑ์ ์ถ๊ฐํด์ผ iOS์์ ์ธ๋ผ์ธ ์ฌ์์ด ๊ฐ๋ฅํด
<video autoplay playsinline muted></video>
โข chrome://webrtc-internals: Chrome์์ WebRTC ์ฐ๊ฒฐ ์ํ๋ฅผ ์์ธํ ๋ณผ ์ ์์ด
โข about:webrtc: Firefox์ WebRTC ๋๋ฒ๊น ํ์ด์ง
โข Wireshark: ๋คํธ์ํฌ ํจํท์ ๋ถ์ํ ์ ์์ด
โข WebRTC Troubleshooter: ์จ๋ผ์ธ ํ ์คํธ ๋๊ตฌ
๐ ์ธ๊ธฐ WebRTC ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ํ๋ ์์ํฌ
์ฒ์๋ถํฐ ๋ชจ๋ ๊ฑธ ์ง์ ๊ตฌํํ๋ ๊ฑด ์๊ฐ์ด ์ค๋ ๊ฑธ๋ ค. ๊ฒ์ฆ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด ํจ์ฌ ๋น ๋ฅด๊ฒ ๊ฐ๋ฐํ ์ ์์ด! ๐
| ๋ผ์ด๋ธ๋ฌ๋ฆฌ | ํน์ง | ๋์ด๋ | ์ถ์ฒ ์ฉ๋ |
|---|---|---|---|
| Simple-Peer | ๊ฐ์ฅ ๊ฐ๋จํ WebRTC ๋ํผ, ์ด๋ณด์ ์นํ์ | โญ | ๊ฐ๋จํ P2P ํต์ |
| PeerJS | ์ฌ์ด API, ๋ฌด๋ฃ ์๊ทธ๋๋ง ์๋ฒ ์ ๊ณต | โญโญ | ํ๋กํ ํ์ , ์๊ท๋ชจ ํ๋ก์ ํธ |
| Mediasoup | ๊ณ ์ฑ๋ฅ SFU, Node.js ๊ธฐ๋ฐ | โญโญโญโญ | ๋๊ท๋ชจ ํ์ ํ์ |
| Janus | C๋ก ์์ฑ๋ ๊ณ ์ฑ๋ฅ ์๋ฒ, ํ๋ฌ๊ทธ์ธ ์์คํ | โญโญโญโญ | ์ํฐํ๋ผ์ด์ฆ๊ธ ์๋น์ค |
| Jitsi | ์์ ํ ํ์ ํ์ ์๋ฃจ์ , ์คํ์์ค | โญโญโญ | ์์ฒด ํธ์คํ ํ์ ํ์ |
| Agora SDK | ์์ฉ SDK, ๋ฐ์ด๋ ํ์ง๊ณผ ์ง์ | โญโญ | ์์ฉ ์๋น์ค |
| Twilio Video | ํด๋ผ์ฐ๋ ๊ธฐ๋ฐ, ์ฌ์ฉํ ๋งํผ ๊ณผ๊ธ | โญโญ | ๋น ๋ฅธ ์ถ์๊ฐ ํ์ํ ์คํํธ์ |
๊ฐ์ฅ ๊ฐ๋จํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ Simple-Peer๋ก P2P ์ฐ๊ฒฐ์ ๋ง๋ค์ด๋ณด์!
// npm install simple-peer
const SimplePeer = require('simple-peer');
// Peer A (๋ฐ์ ์)
const peer1 = new SimplePeer({ initiator: true, stream: localStream });
peer1.on('signal', data => {
// ์ด data๋ฅผ Peer B์๊ฒ ์ ์ก (์๊ทธ๋๋ง ์๋ฒ ํตํด)
socket.emit('signal', data);
});
peer1.on('stream', remoteStream => {
// ์๊ฒฉ ์คํธ๋ฆผ ์์
remoteVideo.srcObject = remoteStream;
});
// Peer B (์์ ์)
const peer2 = new SimplePeer({ stream: localStream });
socket.on('signal', data => {
peer2.signal(data);
});
peer2.on('signal', data => {
socket.emit('signal', data);
});
peer2.on('stream', remoteStream => {
remoteVideo.srcObject = remoteStream;
});
Simple-Peer๋ WebRTC์ ๋ณต์กํ ๋ถ๋ถ์ ์จ๊ฒจ์ฃผ๊ณ ๊ฐ๋จํ API๋ง ์ ๊ณตํด. ํ์ต์ฉ์ด๋ ๊ฐ๋จํ ํ๋ก์ ํธ์ ๋ฑ์ด์ผ! ๐
๋๊ท๋ชจ ํ์ ํ์๋ฅผ ๊ตฌ์ถํ๋ค๋ฉด Mediasoup์ด ์ต๊ณ ์ ์ ํ์ด์ผ!
const mediasoup = require('mediasoup');
// Worker ์์ฑ
const worker = await mediasoup.createWorker({
logLevel: 'warn',
rtcMinPort: 10000,
rtcMaxPort: 10100
});
// Router ์์ฑ
const router = await worker.createRouter({
mediaCodecs: [
{
kind: 'audio',
mimeType: 'audio/opus',
clockRate: 48000,
channels: 2
},
{
kind: 'video',
mimeType: 'video/VP8',
clockRate: 90000
}
]
});
// WebRTC Transport ์์ฑ
const transport = await router.createWebRtcTransport({
listenIps: [{ ip: '0.0.0.0', announcedIp: 'YOUR_PUBLIC_IP' }],
enableUdp: true,
enableTcp: true,
preferUdp: true
});
// Producer ์์ฑ (์ก์ )
const producer = await transport.produce({
kind: 'video',
rtpParameters: clientRtpParameters
});
// Consumer ์์ฑ (์์ )
const consumer = await transport.consume({
producerId: producer.id,
rtpCapabilities: clientRtpCapabilities
});
๐ฐ ๋น์ฉ๊ณผ ์ธํ๋ผ: ์ค์ ์๋น์ค ์ด์ํ๊ธฐ
WebRTC ์๋น์ค๋ฅผ ์ค์ ๋ก ์ด์ํ๋ ค๋ฉด ์ด๋ ์ ๋ ๋น์ฉ์ด ๋ค๊น? ๊ท๋ชจ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง์ง๋ง ๋๋ต์ ์ธ ๊ฐ์ด๋๋ฅผ ์๋ ค์ค๊ฒ! ๐ต
์๊ท๋ชจ (๋์ ์ ์ ~50๋ช )
โข ์๊ทธ๋๋ง ์๋ฒ: AWS t3.small ($15/์) ๋๋ Heroku Hobby ($7/์)
โข TURN ์๋ฒ: Coturn ์์ฒด ํธ์คํ ($20/์) ๋๋ Twilio TURN ($0.0005/๋ถ)
โข ์ด ์์ ๋น์ฉ: $30~50/์ ์ค๊ท๋ชจ (๋์ ์ ์ ~500๋ช )
โข SFU ์๋ฒ: AWS c5.2xlarge ($250/์) x 2๋
โข ์๊ทธ๋๋ง ์๋ฒ: AWS t3.medium ($30/์)
โข TURN ์๋ฒ: ์ ์ฉ ์๋ฒ ($100/์)
โข ๋ก๋ ๋ฐธ๋ฐ์: AWS ALB ($20/์)
โข ์ด ์์ ๋น์ฉ: $650~800/์ ๋๊ท๋ชจ (๋์ ์ ์ ~5,000๋ช )
โข SFU ํด๋ฌ์คํฐ: AWS c5.4xlarge ($500/์) x 10๋
โข Redis ํด๋ฌ์คํฐ: AWS ElastiCache ($200/์)
โข ์๊ทธ๋๋ง ์๋ฒ ํด๋ฌ์คํฐ: ($150/์)
โข TURN ์๋ฒ ํด๋ฌ์คํฐ: ($500/์)
โข CDN ๋ฐ ๊ธฐํ: ($300/์)
โข ์ด ์์ ๋น์ฉ: $6,000~8,000/์
| ์๋น์ค | ๊ฐ๊ฒฉ ๋ชจ๋ธ | ์ฅ์ | ๋จ์ |
|---|---|---|---|
| Twilio Video | $0.0015/๋ถ/์ฐธ๊ฐ์ | ๋น ๋ฅธ ๊ตฌ์ถ, ์์ ์ | ๋น์ฉ์ด ๋์ |
| Agora | $0.99/1,000๋ถ | ํ์ง ์ฐ์, ๊ธ๋ก๋ฒ | ์ค๊ตญ ๊ธฐ์ |
| AWS Chime | $0.0017/๋ถ/์ฐธ๊ฐ์ | AWS ํตํฉ ์ฉ์ด | ๊ธฐ๋ฅ ์ ํ์ |
| Vonage Video | $0.0045/๋ถ/์ฐธ๊ฐ์ | ์ค๋๋ ๊ฒ์ฆ๋ ์๋น์ค | ๋น์ผ ํธ |
| ์์ฒด ๊ตฌ์ถ | ์๋ฒ ๋น์ฉ๋ง | ์์ ํ ์ ์ด, ์ ๋ ด | ๊ฐ๋ฐ/์ด์ ๋ถ๋ด |
โข ๋๋ถ๋ถ์ ์ฌ์ฉ์๋ STUN๋ง์ผ๋ก ์ฐ๊ฒฐ๋๋ฏ๋ก TURN ์๋ฒ ์ฌ์ฉ๋ฅ ์ 10~20% ์ ๋์ผ
โข Simulcast๋ฅผ ์ฌ์ฉํ๋ฉด ๋์ญํญ์ 30~50% ์ ์ฝํ ์ ์์ด
โข ๋ นํ ๊ธฐ๋ฅ์ ๋ณ๋ ์๋ฒ์์ ์ฒ๋ฆฌํด์ SFU ๋ถํ๋ฅผ ์ค์ฌ
โข ๋ฆฌ์ ๋ณ๋ก ์๋ฒ๋ฅผ ๋ถ์ฐํ๋ฉด ์ง์ฐ์๊ฐ์ ์ค์ด๊ณ ๋น์ฉ๋ ์ ๊ฐํ ์ ์์ด
โข ๋ฌด๋ฃ STUN ์๋ฒ(Google, Mozilla)๋ฅผ ํ์ฉํ์
์คํํธ์ ์ด๋ผ๋ฉด ์ฒ์์๋ Twilio๋ Agora ๊ฐ์ ํด๋ผ์ฐ๋ ์๋น์ค๋ฅผ ์ฐ๋ค๊ฐ, ๊ท๋ชจ๊ฐ ์ปค์ง๋ฉด ์์ฒด ๊ตฌ์ถ์ผ๋ก ์ ํํ๋ ๊ฒ ์ผ๋ฐ์ ์ด์ผ. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ WebRTC ๊ฐ๋ฐ ์ ๋ฌธ๊ฐ๋ฅผ ์ฐพ์์ ์ปจ์คํ ์ ๋ฐ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ง! ๐
๐ ๋ฏธ๋์ WebRTC: ์๋ก์ด ๊ธฐ์ ๊ณผ ํธ๋ ๋
WebRTC๋ ๊ณ์ ๋ฐ์ ํ๊ณ ์์ด! ์์ผ๋ก ์ด๋ค ๊ธฐ์ ๋ค์ด ๋์ฌ์ง ์ดํด๋ณด์. ๐ฎ
โข Insertable Streams: ๋ฏธ๋์ด ์คํธ๋ฆผ์ JavaScript๋ก ์ง์ ์กฐ์ํ ์ ์์ด (E2EE, ํํฐ ๋ฑ)
โข WebCodecs API: ์ ์์ค ์ฝ๋ฑ ์ ์ด๊ฐ ๊ฐ๋ฅํด์ ธ
โข WebTransport: QUIC ๊ธฐ๋ฐ์ ์๋ก์ด ์ ์ก ํ๋กํ ์ฝ
โข AV1 ์ฝ๋ฑ: ๋ ๋์ ์์ถ๋ฅ ๋ก ๋์ญํญ ์ ์ฝ
โข ์ค์๊ฐ ๋ฒ์ญ: ๋ค๊ตญ์ด ํ์ ํ์์์ ์๋ง ์๋ ์์ฑ
โข ๋ ธ์ด์ฆ ์ ๊ฑฐ: AI ๊ธฐ๋ฐ ๋ฐฐ๊ฒฝ ์์ ์ ๊ฑฐ (Krisp, NVIDIA RTX Voice)
โข ์๋ ํ๋ ์ด๋ฐ: ์นด๋ฉ๋ผ๊ฐ ์๋์ผ๋ก ์ผ๊ตด์ ์ถ์ ํ๊ณ ์ค
โข ๊ฐ์ ๋ถ์: ์ฐธ๊ฐ์์ ํ์ ์ ๋ถ์ํด์ ํ์ ๋ถ์๊ธฐ ํ์
โข ์ ์ค์ฒ ์ธ์: ์๋์์ผ๋ก ํ๋ฉด ์ ์ด
โข ๋ฉํ๋ฒ์ค ํ์: 3D ์๋ฐํ๋ก ๊ฐ์ ๊ณต๊ฐ์์ ํ์
โข ๊ณต๊ฐ ์ค๋์ค: 3D ์์น ๊ธฐ๋ฐ ์ค๋์ค๋ก ํ์ค๊ฐ ์ฆ๋
โข ๊ฐ์ ์ค๋ธ์ ํธ ๊ณต์ : AR๋ก 3D ๋ชจ๋ธ์ ๊ณต์ ํ๊ณ ํ์
5G์ ์ด์ ์ง์ฐ ํน์ฑ๊ณผ ์ฃ์ง ์ปดํจํ ์ ํ์ฉํ๋ฉด ๋์ฑ ๋ถ๋๋ฌ์ด ํ์ ํต์ ์ด ๊ฐ๋ฅํด์ ธ. ํนํ ๋ชจ๋ฐ์ผ ํ๊ฒฝ์์์ ํ์ง์ด ํฌ๊ฒ ํฅ์๋ ๊ฑฐ์ผ!
WebRTC๋ ์ด์ ๋จ์ํ ํ์ ํตํ๋ฅผ ๋์ด์ ์๊ฒฉ ์๋ฃ, ์๊ฒฉ ๊ต์ก, ๊ฒ์, IoT ๋ฑ ๋ค์ํ ๋ถ์ผ๋ก ํ์ฅ๋๊ณ ์์ด. ์์ผ๋ก ๋์ฑ ํฅ๋ฏธ์ง์งํ ๋ฐ์ ์ด ๊ธฐ๋๋ผ! ๐
๐ ํ์ต ๋ฆฌ์์ค์ ์ปค๋ฎค๋ํฐ
WebRTC๋ฅผ ๋ ๊น์ด ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด ์ด๋ฐ ๋ฆฌ์์ค๋ค์ ํ์ฉํด๋ด! ๐
โข MDN Web Docs: ๊ฐ์ฅ ์น์ ํ WebRTC ๊ฐ์ด๋
โข W3C WebRTC Spec: ๊ณต์ ํ์ค ๋ฌธ์
โข webrtc.org: Google์ WebRTC ํ๋ก์ ํธ ์ฌ์ดํธ
โข WebRTC Samples: ๋ค์ํ ์์ ์ฝ๋
โข discuss-webrtc Google Group: ๊ฐ์ฅ ํ๋ฐํ WebRTC ์ปค๋ฎค๋ํฐ
โข WebRTC Weekly: ์ฃผ๊ฐ ๋ด์ค๋ ํฐ
โข Stack Overflow: webrtc ํ๊ทธ๋ก ์ง๋ฌธํ๊ธฐ
โข Reddit r/webrtc: ๊ฐ๋ฐ์๋ค์ ํ ๋ก ๊ณต๊ฐ
โข Udemy: "WebRTC Practical Course" ๋ฑ ๋ค์ํ ๊ฐ์
โข YouTube: Traversy Media, Fireship ๋ฑ์ ํํ ๋ฆฌ์ผ
โข ์ฌ๋ฅ๋ท: WebRTC ์ ๋ฌธ๊ฐ์ 1:1 ๋ฉํ ๋ง ๋ฐ๊ธฐ (์ฌ๊ธฐ์ ์์ฐ์ค๋ฝ๊ฒ ์ฌ๋ฅ๋ท ์ธ๊ธ! ๐)
1. HTML5 Video์ getUserMedia๋ก ๊ธฐ์ด ๋ค์ง๊ธฐ
2. Simple-Peer๋ก ๊ฐ๋จํ P2P ์ฐ๊ฒฐ ๋ง๋ค์ด๋ณด๊ธฐ
3. ์๊ทธ๋๋ง ์๋ฒ ์ง์ ๊ตฌํํด๋ณด๊ธฐ
4. STUN/TURN ์๋ฒ ์ค์ ํ๊ณ NAT ํต๊ณผ ์ดํดํ๊ธฐ
5. SFU ์ํคํ ์ฒ๋ก ๋ค์๊ฐ ํต์ ๊ตฌํํ๊ธฐ
6. ์ฑ๋ฅ ์ต์ ํ์ ๋ณด์ ๊ฐํํ๊ธฐ
7. ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํ๊ธฐ
๐ฌ ๋ง๋ฌด๋ฆฌ: ๋น์ ๋ WebRTC ๋ง์คํฐ๊ฐ ๋ ์ ์์ด!
์! ์ฌ๊ธฐ๊น์ง ์ฝ์๋ค๋ฉด ์ ๋ง ๋๋จํด! ๐ ์ฐ๋ฆฌ๋ WebRTC์ ๊ธฐ์ด๋ถํฐ ์ค์ ๊ตฌํ, ๊ณ ๊ธ ๊ธฐ๋ฅ, ๊ทธ๋ฆฌ๊ณ ๋ฏธ๋ ํธ๋ ๋๊น์ง ๋ชจ๋ ์ดํด๋ดค์ด.
โข WebRTC๋ P2P ์ค์๊ฐ ํต์ ๊ธฐ์ ์ด์ผ. ๋ธ๋ผ์ฐ์ ๋ผ๋ฆฌ ์ง์ ์ฐ๊ฒฐํด์ ์ค๋์ค, ๋น๋์ค, ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์.
โข ์๊ทธ๋๋ง ์๋ฒ๋ ์ด๊ธฐ ์ฐ๊ฒฐ์ ์ํด ํ์ํ์ง๋ง, ์ค์ ๋ฏธ๋์ด๋ P2P๋ก ์ ์ก๋ผ.
โข STUN/TURN ์๋ฒ๋ NAT ํต๊ณผ๋ฅผ ์ํด ํ์ํด. ๋๋ถ๋ถ์ STUN๋ง์ผ๋ก ์ถฉ๋ถํ์ง๋ง, ์ผ๋ถ๋ TURN์ด ํ์ํด.
โข SFU ์ํคํ ์ฒ๊ฐ ๋ค์๊ฐ ํต์ ์ ๊ฐ์ฅ ํจ์จ์ ์ด์ผ.
โข ๋ณด์์ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ด์ฅ๋์ด ์์ง๋ง, HTTPS์ ์ถ๊ฐ ์ธ์ฆ์ ํ์์ผ.
โข ์ฑ๋ฅ ์ต์ ํ๋ ์ฝ๋ฑ ์ ํ, ๋นํธ๋ ์ดํธ ์กฐ์ , Simulcast ๋ฑ์ผ๋ก ๊ฐ๋ฅํด.
WebRTC๋ ์ฒ์์๋ ๋ณต์กํด ๋ณด์ด์ง๋ง, ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ๋ฐฐ์๊ฐ๋ฉด ์ ๋ง ์ฌ๋ฏธ์๋ ๊ธฐ์ ์ด์ผ! ์ง์ ํ์ ํต์ ์ฑ์ ๋ง๋ค์ด๋ณด๋ฉด์ ์ค๋ ฅ์ ์์๋ณด์. ๐ช
ํน์ ๋งํ๋ ๋ถ๋ถ์ด ์๊ฑฐ๋ ๋ ๊น์ด ๋ฐฐ์ฐ๊ณ ์ถ๋ค๋ฉด, ์ฌ๋ฅ๋ท์์ WebRTC ์ ๋ฌธ๊ฐ๋ฅผ ์ฐพ์์ ๋์์ ๋ฐ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ. ์ค๋ฌด ๊ฒฝํ์ด ํ๋ถํ ๊ฐ๋ฐ์๋ค์ด 1:1๋ก ๋ฉํ ๋งํด์ค ์ ์๊ฑฐ๋ ! ๐
1. ๊ฐ๋จํ 1:1 ํ์ ํตํ ์ฑ ๋ง๋ค์ด๋ณด๊ธฐ
2. ์น๊ตฌ๋ค๊ณผ ํ ์คํธํด๋ณด๊ธฐ
3. ํ๋ฉด ๊ณต์ , ์ฑํ ๋ฑ ๊ธฐ๋ฅ ์ถ๊ฐํ๊ธฐ
4. ํฌํธํด๋ฆฌ์ค์ ์ฌ๋ฆฌ๊ณ ์ทจ์ /ํ๋ฆฌ๋์ฑ ๋์ ํ๊ธฐ
5. ์คํ์์ค ํ๋ก์ ํธ์ ๊ธฐ์ฌํ๊ธฐ
WebRTC๋ ์์ผ๋ก ๋์ฑ ์ค์ํด์ง ๊ธฐ์ ์ด์ผ. ์๊ฒฉ ๊ทผ๋ฌด, ์จ๋ผ์ธ ๊ต์ก, ๋ฉํ๋ฒ์ค ๋ฑ ๋ชจ๋ ๊ณณ์์ ํ์ํ๊ฑฐ๋ . ์ง๊ธ ๋ฐฐ์๋๋ฉด ์ ๋ง ์ ์ฉํ ๊ฑฐ์ผ! ๐
์, ์ด์ ์ฝ๋ ์๋ํฐ๋ฅผ ์ด๊ณ ์ง์ ๋ง๋ค์ด๋ณด์! ๊ถ๊ธํ ์ ์ด ์์ผ๋ฉด ์ธ์ ๋ ์ง ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํ๊ณ , ์คํจ๋ฅผ ๋๋ ค์ํ์ง ๋ง๊ณ ๊ณ์ ๋์ ํด๋ด. ๋น์ ๋ ์ถฉ๋ถํ ๋ฉ์ง ํ์ ํต์ ์ฑ์ ๋ง๋ค ์ ์์ด! ๐
Happy Coding! ๐๐ป๐ฅ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.