๐ ๋ชฉ์ฐจ
- ํ๋ก์ ํธ ์๊ฐ
- TURN/STUN ์๋ฒ ๊ตฌ์ถ
- Spring Boot ์๊ทธ๋๋ง ์๋ฒ ๊ตฌํ
- React ํด๋ผ์ด์ธํธ ๊ตฌํ
- ํ ์คํธ ๋ฐ ๋ฌธ์ ํด๊ฒฐ
๐ฏ ํ๋ก์ ํธ ์๊ฐ
WebRTC(Web Real-Time Communication)๋ฅผ ์ด์ฉํ์ฌ ๋ธ๋ผ์ฐ์ ๊ฐ ์ค์๊ฐ ํ์ ์ฑํ ์ ๊ตฌํํ๋ ํ๋ก์ ํธ์ ๋๋ค.
๐ ๏ธ ์ฌ์ฉ ๊ธฐ์
- TURN/STUN ์๋ฒ: Coturn
- ๋ฐฑ์๋: Spring Boot
- ํ๋ก ํธ์๋: React + TypeScript
- ํต์ : WebSocket, WebRTC
๐ง TURN/STUN ์๋ฒ ๊ตฌ์ถ
1๏ธโฃ Coturn ์ค์น
# ์์คํ
์
๋ฐ์ดํธ
sudo apt update
sudo apt upgrade
# Coturn ์ค์น
sudo apt install coturn
2๏ธโฃ Coturn ์ค์
# ์๋น์ค ํ์ฑํ
sudo sed -i 's/#TURNSERVER_ENABLED=1/TURNSERVER_ENABLED=1/' /etc/default/coturn
# ์ค์ ํ์ผ ์์ฑ
sudo bash -c 'cat > /etc/turnserver.conf << EOF
external-ip=YOUR_PUBLIC_IP
realm=YOUR_PUBLIC_IP
lt-cred-mech
user=turnuser:Turn2024!@#
EOF'
3๏ธโฃ ๋ฐฉํ๋ฒฝ ์ค์
sudo ufw allow 3478/tcp
sudo ufw allow 3478/udp
sudo ufw allow 49152:65535/udp
4๏ธโฃ ์๋น์ค ์์
sudo systemctl start coturn
sudo systemctl enable coturn
๐ป Spring Boot ์๊ทธ๋๋ง ์๋ฒ ๊ตฌํ
1๏ธโฃ ์์กด์ฑ ์ค์
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'com.fasterxml.jackson.core:jackson-databind'
}
2๏ธโฃ WebSocket ์ค์
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new WebRTCSignalingHandler(), "/signal")
.setAllowedOrigins("*");
}
}
3๏ธโฃ ์๊ทธ๋๋ง ๋ฉ์์ง DTO
@Data
public class SignalMessage {
private String type;
private String targetSessionId;
private Object data;
private String roomId;
}
4๏ธโฃ WebRTC ์๊ทธ๋๋ง ํธ๋ค๋ฌ
WebRTCSignalingHandler.java
@Slf4j
public class WebRTCSignalingHandler extends TextWebSocketHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
private final Map<String, String> roomSessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
log.info("New WebSocket connection established: {}", session.getId());
}
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
try {
log.info("Received message from client {}: {}", session.getId(), message.getPayload());
SignalMessage signalMessage = objectMapper.readValue(message.getPayload(), SignalMessage.class);
String roomId = signalMessage.getRoomId();
log.info("Processing {} message for room: {}", signalMessage.getType(), roomId);
switch (signalMessage.getType()) {
case "join":
handleJoinMessage(session, roomId);
break;
case "offer":
log.info("Received offer from: {}", session.getId());
broadcastToRoom(session, message, roomId);
break;
case "answer":
log.info("Received answer from: {}", session.getId());
broadcastToRoom(session, message, roomId);
break;
case "ice-candidate":
log.info("Received ICE candidate from: {}", session.getId());
broadcastToRoom(session, message, roomId);
break;
default:
log.warn("Unknown message type: {}", signalMessage.getType());
}
} catch (Exception e) {
log.error("Error handling message: ", e);
}
}
private void broadcastToRoom(WebSocketSession sender, TextMessage message, String roomId) {
sessions.forEach((sessionId, webSocketSession) -> {
if (!sender.getId().equals(sessionId) &&
roomId.equals(roomSessions.get(sessionId))) {
try {
log.info("Broadcasting message to session {} in room {}", sessionId, roomId);
webSocketSession.sendMessage(message);
} catch (IOException e) {
log.error("Error sending message to session {}: {}", sessionId, e.getMessage());
}
}
});
}
private void handleJoinMessage(WebSocketSession session, String roomId) {
sessions.put(session.getId(), session);
roomSessions.put(session.getId(), roomId);
log.info("Client {} joined room: {}", session.getId(), roomId);
// ๊ฐ์ ๋ฐฉ์ ์ฐธ๊ฐ์ ์ ๋ก๊น
long roomParticipants = roomSessions.values()
.stream()
.filter(room -> room.equals(roomId))
.count();
log.info("Room {} now has {} participants", roomId, roomParticipants);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
String roomId = roomSessions.get(session.getId());
sessions.remove(session.getId());
roomSessions.remove(session.getId());
log.info("Client {} disconnected from room {}", session.getId(), roomId);
// ๋จ์ ์ฐธ๊ฐ์ ์ ๋ก๊น
if (roomId != null) {
long remainingParticipants = roomSessions.values()
.stream()
.filter(room -> room.equals(roomId))
.count();
log.info("Room {} now has {} participants remaining", roomId, remainingParticipants);
}
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) {
log.error("Transport error for session {}: {}", session.getId(), exception.getMessage());
}
}
โ๏ธ React ํด๋ผ์ด์ธํธ ๊ตฌํ
1๏ธโฃ ํ๋ก์ ํธ ๊ตฌ์กฐ
src/
โโโ components/
โ โโโ WebRTCComponent.tsx
โ โโโ WebRTC.css
โโโ utils/
โ โโโ webrtc.ts
โโโ App.tsx
2๏ธโฃ WebRTC ์ ํธ๋ฆฌํฐ
// webrtc.ts
export const checkWebRTCSupport = () => {
const support = {
webRTC: false,
getUserMedia: false,
mediaDevices: false,
peerConnection: false,
websocket: false,
};
try {
support.webRTC = !!window.RTCPeerConnection;
support.getUserMedia = !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
support.mediaDevices = !!navigator.mediaDevices;
support.peerConnection = !!(window.RTCPeerConnection || window.RTCPeerConnection || window.RTCPeerConnection);
support.websocket = !!window.WebSocket;
} catch (e) {
console.error('Error checking WebRTC support:', e);
}
return support;
};
3๏ธโฃ WebRTC ์ปดํฌ๋ํธ
WebRTCComponent.tsx
// WebRTCComponent.tsx
import React, { useEffect, useRef, useState } from 'react';
import { checkWebRTCSupport } from '../utils/webrtc.ts';
import './WebRTC.css';
interface WebRTCComponentProps {
roomId: string;
}
const WebRTCComponent: React.FC<WebRTCComponentProps> = ({ roomId }) => {
const [localStream, setLocalStream] = useState<MediaStream | null>(null);
const [remoteStream, setRemoteStream] = useState<MediaStream | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [isCallStarted, setIsCallStarted] = useState(false);
const [initError, setInitError] = useState<string | null>(null);
const localVideoRef = useRef<HTMLVideoElement>(null);
const remoteVideoRef = useRef<HTMLVideoElement>(null);
const peerConnection = useRef<RTCPeerConnection | null>(null);
const websocket = useRef<WebSocket | null>(null);
const configuration: RTCConfiguration = {
iceServers: [
{
urls: 'turn:[YOUR_PUBLIC_IP]:3478',
username: 'turnuser',
credential: 'Turn2024!@#'
}
]
};
useEffect(() => {
const init = async () => {
try {
console.log('Checking WebRTC support...');
const support = checkWebRTCSupport();
if (!support.webRTC || !support.getUserMedia) {
throw new Error('Your browser does not support required WebRTC features');
}
console.log('Initializing WebRTC...');
// WebSocket ์ฐ๊ฒฐ
websocket.current = new WebSocket('ws://[YOUR_WEBSOCKET_SERVER_URL]/signal');
websocket.current.onopen = () => {
console.log('WebSocket connected');
sendSignalingMessage({
type: 'join',
roomId
});
};
// ๋ฏธ๋์ด ์คํธ๋ฆผ ๊ฐ์ ธ์ค๊ธฐ
let stream: MediaStream;
try {
stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 }
},
audio: true
});
console.log('Local media stream obtained');
} catch (mediaError) {
console.error('Media access error:', mediaError);
throw new Error('Unable to access camera and microphone');
}
setLocalStream(stream);
if (localVideoRef.current) {
localVideoRef.current.srcObject = stream;
}
// WebRTC Peer Connection ์ด๊ธฐํ
peerConnection.current = new RTCPeerConnection(configuration);
console.log('PeerConnection created with config:', configuration);
// ๋ก์ปฌ ์คํธ๋ฆผ ์ถ๊ฐ
stream.getTracks().forEach(track => {
if (peerConnection.current) {
console.log('Adding track to peer connection:', track.kind);
peerConnection.current.addTrack(track, stream);
}
});
// ์๊ฒฉ ์คํธ๋ฆผ ์ฒ๋ฆฌ
peerConnection.current.ontrack = (event) => {
console.log('Received remote track:', event.track.kind);
if (remoteVideoRef.current && event.streams[0]) {
console.log('Setting remote stream');
remoteVideoRef.current.srcObject = event.streams[0];
setRemoteStream(event.streams[0]);
}
};
// ICE ํ๋ณด ์ฒ๋ฆฌ
peerConnection.current.onicecandidate = (event) => {
if (event.candidate) {
console.log('Sending ICE candidate');
sendSignalingMessage({
type: 'ice-candidate',
data: event.candidate,
roomId
});
}
};
// ์ฐ๊ฒฐ ์ํ ๋ชจ๋ํฐ๋ง
peerConnection.current.onconnectionstatechange = () => {
console.log('Connection state changed:', peerConnection.current?.connectionState);
setIsConnected(peerConnection.current?.connectionState === 'connected');
};
peerConnection.current.oniceconnectionstatechange = () => {
console.log('ICE connection state:', peerConnection.current?.iceConnectionState);
};
// WebSocket ๋ฉ์์ง ์ฒ๋ฆฌ
websocket.current.onmessage = async (event) => {
try {
const message = JSON.parse(event.data);
console.log('Received message:', message.type);
switch (message.type) {
case 'offer':
await handleOffer(message.data);
break;
case 'answer':
await handleAnswer(message.data);
break;
case 'ice-candidate':
await handleIceCandidate(message.data);
break;
default:
console.log('Unknown message type:', message.type);
}
} catch (error) {
console.error('Error handling WebSocket message:', error);
}
};
websocket.current.onclose = () => {
console.log('WebSocket disconnected');
setIsConnected(false);
};
websocket.current.onerror = (error) => {
console.error('WebSocket error:', error);
setInitError('WebSocket connection failed');
};
} catch (error) {
console.error('Error initializing WebRTC:', error);
setInitError(error.message);
}
};
init();
// Cleanup
return () => {
console.log('Cleaning up...');
localStream?.getTracks().forEach(track => {
console.log('Stopping track:', track.kind);
track.stop();
});
if (peerConnection.current) {
console.log('Closing peer connection');
peerConnection.current.close();
}
if (websocket.current) {
console.log('Closing WebSocket');
websocket.current.close();
}
};
}, [roomId]);
const sendSignalingMessage = (message: any) => {
if (websocket.current?.readyState === WebSocket.OPEN) {
console.log('Sending message:', message.type);
websocket.current.send(JSON.stringify(message));
} else {
console.warn('WebSocket is not open');
}
};
const handleOffer = async (offer: RTCSessionDescriptionInit) => {
try {
if (peerConnection.current) {
console.log('Setting remote description (offer)');
await peerConnection.current.setRemoteDescription(new RTCSessionDescription(offer));
console.log('Creating answer');
const answer = await peerConnection.current.createAnswer();
console.log('Setting local description (answer)');
await peerConnection.current.setLocalDescription(answer);
sendSignalingMessage({
type: 'answer',
data: answer,
roomId
});
}
} catch (error) {
console.error('Error handling offer:', error);
}
};
const handleAnswer = async (answer: RTCSessionDescriptionInit) => {
try {
if (peerConnection.current) {
console.log('Setting remote description (answer)');
await peerConnection.current.setRemoteDescription(new RTCSessionDescription(answer));
}
} catch (error) {
console.error('Error handling answer:', error);
}
};
const handleIceCandidate = async (candidate: RTCIceCandidateInit) => {
try {
if (peerConnection.current) {
console.log('Adding ICE candidate');
await peerConnection.current.addIceCandidate(new RTCIceCandidate(candidate));
}
} catch (error) {
console.error('Error handling ICE candidate:', error);
}
};
const startCall = async () => {
try {
if (peerConnection.current) {
console.log('Creating offer');
const offer = await peerConnection.current.createOffer();
console.log('Setting local description (offer)');
await peerConnection.current.setLocalDescription(offer);
sendSignalingMessage({
type: 'offer',
data: offer,
roomId
});
setIsCallStarted(true);
}
} catch (error) {
console.error('Error starting call:', error);
}
};
const endCall = () => {
try {
localStream?.getTracks().forEach(track => track.stop());
peerConnection.current?.close();
setIsCallStarted(false);
setIsConnected(false);
setRemoteStream(null);
console.log('Call ended');
} catch (error) {
console.error('Error ending call:', error);
}
};
if (initError) {
return (
<div className="webrtc-error">
<h2>WebRTC Error</h2>
<p>{initError}</p>
<p>Please use a modern browser with camera and microphone support.</p>
<p>Supported browsers:</p>
<ul>
<li>Google Chrome (recommended)</li>
<li>Mozilla Firefox</li>
<li>Microsoft Edge</li>
<li>Safari 11+</li>
</ul>
</div>
);
}
return (
<div className="webrtc-container">
<div className="video-grid">
<div className="video-wrapper">
<video
ref={localVideoRef}
autoPlay
playsInline
muted
className="video-stream"
/>
<div className="video-label">Local Stream</div>
</div>
<div className="video-wrapper">
<video
ref={remoteVideoRef}
autoPlay
playsInline
className="video-stream"
/>
<div className="video-label">Remote Stream</div>
</div>
</div>
<div className="controls">
{!isCallStarted ? (
<button
onClick={startCall}
className="control-button start-call"
disabled={!localStream}
>
Start Call
</button>
) : (
<button
onClick={endCall}
className="control-button end-call"
>
End Call
</button>
)}
</div>
<div className="connection-status">
Status: {isConnected ? 'Connected' : 'Disconnected'}
</div>
</div>
);
};
export default WebRTCComponent;
4๏ธโฃ ์คํ์ผ๋ง
WebRTC.css
/* WebRTC.css */
.webrtc-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.video-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
width: 100%;
}
.video-wrapper {
position: relative;
background: #000;
border-radius: 8px;
overflow: hidden;
aspect-ratio: 16/9;
}
.video-stream {
width: 100%;
height: 100%;
object-fit: cover;
background: #2c3e50;
}
.video-label {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.5);
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 14px;
}
.controls {
display: flex;
gap: 10px;
margin-top: 20px;
}
.control-button {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s ease;
}
.start-call {
background-color: #2ecc71;
color: white;
}
.start-call:hover {
background-color: #27ae60;
}
.start-call:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
.end-call {
background-color: #e74c3c;
color: white;
}
.end-call:hover {
background-color: #c0392b;
}
.connection-status {
margin-top: 10px;
padding: 5px 10px;
border-radius: 4px;
background: #f0f0f0;
font-size: 14px;
}
.webrtc-error {
max-width: 600px;
margin: 2rem auto;
padding: 2rem;
background-color: #fff3f3;
border: 1px solid #ffcdd2;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.webrtc-error h2 {
color: #d32f2f;
margin-bottom: 1rem;
}
.webrtc-error ul {
list-style-type: none;
padding-left: 0;
}
.webrtc-error li {
padding: 0.5rem 0;
color: #666;
}
.webrtc-error p {
margin: 0.5rem 0;
color: #333;
}
๐งช ํ ์คํธ ๋ฐ ๋ฌธ์ ํด๊ฒฐ
โ ๊ธฐ๋ณธ ๋์ ํ ์คํธ
- TURN ์๋ฒ ์ํ ํ์ธ
sudo systemctl status coturn
- ์๊ทธ๋๋ง ์๋ฒ ๋ก๊ทธ ํ์ธ
tail -f /var/log/spring-boot.log
- ๋ธ๋ผ์ฐ์ ์ฝ์์์ ํ์ธํ ์ฌํญ
- WebSocket ์ฐ๊ฒฐ ์ํ
- ICE Candidate ์์ง ์ํ
- Peer Connection ์ํ
๐ ๋ฌธ์ ํด๊ฒฐ
WebRTC ๋ฏธ์ง์ ๋ธ๋ผ์ฐ์
if (!support.webRTC || !support.getUserMedia) {
throw new Error('Browser not supported');
}
TURN ์๋ฒ ์ฐ๊ฒฐ ์คํจ
- ๋ฐฉํ๋ฒฝ ์ค์ ํ์ธ
- TURN ์๋ฒ ๋ก๊ทธ ํ์ธ
- ICE ์๋ฒ ์ค์ ํ์ธ
- ๐ WebRTC์ ๋ณด์: HTTPS ํ์์ฑ
โ ๏ธ HTTP์์ WebRTC ์ฌ์ฉ ์ ์ ํ์ฌํญ
ํ๋ ๋ธ๋ผ์ฐ์ ๋ค์ ๋ณด์์ ์ํด ๋ฏผ๊ฐํ ๊ธฐ๋ฅ๋ค์ HTTPS ํ๊ฒฝ์์๋ง ์ฌ์ฉํ ์ ์๋๋ก ์ ํํ๊ณ ์์ต๋๋ค. ๊ทธ ์ค ํ๋๊ฐ getUserMedia()
API์
๋๋ค.
๐ซ HTTP ํ๊ฒฝ์์ ๋ฐ์ํ๋ ์ค๋ฅ
// HTTP ํ๊ฒฝ์์ ์คํ ์
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
// ์ด ์ฝ๋๋ ์คํ๋์ง ์์ต๋๋ค
})
.catch(error => {
// NotAllowedError: Permission denied
// ๋๋
// SecurityError: The operation is insecure
});
๐ ๏ธ ํด๊ฒฐ ๋ฐฉ๋ฒ
1๏ธโฃ ๊ฐ๋ฐ ํ๊ฒฝ์์์ ์์ ํด๊ฒฐ์ฑ
localhost ์์ธ ์ฒ๋ฆฌ
localhost
๋๋127.0.0.1
์์๋ HTTP๋ก๋ ์นด๋ฉ๋ผ ์ ๊ทผ์ด ๊ฐ๋ฅํฉ๋๋ค.// ๊ฐ๋ฐ ์๋ฒ ์คํ npm start // ๊ธฐ๋ณธ์ ์ผ๋ก http://localhost:3000 ์ผ๋ก ์คํ
2๏ธโฃ ํ๋ก๋์ ํ๊ฒฝ ํด๊ฒฐ์ฑ
A. SSL ์ธ์ฆ์ ์ค์น
# Let's Encrypt ์ค์น
sudo apt-get install certbot
# SSL ์ธ์ฆ์ ๋ฐ๊ธ
sudo certbot certonly --standalone -d your-domain.com
# Nginx ์ค์ ์์
sudo nano /etc/nginx/sites-available/default
server {
listen 443 ssl;
server_name your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
}
location /signal {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
}
}
B. Spring Boot SSL ์ค์
# application.yml
server:
ssl:
key-store: classpath:keystore.p12
key-store-password: your-password
key-store-type: PKCS12
key-alias: your-alias
port: 8443
๐ ๋ณด์ ๊ด๋ จ ์ฒดํฌ๋ฆฌ์คํธ
โ ํ์ ํ์ธ ์ฌํญ
- HTTPS ํ๋กํ ์ฝ ์ฌ์ฉ ์ฌ๋ถ
- SSL ์ธ์ฆ์ ์ ํจ์ฑ
- WebSocket ๋ณด์ ์ฐ๊ฒฐ (WSS)
- TURN ์๋ฒ ์ธ์ฆ ์ค์
๐ฆ ํ๊ฒฝ๋ณ ๊ตฌ์ฑ ๋ฐฉ๋ฒ
1๏ธโฃ ๊ฐ๋ฐ ํ๊ฒฝ
// development ํ๊ฒฝ ์ค์
const wsUrl = process.env.NODE_ENV === 'development'
? 'ws://localhost:8080/signal'
: 'wss://your-domain.com/signal';
2๏ธโฃ ํ๋ก๋์ ํ๊ฒฝ
// WebRTCComponent.tsx
const websocket = new WebSocket('wss://your-domain.com/signal');
๐ก ๋ฌธ์ ํด๊ฒฐ ๊ฐ์ด๋
๐ ์ผ๋ฐ์ ์ธ ์๋ฌ ๋ฉ์์ง์ ํด๊ฒฐ๋ฐฉ๋ฒ
NotAllowedError: Permission denied
์์ธ: HTTP ํ๊ฒฝ์์ getUserMedia ํธ์ถ ํด๊ฒฐ: HTTPS ํ๋กํ ์ฝ ์ฌ์ฉ
SecurityError: The operation is insecure
์์ธ: ๋ณด์๋์ง ์์ ์ปจํ ์คํธ ํด๊ฒฐ: SSL ์ธ์ฆ์ ์ค์น ๋ฐ HTTPS ํ์ฑํ
๐ ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ ๋๋ฒ๊น
const initWebRTC = async () => {
try {
// ํ๋กํ ์ฝ ์ฒดํฌ
if (window.location.protocol !== 'https:' && window.location.hostname !== 'localhost') {
throw new Error('HTTPS is required for camera access');
}
// getUserMedia ํธ์ถ
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
// ์ฑ๊ณต์ ์ผ๋ก ์คํธ๋ฆผ ํ๋
console.log('Camera access granted');
} catch (error) {
if (error.name === 'NotAllowedError') {
console.error('Camera access denied by user or HTTP protocol');
} else if (error.name === 'SecurityError') {
console.error('Security error: HTTPS required');
}
// ์๋ฌ ์ฒ๋ฆฌ
handleError(error);
}
};
๐ ์ ๋ฆฌ
โจ ํต์ฌ ํฌ์ธํธ
- WebRTC์
getUserMedia()
๋ ๋ณด์์์ ์ด์ ๋ก HTTPS๊ฐ ํ์ - ๊ฐ๋ฐ ์์๋ localhost ์์ธ ํ์ฉ
- ํ๋ก๋์ ๋ฐฐํฌ ์์๋ ๋ฐ๋์ SSL/TLS ์ธ์ฆ์ ์ค์น
๐ฏ ๊ถ์ฅ ์ฌํญ
- ๊ฐ๋ฐ ์ด๊ธฐ๋ถํฐ HTTPS ํ๊ฒฝ ๊ตฌ์ฑ
- SSL ์ธ์ฆ์๋ ์ ๋ขฐํ ์ ์๋ ์ธ์ฆ ๊ธฐ๊ด์์ ๋ฐ๊ธ
- ๋ชจ๋ ์ฐ๊ฒฐ(WebSocket, TURN ์๋ฒ ๋ฑ)์ ๋ณด์ ํ๋กํ ์ฝ ์ ์ฉ
โ ๏ธ ์ฃผ์ ์ฌํญ
- ์์ฒด ์๋ช ์ธ์ฆ์๋ ๊ฐ๋ฐ ์ฉ๋๋ก๋ง ์ฌ์ฉ
- ํ๋ก๋์ ํ๊ฒฝ์์๋ ๋ฐ๋์ ๊ณต์ธ ์ธ์ฆ์ ์ฌ์ฉ
- ๋ชจ๋ ์ธ๋ถ ์ฐ๊ฒฐ์ ๋ํด ๋ณด์ ํ๋กํ ์ฝ ์ ์ฉ ํ์
์ด๋ฌํ ๋ณด์ ์๊ตฌ์ฌํญ๋ค์ ์ฌ์ฉ์์ ๊ฐ์ธ์ ๋ณด๋ฅผ ๋ณดํธํ๊ณ , ์์ ํ WebRTC ์๋น์ค๋ฅผ ์ ๊ณตํ๊ธฐ ์ํด ๊ผญ ํ์ํ ์์๋ค์ ๋๋ค. ๐ฎโ๏ธ
๐ ์ฐธ๊ณ ์๋ฃ
๐ ์ฃผ์ ํฌ์ธํธ
- WebRTC๋ P2P ์ฐ๊ฒฐ์ด ๊ธฐ๋ณธ์ด์ง๋ง, NAT ํต๊ณผ๋ฅผ ์ํด TURN ์๋ฒ๊ฐ ํ์
- ์๊ทธ๋๋ง ์๋ฒ๋ ์ด๊ธฐ ์ฐ๊ฒฐ ์ค์ ์๋ง ์ฌ์ฉ
- ๋ฏธ๋์ด ์คํธ๋ฆผ์ ์ต๋ํ P2P๋ก ์ ์ก๋๋๋ก ๊ตฌํ
- ๋ธ๋ผ์ฐ์ ํธํ์ฑ ์ฒดํฌ๋ ํ์
์ด ๊ธ์ด WebRTC๋ฅผ ์ด์ฉํ ํ์ ์ฑํ ๊ตฌํ์ ๋์์ด ๋๊ธธ ๋ฐ๋๋๋ค! ๐