Ver3.0 ๐ฅ WebRTC๋ก ์ค์๊ฐ ์์ ํตํ ๋ง๋ค๊ธฐ

๐ฅ WebRTC๋ก ์ค์๊ฐ ์์ ํตํ ๋ง๋ค๊ธฐ
์ค๋์ ๋ฐ๋ก ๊ทธ ๊ถ๊ธ์ฆ์ ํด๊ฒฐํด์ค WebRTC(Web Real-Time Communication)์ ๋ํด ์ด์ผ๊ธฐํด๋ณผ๊ฒ! ๐ ๋ณต์กํด ๋ณด์ด์ง๋ง, ์ฐจ๊ทผ์ฐจ๊ทผ ๋ฐ๋ผ์ค๋ฉด ์๊ฐ๋ณด๋ค ์ฌ๋ฏธ์๊ณ ์ ๊ธฐํ ๊ธฐ์ ์ด์ผ. ํ๋ก๊ทธ๋๋ฐ์ ๊ด์ฌ ์๋ ์น๊ตฌ๋ค์ด๋ผ๋ฉด ๊ผญ ์์๋๋ฉด ์ข์ JavaScript ๊ธฐ๋ฐ ๊ธฐ์ ์ด๋๊น ์ง์คํด๋ณด์!
๐ฏ WebRTC๊ฐ ๋ญ๊ธธ๋?
์์ ์๋ ํ์ ํตํ๋ฅผ ํ๋ ค๋ฉด ํ๋์(Flash)๋ ํน๋ณํ ํ๋ฌ๊ทธ์ธ์ ์ค์นํด์ผ ํ์ด. ๊ธฐ์ต๋? ๊ทธ๋ฐ๋ฐ WebRTC๋ ๋ณ๋์ ์ค์น ์์ด ๋ธ๋ผ์ฐ์ ๋ง ์์ผ๋ฉด ๋ฐ๋ก ์ฌ์ฉํ ์ ์์ด. ํฌ๋กฌ, ํ์ด์ดํญ์ค, ์ฌํ๋ฆฌ, ์ฃ์ง ๋ฑ ์ฃผ์ ๋ธ๋ผ์ฐ์ ๋ค์ด ๋ชจ๋ ์ง์ํ๊ณ ์์ง! ๐
๊ฐ์ฅ ๋ฉ์ง ์ ์ P2P(Peer-to-Peer) ๋ฐฉ์์ผ๋ก ๋์ํ๋ค๋ ๊ฑฐ์ผ. ์๋ฒ๋ฅผ ๊ฑฐ์น์ง ์๊ณ ์ฌ์ฉ์๋ผ๋ฆฌ ์ง์ ์ฐ๊ฒฐ๋๊ธฐ ๋๋ฌธ์ ์ง์ฐ ์๊ฐ์ด ์์ฒญ ์งง์. ๊ฒ์ ์คํธ๋ฆฌ๋ฐ์ด๋ ํ์ ํ์์ฒ๋ผ ์ค์๊ฐ์ฑ์ด ์ค์ํ ์๋น์ค์ ๋ฑ์ด์ง! โก
- โ ๋ฌด๋ฃ ์คํ์์ค: ๊ตฌ๊ธ์ด ์ฃผ๋ํ๋ ์คํ์์ค ํ๋ก์ ํธ
- โ ํ๋ฌ๊ทธ์ธ ๋ถํ์: ๋ธ๋ผ์ฐ์ ์์ฒด์ ๋ด์ฅ๋ API
- โ ์ด์ ์ง์ฐ: P2P ์ง์ ์ฐ๊ฒฐ๋ก ์ค์๊ฐ ํต์
- โ ๋ค์ํ ๋ฏธ๋์ด: ์์, ์์ฑ, ๋ฐ์ดํฐ ๋ชจ๋ ์ ์ก ๊ฐ๋ฅ
- โ ๋ณด์: ๊ธฐ๋ณธ์ ์ผ๋ก ์ํธํ(DTLS, SRTP) ์ ์ฉ
- โ ํฌ๋ก์ค ํ๋ซํผ: ์น, ๋ชจ๋ฐ์ผ, ๋ฐ์คํฌํฑ ๋ชจ๋ ์ง์
๐๏ธ WebRTC์ ๊ตฌ์กฐ์ ํต์ฌ API
1๏ธโฃ MediaStream API (getUserMedia)
๊ฐ๋จํ ์์ ๋ฅผ ๋ณผ๊น?
// ์นด๋ฉ๋ผ์ ๋ง์ดํฌ ์ ๊ทผํ๊ธฐ
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
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('์๋๋ฐฉ ์์ ์์ ! ๐');
};
P2P ์ฐ๊ฒฐ์ด ์ด์์ ์ด์ง๋ง, ๋ฐฉํ๋ฒฝ์ด๋ NAT ๋๋ฌธ์ ์ง์ ์ฐ๊ฒฐ์ด ์ ๋ ๋๊ฐ ์์ด. ์ด๋ ํ์ํ ๊ฒ:
STUN (Session Traversal Utilities for NAT)
โ ์์ ์ ๊ณต์ธ IP ์ฃผ์๋ฅผ ์์๋ด๋ ์๋ฒ. ๋ฌด๋ฃ๋ก ๋ง์ด ์ ๊ณต๋ผ! ๐
TURN (Traversal Using Relays around NAT)
โ ์ง์ ์ฐ๊ฒฐ์ด ๋ถ๊ฐ๋ฅํ ๋ ์ค๊ณ ์ญํ ์ ํ๋ ์๋ฒ. ํธ๋ํฝ ๋น์ฉ์ด ๋ค์ด์ ์ ๋ฃ๊ฐ ๋ง์. ๐ฐ
3๏ธโฃ RTCDataChannel API
// ๋ฐ์ดํฐ ์ฑ๋ ์์ฑ
const dataChannel = peerConnection.createDataChannel('chat');
// ๋ฉ์์ง ์ ์ก
dataChannel.send('์๋
ํ์ธ์! ๐');
// ๋ฉ์์ง ์์
dataChannel.onmessage = (event) => {
console.log('๋ฐ์ ๋ฉ์์ง:', event.data);
};
// ์ฐ๊ฒฐ ์ํ ํ์ธ
dataChannel.onopen = () => {
console.log('๋ฐ์ดํฐ ์ฑ๋ ์ฐ๊ฒฐ๋จ! โ
');
};
dataChannel.onclose = () => {
console.log('๋ฐ์ดํฐ ์ฑ๋ ์ข
๋ฃ๋จ! โ');
};
๐ ์๊ทธ๋๋ง(Signaling) ํ๋ก์ธ์ค
์๊ฐํด๋ด. ๋ ์ฌ๋์ด ์ ํ ํตํ๋ฅผ ํ๋ ค๋ฉด ๋จผ์ ์ ํ๋ฒํธ๋ฅผ ๊ตํํด์ผ ํ์์? WebRTC๋ ๋ง์ฐฌ๊ฐ์ง์ผ. ์๋ก์ ๋คํธ์ํฌ ์ ๋ณด๋ฅผ ๊ตํํด์ผ ์ง์ ์ฐ๊ฒฐํ ์ ์์ง!
๐ SDP (Session Description Protocol)
์ฐ๊ฒฐ ๊ณผ์ ์ ์ด๋ ๊ฒ ์งํ๋ผ:
์ฌ์ฉ์ A๊ฐ ์ฐ๊ฒฐ์ ์์ํ๋ฉด์ ์์ ์ SDP Offer๋ฅผ ์์ฑํด.
// Offer ์์ฑ
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
// ์๊ทธ๋๋ง ์๋ฒ๋ก ์ ์ก
signalingServer.send({
type: 'offer',
sdp: offer
});
์ฌ์ฉ์ 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 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 ํ์ ํตํ ๋ง๋ค๊ธฐ
๐ฅ๏ธ ์๋ฒ ์ธก ์ฝ๋ (Node.js + 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 ? '๐น' : '๐ด';
});
๐ฅ๏ธ ํ๋ฉด ๊ณต์
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('ํ๋ฉด ๊ณต์ ์ข
๋ฃ! ๐น');
}
๐ฌ ์ฑํ ๊ธฐ๋ฅ ์ถ๊ฐ
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 ๋ฐฉ์)
์ธ ๊ฐ์ง ๋ฐฉ์์ด ์์ด:
| ๋ฐฉ์ | ์ค๋ช | ์ฅ์ | ๋จ์ |
|---|---|---|---|
| Mesh (P2P) | ๋ชจ๋ ์ฐธ๊ฐ์๊ฐ ์๋ก ์ง์ ์ฐ๊ฒฐ | ์ง์ฐ ์๊ฐ ์ต์, ์๋ฒ ๋ถ๋ด ์์ | ์ฐธ๊ฐ์ ์ฆ๊ฐ ์ ๋์ญํญ ํญ์ฆ (3๋ช ๊น์ง ๊ถ์ฅ) |
| SFU | ์๋ฒ๊ฐ ์คํธ๋ฆผ ์ค๊ณ (๋ฏน์ฑ ์์) | ํ์ฅ์ฑ ์ข์, ํ์ง ์ ์ง | ์๋ฒ ๋์ญํญ ํ์ |
| MCU | ์๋ฒ๊ฐ ์คํธ๋ฆผ ๋ฏน์ฑ | ํด๋ผ์ด์ธํธ ๋ถ๋ด ์ต์ | ์๋ฒ CPU ๋ถ๋ด ํผ, ์ง์ฐ ๋ฐ์ |
๊ฐ๋จํ 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);
}
}
๐ ๋ณด์ ๊ณ ๋ ค์ฌํญ
๐ก๏ธ ๊ธฐ๋ณธ ๋ณด์ ๊ธฐ๋ฅ
โ 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 });
});
});
๐ ๋๋ฒ๊น ๋ฐ ๋ฌธ์ ํด๊ฒฐ
โ ์์ฃผ ๋ฐ์ํ๋ ์ค๋ฅ๋ค
์ฌ์ฉ์๊ฐ ์นด๋ฉ๋ผ/๋ง์ดํฌ ๊ถํ์ ๊ฑฐ๋ถํ์ ๋ ๋ฐ์ํด.
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;
}
}
๋ฐฉํ๋ฒฝ์ด๋ 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 ์ ์ก...
}
์๊ธฐ ๋ชฉ์๋ฆฌ๊ฐ ๋ค์ ๋ค๋ฆฌ๋ ํ์.
// ํด๊ฒฐ ๋ฐฉ๋ฒ 1: ๋ก์ปฌ ๋น๋์ค ์์๊ฑฐ
document.getElementById('localVideo').muted = true;
// ํด๊ฒฐ ๋ฐฉ๋ฒ 2: ์์ฝ ์ ๊ฑฐ ์ต์
ํ์ฑํ
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
},
video: true
});
๐ ๋๋ฒ๊น ๋๊ตฌ
ํฌ๋กฌ ์ฃผ์์ฐฝ์ 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 ํ์ฑํ
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 ์ ๋ฌธ๊ฐ๋ฅผ ์ฐพ์๋ณผ ์๋ ์์ด. ์ค๋ฌด ๊ฒฝํ์ด ํ๋ถํ ๊ฐ๋ฐ์๋ค์ด ๋ง์ด ํ๋ํ๊ณ ์๊ฑฐ๋ ! ๐
1๏ธโฃ ์จ๋ผ์ธ ์คํฐ๋ ๋ฃธ
โข ํ์ด๋จธ ๋ฐ ์ง์ค ์๊ฐ ์ธก์
โข ๊ณต๋ถ ์๊ฐ ํต๊ณ
โข ๋ฐฐ๊ฒฝ ์์ ๊ณต์ ๊ธฐ๋ฅ
2๏ธโฃ ์๊ฒฉ ์๋ฃ ์๋ด ํ๋ซํผ
โข ์๋ฃ ๊ธฐ๋ก ํ๋ฉด ๊ณต์
โข ์ฑํ ์ผ๋ก ์ฒ๋ฐฉ์ ์ ๋ฌ
โข ์์ฝ ์์คํ ์ฐ๋
3๏ธโฃ ์ค์๊ฐ ํ์ ํ์ดํธ๋ณด๋
โข ์ค์๊ฐ ๊ทธ๋ฆผ ๊ณต์ (DataChannel)
โข ๋ค์ค ์ฌ์ฉ์ ๋์ ํธ์ง
โข ์คํฌ๋ฆฐ์ท ์ ์ฅ ๊ธฐ๋ฅ
4๏ธโฃ ๋๋ค ์ฑํ ๋ฃฐ๋
โข ๊ด์ฌ์ฌ ๊ธฐ๋ฐ ํํฐ๋ง
โข ์ ๊ณ ๋ฐ ์ฐจ๋จ ์์คํ
โข ์ต๋ช ์ฑ ๋ณด์ฅ
5๏ธโฃ ์จ๋ผ์ธ ๋ฉด์ ์์คํ
โข ํ๋ฉด ๋ นํ ๊ธฐ๋ฅ
โข ์ฝ๋ฉ ํ ์คํธ ํ๋ฉด ๊ณต์
โข ํ๊ฐ ๋ฉ๋ชจ ๊ธฐ๋ฅ
๐ ์ ์ฉํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ฐ ํ๋ ์์ํฌ
๐ฏ ์ถ์ฒ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
โ 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)
});
// ์ด๊ฒ ๋! ์์ฒญ ๊ฐ๋จํ์ง? ๐
๐ ํ์ต ๋ฆฌ์์ค ๋ฐ ์ปค๋ฎค๋ํฐ
โข 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์ ๊ธฐ๋ณธ ๊ฐ๋ : P2P ์ค์๊ฐ ํต์
โ ํต์ฌ API 3๊ฐ์ง: MediaStream, RTCPeerConnection, RTCDataChannel
โ ์๊ทธ๋๋ง ํ๋ก์ธ์ค: SDP์ ICE Candidate ๊ตํ
โ ์ค์ ๊ตฌํ: 1๋1 ํ์ ํตํ ๋ง๋ค๊ธฐ
โ ๊ณ ๊ธ ๊ธฐ๋ฅ: ํ๋ฉด ๊ณต์ , ์ฑํ , ๋ค์๊ฐ ํ์
โ ๋ณด์๊ณผ ์ต์ ํ: HTTPS, TURN ์ธ์ฆ, ๋นํธ๋ ์ดํธ ์กฐ์
โ ๋๋ฒ๊น ๋ฐฉ๋ฒ: chrome://webrtc-internals ํ์ฉ
์ด์ ๋๋ WebRTC ๊ฐ๋ฐ์์ผ! ๐ ์์ ํ๋ก์ ํธ๋ถํฐ ์์ํด์ ์ ์ ๋ณต์กํ ๊ธฐ๋ฅ์ ์ถ๊ฐํด๋ณด์. ์ค์ ๋ก ๋ง๋ค์ด๋ณด๋ฉด์ ๋ฐฐ์ฐ๋ ๊ฒ ๊ฐ์ฅ ๋น ๋ฅธ ๋ฐฉ๋ฒ์ด์ผ.
ํน์ ๊ฐ๋ฐํ๋ค๊ฐ ์ด๋ ค์ด ๋ถ๋ถ์ด ์์ผ๋ฉด ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํ๊ฑฐ๋, ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ๋ ๊ฒ๋ ์ข์. ํผ์ ๊ณ ๋ฏผํ์ง ๋ง๊ณ ์ ๊ทน์ ์ผ๋ก ๋์์ ์์ฒญํ์! ๐ช
WebRTC๋ ๊ณ์ ๋ฐ์ ํ๊ณ ์๋ ๊ธฐ์ ์ด์ผ. ์๋ก์ด ๊ธฐ๋ฅ๋ค์ด ์ถ๊ฐ๋๊ณ , ๋ธ๋ผ์ฐ์ ์ง์๋ ์ ์ ์ข์์ง๊ณ ์์ง. ์์ผ๋ก ๋ ๋ง์ ์๋น์ค๋ค์ด WebRTC๋ฅผ ํ์ฉํ ๊ฑฐ์ผ. ์ง๊ธ ๋ฐฐ์๋๋ฉด ์ ๋ง ์ ์ฉํ ๊ฑฐ์ผ! ๐
์, ์ด์ ์ฝ๋ ์๋ํฐ๋ฅผ ์ด๊ณ ์ฒซ ๋ฒ์งธ WebRTC ํ๋ก์ ํธ๋ฅผ ์์ํด๋ณด์! ํ์ดํ ! ๐ปโจ
WebRTC ๊ฐ๋ฐ์ ์์ํ๋ ๋ชจ๋ ๋ถ๋ค์ ์์ํฉ๋๋ค! ๐ช
๊ถ๊ธํ ์ ์ด ์๋ค๋ฉด ์ธ์ ๋ ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํด๋ณด์ธ์.
ํจ๊ป ์ฑ์ฅํ๋ ๊ฐ๋ฐ์๊ฐ ๋์ด์! ๐ฑ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.