-
Notifications
You must be signed in to change notification settings - Fork 7
๐ชต 2. WebRTC ์ค์ต (2) : 1:N Mesh ๋ฐฉ์
ssum1ra edited this page Dec 5, 2024
·
1 revision
@WebSocketGateway({ cors: { origin: '*' } })
export class WebRTCGateway implements OnGatewayDisconnect {
@WebSocketServer()
server: Server;
-
WebSocket ์๋ฒ ๊ตฌ์ฑ:
@WebSocketGateway
๋ฐ์ฝ๋ ์ดํฐ๋ก ์ค์ ์ ์ถ๊ฐํ์ฌ ์๋ฒ๋ฅผ ์ค์ ํ๊ณ ,@WebSocketServer()
๋ก ์๋ฒ ์ธ์คํด์ค๋ฅผ ๋ง๋ค์ด WebSocket ํต์ ์ ๊ด๋ฆฌํ๋ค. - CORS ์ค์ : ๋ค์ํ ๋๋ฉ์ธ์์ ์๋ฒ์ ์ ๊ทผํ ์ ์๋๋ก ํ์ฉํ์ฌ ์ฌ๋ฌ ํด๋ผ์ด์ธํธ๊ฐ ์ด ์๋ฒ์ ์ ์ํ ์ ์๊ฒ ํ๋ค.
-
ํด๋ผ์ด์ธํธ ์ฐ๊ฒฐ ๊ด๋ฆฌ:
OnGatewayDisconnect
๋ฅผ ๊ตฌํํ์ฌ ํด๋ผ์ด์ธํธ๊ฐ ์ฐ๊ฒฐ์ ์ข ๋ฃํ ๋ ๋ฐ์ํ๋ ์์ ์ ์ ์ํ๊ณ , ์๋ฒ ์ธ์คํด์ค(server
)๋ฅผ ํตํด ์ฐ๊ฒฐ๋ ํด๋ผ์ด์ธํธ๋ค์๊ฒ ์ด๋ฒคํธ๋ฅผ ์ ์กํ ์ ์๋๋ก ์ค๋นํ๋ค.
handleConnection(client: Socket) {
console.log(`Client connected: ${client.id}`);
}
handleDisconnect(client: Socket) {
console.log(`Client disconnected: ${client.id}`);
// ํด๋ผ์ด์ธํธ๊ฐ ์ํ ๋ชจ๋ ๋ฐฉ์ ์ฐ๊ฒฐ ์ข
๋ฃ ์๋ฆผ
this.rooms.forEach((clients, roomId) => {
if (clients.has(client.id)) {
clients.delete(client.id);
client.to(roomId).emit('user-left', client.id);
console.log(`Notified room ${roomId} about user ${client.id} leaving`);
}
});
}
-
handleConnection
- ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์ ์ฐ๊ฒฐ๋๋ฉด ID๋ฅผ ์ฝ์์ ํ์ํ๋ค.
-
handleDisconnect
- ํด๋ผ์ด์ธํธ๊ฐ ์ฐ๊ฒฐ์ ๋์ผ๋ฉด, ํด๋ผ์ด์ธํธ๊ฐ ์ํ ๋ฐฉ์ ์ฐพ์ ์ ๊ฑฐํ๊ณ , ๋ฐฉ์ ๋ค๋ฅธ ์ฌ์ฉ์์๊ฒ ํด๋ผ์ด์ธํธ๊ฐ ๋ ๋ฌ๋ค๋ ์ฌ์ค์ ์๋ฆฐ๋ค.
-
user-left
์ด๋ฒคํธ๋ฅผ ํตํด ๋ฐฉ์ ๋ค๋ฅธ ์ฌ์ฉ์๋ค์๊ฒ ์ฐ๊ฒฐ ์ข ๋ฃ๋ฅผ ํต์งํ๋ค.
@SubscribeMessage('joinRoom')
async handleJoinRoom(
@MessageBody() roomId: string,
@ConnectedSocket() client: Socket,
) {
console.log(`Client ${client.id} joining room: ${roomId}`);
// ๋ฐฉ ์ฐธ์ฌ
await client.join(roomId);
// ๋ฐฉ ์ฌ์ฉ์ ๋ชฉ๋ก ๊ด๋ฆฌ
if (!this.rooms.has(roomId)) {
this.rooms.set(roomId, new Set());
}
const roomClients = this.rooms.get(roomId);
// ํ์ฌ ๋ฐฉ์ ์๋ ๋ชจ๋ ์ฌ์ฉ์ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
const currentUsers = Array.from(roomClients);
// ์ ๊ท ์ฌ์ฉ์์๊ฒ ํ์ฌ ๋ฐฉ ์ฌ์ฉ์ ๋ชฉ๋ก ์ ์ก
client.emit('room-users', currentUsers);
console.log(`Sent current users to ${client.id}:`, currentUsers);
// ๋ฐฉ์ ์ ์ฌ์ฉ์ ์ถ๊ฐ
roomClients.add(client.id);
// ๋ฐฉ์ ๋ค๋ฅธ ์ฌ์ฉ์๋ค์๊ฒ ์ ์ฌ์ฉ์ ์๋ฆผ
client.to(roomId).emit('user-joined', client.id);
console.log(`Notified room about new user ${client.id}`);
// ์ฐธ์ฌ ์๋ฃ ์๋ฆผ
client.emit('joinedRoom', roomId);
}
-
๋ฐฉ ์ฐธ์ฌ ์์ฒญ: ํด๋ผ์ด์ธํธ๊ฐ
joinRoom
์์ฒญ์ ๋ณด๋์ผ๋ก์จ ํน์ ๋ฐฉ์ ์ฐธ์ฌํ ์ ์๋ค. - ํ์ฌ ์ฌ์ฉ์ ๋ชฉ๋ก ์ ์ก: ์๋ก์ด ์ฌ์ฉ์์๊ฒ ํ์ฌ ๋ฐฉ์ ์๋ ๋ค๋ฅธ ์ฌ์ฉ์ ๋ชฉ๋ก์ ์ ์กํ์ฌ, ๋ฐฉ์ ๋๊ฐ ์๋์ง ์ ์ ์๊ฒ ํ๋ค.
- ๋ฐฉ์ ์ฌ์ฉ์ ์ถ๊ฐ ๋ฐ ์๋ฆผ: ๋ฐฉ ๋ชฉ๋ก์ ์ ์ฌ์ฉ์๋ฅผ ์ถ๊ฐํ๊ณ , ๋ค๋ฅธ ์ฌ์ฉ์๋ค์๊ฒ ์ ์ฌ์ฉ์๊ฐ ์ฐธ์ฌํ์์ ์๋ฆฐ๋ค.
-
์ฐธ์ฌ ์๋ฃ ์๋ฆผ: ์ ์ฌ์ฉ์๊ฐ ๋ฐฉ์ ์ ์์ ์ผ๋ก ์ฐธ์ฌํ์์ ์๋ฆฌ๋
joinedRoom
์ด๋ฒคํธ๋ฅผ ๋ณด๋ธ๋ค.
@SubscribeMessage('offer')
handleOffer(
@MessageBody()
data: {
roomId: string;
userId: string;
offer: RTCSessionDescriptionInit;
},
@ConnectedSocket() client: Socket,
) {
console.log(`Received offer from ${client.id} to ${data.userId}`);
// userId๋ก ํน์ ์ฌ์ฉ์์๊ฒ๋ง offer ์ ์ก
this.server.to(data.userId).emit('offer', {
offer: data.offer,
senderId: client.id,
});
}
-
offer ์ด๋ฒคํธ ์ฒ๋ฆฌ:
offer
์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ฉด ํน์ ์ฌ์ฉ์์๊ฒ WebRTC offer๋ฅผ ์ ์กํ๋ค. -
์ ์ก ๊ตฌ์กฐ
-
senderId
๋ฅผ ํฌํจํ์ฌ offer๋ฅผ ๋ณด๋ธ ์ฌ์ฉ์๊ฐ ๋๊ตฌ์ธ์ง ์ ์ ์๋๋ก ํ๋ค. - ๋์ ์ฌ์ฉ์๋
offer
๋ฅผ ์์ ํ์ฌ WebRTC ์ฐ๊ฒฐ์ ์ํanswer
๋ฅผ ์์ฑํ ์ ์๋ค.
-
-
ํน์ ์ฌ์ฉ์์๊ฒ๋ง ์ ๋ฌ:
userId
๋ก ์ง์ ๋ ์ฌ์ฉ์์๊ฒ๋ง offer๊ฐ ์ ์ก๋๋ฏ๋ก, ๋ค๋ฅธ ์ฌ์ฉ์๋ ์ด ์ด๋ฒคํธ๋ฅผ ์์ ํ์ง ์๋๋ค.
@SubscribeMessage('answer')
handleAnswer(
@MessageBody()
data: {
roomId: string;
userId: string;
answer: RTCSessionDescriptionInit;
},
@ConnectedSocket() client: Socket,
) {
console.log(`Received answer from ${client.id} to ${data.userId}`);
// userId๋ก ํน์ ์ฌ์ฉ์์๊ฒ๋ง answer ์ ์ก
this.server.to(data.userId).emit('answer', {
answer: data.answer,
senderId: client.id,
});
}
-
answer ์ด๋ฒคํธ ์ฒ๋ฆฌ:
answer
์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ฉด ํน์ ์ฌ์ฉ์์๊ฒ WebRTC answer๋ฅผ ์ ์กํ๋ค. -
์ ์ก ๊ตฌ์กฐ
-
senderId
๋ฅผ ํฌํจํ์ฌ answer๋ฅผ ๋ณด๋ธ ์ฌ์ฉ์๊ฐ ๋๊ตฌ์ธ์ง ์ ์ ์๋๋ก ํ๋ค. - ๋์ ์ฌ์ฉ์๋
answer
๋ฅผ ์์ ํ์ฌ WebRTC ์ฐ๊ฒฐ์ ํ๋ฆฝํ ์ ์๋ค.
-
-
ํน์ ์ฌ์ฉ์์๊ฒ๋ง ์ ๋ฌ:
userId
๋ก ์ง์ ๋ ์ฌ์ฉ์์๊ฒ๋ง answer๊ฐ ์ ์ก๋๋ฏ๋ก, ๋ค๋ฅธ ์ฌ์ฉ์๋ ์ด ์ด๋ฒคํธ๋ฅผ ์์ ํ์ง ์๋๋ค.
@SubscribeMessage('ice-candidate')
handleIceCandidate(
@MessageBody()
data: {
roomId: string;
userId: string;
candidate: RTCIceCandidateInit;
},
@ConnectedSocket() client: Socket,
) {
console.log(`Received ICE candidate from ${client.id} to ${data.userId}`);
// userId๋ก ํน์ ์ฌ์ฉ์์๊ฒ๋ง candidate ์ ์ก
this.server.to(data.userId).emit('ice-candidate', {
candidate: data.candidate,
senderId: client.id,
});
}
- ICE ํ๋ณด ์ฒ๋ฆฌ: ํด๋ผ์ด์ธํธ๊ฐ ์๋ก์ด ICE ํ๋ณด๋ฅผ ๋ฐ๊ฒฌํ ๋๋ง๋ค ์๋๋ฐฉ์๊ฒ ์ด ํ๋ณด ์ ๋ณด๋ฅผ ์ ์กํ์ฌ ์ต์ ์ ๋คํธ์ํฌ ๊ฒฝ๋ก๋ฅผ ์ฐพ๋๋ค.
-
์ ์ก ๊ตฌ์กฐ
-
senderId
๋ฅผ ํฌํจํ์ฌ ICE ํ๋ณด๋ฅผ ๋ณด๋ธ ์ฌ์ฉ์๊ฐ ๋๊ตฌ์ธ์ง ์ ์ ์๋๋ก ํ๋ค. - ๋์ ์ฌ์ฉ์๋ ์ด ICE ํ๋ณด๋ฅผ ์ฌ์ฉํ์ฌ ์ฐ๊ฒฐ์ ์ค์ ํ๋ค.
-
-
ํน์ ์ฌ์ฉ์์๊ฒ๋ง ์ ๋ฌ:
userId
๋ก ์ง์ ๋ ์ฌ์ฉ์์๊ฒ๋ง ICE ํ๋ณด๊ฐ ์ ์ก๋๋ฏ๋ก, ๋ค๋ฅธ ์ฌ์ฉ์๋ ์ด ์ด๋ฒคํธ๋ฅผ ์์ ํ์ง ์๋๋ค.
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
MessageBody,
ConnectedSocket,
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({ cors: { origin: '*' } })
export class WebRTCGateway implements OnGatewayDisconnect {
@WebSocketServer()
server: Server;
private rooms = new Map<string, Set<string>>();
handleConnection(client: Socket) {
console.log(`Client connected: ${client.id}`);
}
handleDisconnect(client: Socket) {
console.log(`Client disconnected: ${client.id}`);
// ํด๋ผ์ด์ธํธ๊ฐ ์ํ ๋ชจ๋ ๋ฐฉ์ ์ฐ๊ฒฐ ์ข
๋ฃ ์๋ฆผ
this.rooms.forEach((clients, roomId) => {
if (clients.has(client.id)) {
clients.delete(client.id);
client.to(roomId).emit('user-left', client.id);
console.log(`Notified room ${roomId} about user ${client.id} leaving`);
}
});
}
// ๋ฐฉ์ ์ฌ์ฉ์ ์ถ๊ฐ ๋ฐ ํ์ฌ ์ฌ์ฉ์ ๋ชฉ๋ก ์ ์ก
@SubscribeMessage('joinRoom')
async handleJoinRoom(
@MessageBody() roomId: string,
@ConnectedSocket() client: Socket,
) {
console.log(`Client ${client.id} joining room: ${roomId}`);
// ๋ฐฉ ์ฐธ์ฌ
await client.join(roomId);
// ๋ฐฉ ์ฌ์ฉ์ ๋ชฉ๋ก ๊ด๋ฆฌ
if (!this.rooms.has(roomId)) {
this.rooms.set(roomId, new Set());
}
const roomClients = this.rooms.get(roomId);
// ํ์ฌ ๋ฐฉ์ ์๋ ๋ชจ๋ ์ฌ์ฉ์ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
const currentUsers = Array.from(roomClients);
// ์ ๊ท ์ฌ์ฉ์์๊ฒ ํ์ฌ ๋ฐฉ ์ฌ์ฉ์ ๋ชฉ๋ก ์ ์ก
client.emit('room-users', currentUsers);
console.log(`Sent current users to ${client.id}:`, currentUsers);
// ๋ฐฉ์ ์ ์ฌ์ฉ์ ์ถ๊ฐ
roomClients.add(client.id);
// ๋ฐฉ์ ๋ค๋ฅธ ์ฌ์ฉ์๋ค์๊ฒ ์ ์ฌ์ฉ์ ์๋ฆผ
client.to(roomId).emit('user-joined', client.id);
console.log(`Notified room about new user ${client.id}`);
// ์ฐธ์ฌ ์๋ฃ ์๋ฆผ
client.emit('joinedRoom', roomId);
}
@SubscribeMessage('offer')
handleOffer(
@MessageBody()
data: {
roomId: string;
userId: string;
offer: RTCSessionDescriptionInit;
},
@ConnectedSocket() client: Socket,
) {
console.log(`Received offer from ${client.id} to ${data.userId}`);
// userId๋ก ํน์ ์ฌ์ฉ์์๊ฒ๋ง offer ์ ์ก
this.server.to(data.userId).emit('offer', {
offer: data.offer,
senderId: client.id,
});
}
@SubscribeMessage('answer')
handleAnswer(
@MessageBody()
data: {
roomId: string;
userId: string;
answer: RTCSessionDescriptionInit;
},
@ConnectedSocket() client: Socket,
) {
console.log(`Received answer from ${client.id} to ${data.userId}`);
// userId๋ก ํน์ ์ฌ์ฉ์์๊ฒ๋ง answer ์ ์ก
this.server.to(data.userId).emit('answer', {
answer: data.answer,
senderId: client.id,
});
}
@SubscribeMessage('ice-candidate')
handleIceCandidate(
@MessageBody()
data: {
roomId: string;
userId: string;
candidate: RTCIceCandidateInit;
},
@ConnectedSocket() client: Socket,
) {
console.log(`Received ICE candidate from ${client.id} to ${data.userId}`);
// userId๋ก ํน์ ์ฌ์ฉ์์๊ฒ๋ง candidate ์ ์ก
this.server.to(data.userId).emit('ice-candidate', {
candidate: data.candidate,
senderId: client.id,
});
}
}
const canvasRef = useRef<HTMLCanvasElement>(null);
const peerConnections = useRef<Map<string, RTCPeerConnection>>(new Map());
const dataChannels = useRef<Map<string, RTCDataChannel>>(new Map());
const pendingCandidates = useRef<Map<string, RTCIceCandidateInit[]>>(new Map());
const socketRef = useRef<SocketType>();
const [isDrawing, setIsDrawing] = useState(false);
const myIdRef = useRef<string>('');
-
canvasRef
: ์บ๋ฒ์ค ์์์ ์ ๊ทผํ์ฌ ๊ทธ๋ฆผ์ ๊ทธ๋ฆฌ๊ธฐ ์ํ ์ฐธ์กฐ -
peerConnections
๋ฐdataChannels
: ๊ฐ ์ฌ์ฉ์์์ WebRTC ์ฐ๊ฒฐ ๋ฐ ๋ฐ์ดํฐ ์ฑ๋์ ๊ด๋ฆฌํ์ฌ ์ค์๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์ -
pendingCandidates
: WebRTC ์ฐ๊ฒฐ ์ค์ ์ ๋ฐ์ ICE ํ๋ณด๋ฅผ ์์ ์ ์ฅ -
socketRef
: ์๋ฒ์์ WebSocket ์ฐ๊ฒฐ ๊ด๋ฆฌ -
isDrawing
: ์ฌ์ฉ์๊ฐ ๊ทธ๋ฆผ์ ๊ทธ๋ฆฌ๋ ์ํ๋ฅผ ์ถ์ -
myIdRef
: ํ์ฌ ํด๋ผ์ด์ธํธ์ ๊ณ ์ ID๋ฅผ ์ ์ฅ
useEffect(() => {
socketRef.current = io(SOCKET_SERVER);
socketRef.current.on('connect', () => {
console.log('Connected to server');
myIdRef.current = socketRef.current?.id || '';
socketRef.current?.emit('joinRoom', 'canvas-room');
});
// ๋ฐฉ์ ์ฐธ์ฌํ์ ๋ ํ์ฌ ๋ฐฉ์ ์๋ ๋ค๋ฅธ ์ ์ ๋ค์ ๋ชฉ๋ก์ ๋ฐ์
socketRef.current.on('room-users', (users: string[]) => {
console.log('Current users in room:', users);
// ์์ ์ ์ ์ธํ ๊ฐ ์ ์ ์ ์ฐ๊ฒฐ ์ค์
users.forEach(userId => {
if (userId !== myIdRef.current && !peerConnections.current.has(userId)) {
createPeerConnection(userId, true);
}
});
});
// ์๋ก์ด ์ ์ ๊ฐ ๋ค์ด์์ ๋
socketRef.current.on('user-joined', (userId: string) => {
console.log('New user joined:', userId);
if (userId !== myIdRef.current && !peerConnections.current.has(userId)) {
createPeerConnection(userId, false);
}
});
socketRef.current.on('offer', async ({ offer, senderId }) => {
console.log('Received offer from:', senderId);
await handleOffer(offer, senderId);
});
socketRef.current.on('answer', async ({ answer, senderId }) => {
console.log('Received answer from:', senderId);
await handleAnswer(answer, senderId);
});
socketRef.current.on('ice-candidate', async ({ candidate, senderId }) => {
await handleIceCandidate(candidate, senderId);
});
socketRef.current.on('user-left', (userId: string) => {
console.log('User left:', userId);
cleanupPeerConnection(userId);
});
return () => {
peerConnections.current.forEach((_, userId) => {
cleanupPeerConnection(userId);
});
socketRef.current?.disconnect();
};
}, []);
-
์๋ฒ ์ฐ๊ฒฐ: ํด๋ผ์ด์ธํธ๊ฐ Socket.io ์๋ฒ์ ์ฐ๊ฒฐํ๊ณ , ์๋ฒ๋ก๋ถํฐ ํ ๋น๋ฐ์ ๊ณ ์ ID๋ฅผ
myIdRef
์ ์ ์ฅํ๋ค. -
๋ฐฉ ์ฐธ์ฌ ์์ฒญ: ํด๋ผ์ด์ธํธ๊ฐ
canvas-room
์ด๋ผ๋ ๋ฐฉ์ ์ฐธ์ฌํ๊ธฐ ์ํดjoinRoom
์ด๋ฒคํธ๋ฅผ ์๋ฒ๋ก ๋ณด๋ธ๋ค. - ์๋ฒ๋ก๋ถํฐ ํ์ฌ ๋ฐฉ(
canvas-room
)์ ์๋ ๋ค๋ฅธ ์ฌ์ฉ์ ID ๋ชฉ๋ก(users
)์ ๋ฐ๋๋ค. -
ํผ์ด ์ฐ๊ฒฐ ์์ฑ:
myIdRef
(์์ ์ ID)๊ฐ ์๋ ์ฌ์ฉ์๋ค์๊ฒ๋ง ์๋ก์ด ํผ์ด ์ฐ๊ฒฐ์ ์ค์ ํ๋ค. - ์๋ก์ด ์ฌ์ฉ์๊ฐ ์ฐธ์ฌํ ๋ ๊ธฐ์กด ์ฌ์ฉ์๋ค์ ์๋ฆผ์ ๋ฐ๋๋ค.
- ํผ์ด ์ฐ๊ฒฐ ์์ฑ: ๋ฐฉ์ ์๋ก ๋ค์ด์จ ์ฌ์ฉ์๊ฐ ๊ธฐ์กด ์ฌ์ฉ์์์ ์ฐ๊ฒฐ์ ์์ํ๋ฏ๋ก, ๊ธฐ์กด ์ฌ์ฉ์๋ ์ฐ๊ฒฐ ์์ฒญ์ ๊ธฐ๋ค๋ฆฐ๋ค.
-
Offer: ์ฐ๊ฒฐ ์์ฒญ์ ์์ ํ์ ๋
handleOffer
๋ฅผ ํตํด WebRTC offer๋ฅผ ์ฒ๋ฆฌํ๋ค. -
Answer: ์ฐ๊ฒฐ ์์ฒญ์ ์๋ฝํ ์๋๋ฐฉ์ด ๋ณด๋ธ answer๋ฅผ
handleAnswer
๋ก ์ฒ๋ฆฌํ๋ค. -
ICE ํ๋ณด:
handleIceCandidate
๋ฅผ ํตํด WebRTC ์ฐ๊ฒฐ์ ํ์ํ ๋คํธ์ํฌ ๊ฒฝ๋ก ์ ๋ณด๋ฅผ ์ถ๊ฐํ๋ค. - ์ฌ์ฉ์๊ฐ ๋ฐฉ์ ๋ ๋ ๋
user-left
์ด๋ฒคํธ๋ฅผ ์์ ํ๊ณ , ์ฐ๊ฒฐ๋ ํผ์ด์ ๋ฆฌ์์ค๋ฅผ ์ ๋ฆฌํ๋ค. - cleanupPeerConnection: WebRTC ์ฐ๊ฒฐ์ ์ข ๋ฃํ๊ณ , ๊ด๋ จ ๋ฆฌ์์ค๋ฅผ ํด์ ํ๋ค.
- ์ปดํฌ๋ํธ๊ฐ ์ธ๋ง์ดํธ๋ ๋ ๋ชจ๋ ํผ์ด ์ฐ๊ฒฐ์ ์ ๋ฆฌํ๊ณ ์๋ฒ ์ฐ๊ฒฐ์ ์ข ๋ฃํ๋ค.
const cleanupPeerConnection = (userId: string) => {
const peerConnection = peerConnections.current.get(userId);
if (peerConnection) {
peerConnection.close();
peerConnections.current.delete(userId);
}
const dataChannel = dataChannels.current.get(userId);
if (dataChannel) {
dataChannel.close();
dataChannels.current.delete(userId);
}
pendingCandidates.current.delete(userId);
};
-
ํผ์ด ์ฐ๊ฒฐ ์ข
๋ฃ:
peerConnections
์์ ํด๋น ์ฌ์ฉ์ ID์ ์ฐ๊ฒฐ์ ๋ซ๊ณ ์ญ์ ํ๋ค. -
๋ฐ์ดํฐ ์ฑ๋ ์ข
๋ฃ:
dataChannels
์์ ํด๋น ID์ ๋ฐ์ดํฐ ์ฑ๋์ ๋ซ๊ณ ์ญ์ ํ๋ค. -
๋๊ธฐ ์ค์ธ ICE ํ๋ณด ์ญ์ :
pendingCandidates
์์ ํด๋น ID์ ํญ๋ชฉ์ ์ญ์ ํ๋ค.
const createPeerConnection = (userId: string, initiator: boolean): RTCPeerConnection => {
console.log(`Creating peer connection with ${userId} (initiator: ${initiator})`);
const peerConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
console.log('Sending ICE candidate to:', userId);
socketRef.current?.emit('ice-candidate', {
candidate: event.candidate,
roomId: 'canvas-room',
userId
});
}
};
peerConnection.oniceconnectionstatechange = () => {
console.log(`ICE Connection State with ${userId}: ${peerConnection.iceConnectionState}`);
};
peerConnection.ondatachannel = (event) => {
console.log('Received data channel from:', userId);
const dataChannel = event.channel;
dataChannels.current.set(userId, dataChannel);
setupDataChannelListeners(dataChannel, userId);
};
if (initiator) {
console.log('Creating data channel as initiator for:', userId);
const dataChannel = peerConnection.createDataChannel("canvas", {
ordered: true
});
dataChannels.current.set(userId, dataChannel);
setupDataChannelListeners(dataChannel, userId);
// Initiator creates and sends offer
createAndSendOffer(peerConnection, userId);
}
peerConnections.current.set(userId, peerConnection);
return peerConnection;
};
-
ํผ์ด ์ฐ๊ฒฐ ์์ฑ: ICE ์๋ฒ๋ฅผ ์ค์ ํ์ฌ
RTCPeerConnection
์ ์์ฑํ๊ณ ,peerConnections
์ ์ ์ฅํ๋ค. -
ICE ํ๋ณด ์ด๋ฒคํธ ์ฒ๋ฆฌ: ์๋ก์ด ICE ํ๋ณด๊ฐ ์์ฑ๋๋ฉด ์๋ฒ์ ์ ์กํ์ฌ P2P ์ฐ๊ฒฐ์ ์ค์ ํ๋ค.
-
onicecandidate
์ด๋ฒคํธ๋ ICE candidate๊ฐ ์์ฑ๋ ๋๋ง๋ค ํธ์ถ๋๋ค.
-
-
๋ฐ์ดํฐ ์ฑ๋ ์์ : ์๋๋ฐฉ์ด ๋ฐ์ดํฐ ์ฑ๋์ ์์ฑํ ๊ฒฝ์ฐ ํด๋น ์ฑ๋์ ์์ ํ๊ณ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ์ค์ ํ๋ค.
-
ondatachannel
์ ์๋๋ฐฉ์ด Data Channel์ ์์ฑํด ๋ณด๋์ ๋ ํธ์ถ๋๋ค.
-
-
initiator: initiator์ผ ๊ฒฝ์ฐ ๋ฐ์ดํฐ ์ฑ๋์ ์์ฑํ๊ณ , ์คํผ๋ฅผ ๋ง๋ค์ด ์๋๋ฐฉ์๊ฒ ์ ์กํ๋ค.
- initiator๊ฐ
true
์ธ ๊ฒฝ์ฐ, ํ์ฌ ์ฌ์ฉ์๊ฐ ์ฐ๊ฒฐ์ ์์ํ๋ ์ชฝ์ด๋ค.
- initiator๊ฐ
const createAndSendOffer = async (peerConnection: RTCPeerConnection, userId: string) => {
try {
const offer = await peerConnection.createOffer();
console.log('Created offer for:', userId);
await peerConnection.setLocalDescription(offer);
console.log('Set local description (offer) for:', userId);
socketRef.current?.emit('offer', {
offer,
roomId: 'canvas-room',
userId
});
} catch (error) {
console.error('Error creating offer:', error);
}
};
-
Offer ์์ฑ:
peerConnection
์์ Offer๋ฅผ ์์ฑํ๋ค. - Local Description ์ค์ : ์์ฑํ Offer๋ฅผ Local Description์ผ๋ก ์ค์ ํ์ฌ ์ฐ๊ฒฐ ์์ ์ค๋น๋ฅผ ์๋ฃํ๋ค.
-
Offer ์ ์ก: ์์ผ์ ํตํด ํน์
userId
์ ๋ฐฉ(roomId
)์ผ๋ก Offer๋ฅผ ์ ์กํฉ๋๋ค
const handleOffer = async (offer: RTCSessionDescriptionInit, userId: string) => {
try {
let peerConnection = peerConnections.current.get(userId);
if (!peerConnection) {
console.log('Creating new peer connection for offer from:', userId);
peerConnection = createPeerConnection(userId, false);
}
if (peerConnection.signalingState !== "stable") {
console.log('Ignoring offer in non-stable state');
return;
}
await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
console.log('Set remote description (offer) for:', userId);
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
console.log('Set local description (answer) for:', userId);
socketRef.current?.emit('answer', {
answer,
roomId: 'canvas-room',
userId
});
// Process any pending ICE candidates
const candidates = pendingCandidates.current.get(userId) || [];
for (const candidate of candidates) {
await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
}
pendingCandidates.current.delete(userId);
} catch (error) {
console.error('Error handling offer:', error);
}
};
-
Peer Connection ํ์ธ:
userId
์ ๋ํ Peer Connection์ด ์์ผ๋ฉด ์๋ก ์์ฑํ๋ค. -
์๊ทธ๋๋ง ์ํ ํ์ธ: stable ์ํ์ผ ๋๋ง Offer๋ฅผ ์ฒ๋ฆฌํ๋ค.
-
stable
์ํ: WebRTC ์ฐ๊ฒฐ์์ ์์ ๋ ์ํ๋ฅผ ์๋ฏธํ๋ฉฐ, ์๋ก์ด Offer๋ฅผ ์ฒ๋ฆฌํ ์ค๋น๊ฐ ๋์ด ์๋ค๋ ๊ฒ์ ๋ํ๋ธ๋ค.
-
- Remote Description ์ค์ : ์๋๋ฐฉ์ Offer๋ฅผ ์ ์ฉํ๋ค.
- Answer ์์ฑ ๋ฐ ์ ์ก: Answer๋ฅผ ์์ฑํ์ฌ Local Description์ผ๋ก ์ค์ ํ๊ณ , ์๋๋ฐฉ์๊ฒ ์ ์กํ๋ค.
- ICE ํ๋ณด ์ฒ๋ฆฌ: ๋๊ธฐ ์ค์ธ ICE ํ๋ณด๊ฐ ์์ผ๋ฉด Peer Connection์ ์ถ๊ฐํ๋ค.
const handleAnswer = async (answer: RTCSessionDescriptionInit, userId: string) => {
try {
const peerConnection = peerConnections.current.get(userId);
if (!peerConnection) {
console.error('No peer connection found for user:', userId);
return;
}
const currentState = peerConnection.signalingState;
console.log(`Current signaling state for ${userId}:`, currentState);
if (currentState === "stable") {
console.log('Connection already stable with:', userId);
return;
}
if (currentState !== "have-local-offer") {
console.log(`Unexpected signaling state ${currentState} for answer from:`, userId);
return;
}
await peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
console.log('Set remote description (answer) for:', userId);
// Process any pending ICE candidates
const candidates = pendingCandidates.current.get(userId) || [];
for (const candidate of candidates) {
await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
}
pendingCandidates.current.delete(userId);
} catch (error) {
console.error('Error handling answer:', error);
}
};
-
Peer Connection ํ์ธ
-
userId
์ ํด๋นํ๋ Peer Connection์ด ์กด์ฌํ๋์ง ํ์ธํ๋ค. - ์์ผ๋ฉด ์ค๋ฅ ์ถ๋ ฅ ํ ์ข ๋ฃํ๋ค.
-
-
์๊ทธ๋๋ง ์ํ ํ์ธ
- ์ฐ๊ฒฐ์ด ์ด๋ฏธ "stable" ์ํ๋ผ๋ฉด, Answer๋ฅผ ๋ฌด์ํ๋ค.
- ์ํ๊ฐ "have-local-offer"์ธ ๊ฒฝ์ฐ์๋ง Answer๋ฅผ ์ฒ๋ฆฌํ๋ค.
- Remote Description ์ค์ : Answer๋ฅผ Remote Description์ผ๋ก ์ค์ ํ๋ค.
- ๋๊ธฐ ์ค์ธ ICE ํ๋ณด ์ฒ๋ฆฌ: Answer ์ดํ์๋ ๋๊ธฐ ์ค์ธ ICE ํ๋ณด๋ค์ Peer Connection์ ์ถ๊ฐํ๋ค.
const handleIceCandidate = async (candidate: RTCIceCandidateInit, userId: string) => {
try {
const peerConnection = peerConnections.current.get(userId);
if (!peerConnection) {
console.log('No peer connection found for ICE candidate from:', userId);
return;
}
if (peerConnection.remoteDescription && peerConnection.remoteDescription.type) {
console.log('Adding ICE candidate for:', userId);
await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
} else {
console.log('Queueing ICE candidate for:', userId);
if (!pendingCandidates.current.has(userId)) {
pendingCandidates.current.set(userId, []);
}
pendingCandidates.current.get(userId)?.push(candidate);
}
} catch (error) {
console.error('Error handling ICE candidate:', error);
}
};
- ํผ์ด ์ฐ๊ฒฐ ํ์ธ: ํผ์ด ์ฐ๊ฒฐ์ด ์์ผ๋ฉด ์ข ๋ฃํ๊ณ , ์์ผ๋ฉด ๋ค์ ๋จ๊ณ๋ก ์งํํ๋ค.
- ICE ํ๋ณด ์ถ๊ฐ ๋๋ ํ์ ์ ์ฅ: ์๊ฒฉ ์ค๋ช ์ด ์ค์ ๋ ๊ฒฝ์ฐ ๋ฐ๋ก ICE ํ๋ณด๋ฅผ ์ถ๊ฐํ๊ณ , ๊ทธ๋ ์ง ์์ผ๋ฉด ํ๋ณด๋ฅผ ๋๊ธฐ ํ์ ์ ์ฅํ๋ค.
- ์๋ฌ ์ฒ๋ฆฌ: ICE ํ๋ณด ์ถ๊ฐ ์ค ์ค๋ฅ ๋ฐ์ ์ ์ฝ์์ ์ค๋ฅ๋ฅผ ์ถ๋ ฅํ๋ค.
const setupDataChannelListeners = (dataChannel: RTCDataChannel, userId: string) => {
dataChannel.onopen = () => {
console.log(`Data channel opened with ${userId}`);
};
dataChannel.onclose = () => {
console.log(`Data channel closed with ${userId}`);
};
dataChannel.onerror = (error) => {
console.error(`Data channel error with ${userId}:`, error);
};
dataChannel.onmessage = (event) => {
const { type, x, y } = JSON.parse(event.data);
handleRemoteDrawing(type, x, y);
};
};
-
onmessage
์ด๋ฒคํธ: ๋ค๋ฅธ ์ฌ์ฉ์๋ก๋ถํฐ ๊ทธ๋ฆฌ๊ธฐ ๋ฐ์ดํฐ ๋ฉ์์ง๋ฅผ ๋ฐ์ผ๋ฉด JSON์ผ๋ก ํ์ฑํ์ฌtype
,x
,y
๊ฐ์ ์ถ์ถํ๊ณ , ์ด๋ฅผhandleRemoteDrawing
ํจ์์ ์ ๋ฌํ์ฌ ์๊ฒฉ ๊ทธ๋ฆฌ๊ธฐ ์์ ์ ์ฒ๋ฆฌํ๋ค.
const sendDrawingData = (type: string, x: number, y: number) => {
const message = JSON.stringify({ type, x, y });
dataChannels.current.forEach((dataChannel) => {
if (dataChannel.readyState === "open") {
dataChannel.send(message);
}
});
};
-
type
,x
,y
๊ฐ์ JSON ํ์์ ๋ฉ์์ง๋ก ๋ง๋ ๋ค. - ์ด๋ ค ์๋ ๋ชจ๋ ๋ฐ์ดํฐ ์ฑ๋์ ์ด ๋ฉ์์ง๋ฅผ ์ ์กํ๋ค.
import { useEffect, useRef, useState } from 'react';
import io from 'socket.io-client';
const SOCKET_SERVER = 'http://localhost:3000';
type SocketType = ReturnType<typeof io>;
function App() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const peerConnections = useRef<Map<string, RTCPeerConnection>>(new Map());
const dataChannels = useRef<Map<string, RTCDataChannel>>(new Map());
const pendingCandidates = useRef<Map<string, RTCIceCandidateInit[]>>(new Map());
const socketRef = useRef<SocketType>();
const [isDrawing, setIsDrawing] = useState(false);
const myIdRef = useRef<string>('');
useEffect(() => {
socketRef.current = io(SOCKET_SERVER);
socketRef.current.on('connect', () => {
console.log('Connected to server');
myIdRef.current = socketRef.current?.id || '';
socketRef.current?.emit('joinRoom', 'canvas-room');
});
// ๋ฐฉ์ ์ฐธ์ฌํ์ ๋ ํ์ฌ ๋ฐฉ์ ์๋ ๋ค๋ฅธ ์ ์ ๋ค์ ๋ชฉ๋ก์ ๋ฐ์
socketRef.current.on('room-users', (users: string[]) => {
console.log('Current users in room:', users);
// ์์ ์ ์ ์ธํ ๊ฐ ์ ์ ์ ์ฐ๊ฒฐ ์ค์
users.forEach(userId => {
if (userId !== myIdRef.current && !peerConnections.current.has(userId)) {
createPeerConnection(userId, true);
}
});
});
// ์๋ก์ด ์ ์ ๊ฐ ๋ค์ด์์ ๋
socketRef.current.on('user-joined', (userId: string) => {
console.log('New user joined:', userId);
if (userId !== myIdRef.current && !peerConnections.current.has(userId)) {
// ์๋ก์ด ์ ์ ๊ฐ ๋ค์ด์์ ๋๋ ๊ธฐ์กด ์ ์ ๋ค์ด ์ฐ๊ฒฐ์ ์์ํ์ง ์์
// ์๋ก์ด ์ ์ ๊ฐ ๊ฐ ๊ธฐ์กด ์ ์ ์๊ฒ ์ฐ๊ฒฐ์ ์์ํ ๊ฒ์
createPeerConnection(userId, false);
}
});
socketRef.current.on('offer', async ({ offer, senderId }) => {
console.log('Received offer from:', senderId);
await handleOffer(offer, senderId);
});
socketRef.current.on('answer', async ({ answer, senderId }) => {
console.log('Received answer from:', senderId);
await handleAnswer(answer, senderId);
});
socketRef.current.on('ice-candidate', async ({ candidate, senderId }) => {
await handleIceCandidate(candidate, senderId);
});
socketRef.current.on('user-left', (userId: string) => {
console.log('User left:', userId);
cleanupPeerConnection(userId);
});
return () => {
peerConnections.current.forEach((_, userId) => {
cleanupPeerConnection(userId);
});
socketRef.current?.disconnect();
};
}, []);
const cleanupPeerConnection = (userId: string) => {
const peerConnection = peerConnections.current.get(userId);
if (peerConnection) {
peerConnection.close();
peerConnections.current.delete(userId);
}
const dataChannel = dataChannels.current.get(userId);
if (dataChannel) {
dataChannel.close();
dataChannels.current.delete(userId);
}
pendingCandidates.current.delete(userId);
};
const createPeerConnection = (userId: string, initiator: boolean): RTCPeerConnection => {
console.log(`Creating peer connection with ${userId} (initiator: ${initiator})`);
const peerConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
console.log('Sending ICE candidate to:', userId);
socketRef.current?.emit('ice-candidate', {
candidate: event.candidate,
roomId: 'canvas-room',
userId
});
}
};
peerConnection.oniceconnectionstatechange = () => {
console.log(`ICE Connection State with ${userId}: ${peerConnection.iceConnectionState}`);
};
peerConnection.ondatachannel = (event) => {
console.log('Received data channel from:', userId);
const dataChannel = event.channel;
dataChannels.current.set(userId, dataChannel);
setupDataChannelListeners(dataChannel, userId);
};
if (initiator) {
console.log('Creating data channel as initiator for:', userId);
const dataChannel = peerConnection.createDataChannel("canvas", {
ordered: true
});
dataChannels.current.set(userId, dataChannel);
setupDataChannelListeners(dataChannel, userId);
// Initiator creates and sends offer
createAndSendOffer(peerConnection, userId);
}
peerConnections.current.set(userId, peerConnection);
return peerConnection;
};
const createAndSendOffer = async (peerConnection: RTCPeerConnection, userId: string) => {
try {
const offer = await peerConnection.createOffer();
console.log('Created offer for:', userId);
await peerConnection.setLocalDescription(offer);
console.log('Set local description (offer) for:', userId);
socketRef.current?.emit('offer', {
offer,
roomId: 'canvas-room',
userId
});
} catch (error) {
console.error('Error creating offer:', error);
}
};
const handleOffer = async (offer: RTCSessionDescriptionInit, userId: string) => {
try {
let peerConnection = peerConnections.current.get(userId);
if (!peerConnection) {
console.log('Creating new peer connection for offer from:', userId);
peerConnection = createPeerConnection(userId, false);
}
if (peerConnection.signalingState !== "stable") {
console.log('Ignoring offer in non-stable state');
return;
}
await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
console.log('Set remote description (offer) for:', userId);
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
console.log('Set local description (answer) for:', userId);
socketRef.current?.emit('answer', {
answer,
roomId: 'canvas-room',
userId
});
// Process any pending ICE candidates
const candidates = pendingCandidates.current.get(userId) || [];
for (const candidate of candidates) {
await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
}
pendingCandidates.current.delete(userId);
} catch (error) {
console.error('Error handling offer:', error);
}
};
const handleAnswer = async (answer: RTCSessionDescriptionInit, userId: string) => {
try {
const peerConnection = peerConnections.current.get(userId);
if (!peerConnection) {
console.error('No peer connection found for user:', userId);
return;
}
const currentState = peerConnection.signalingState;
console.log(`Current signaling state for ${userId}:`, currentState);
if (currentState === "stable") {
console.log('Connection already stable with:', userId);
return;
}
if (currentState !== "have-local-offer") {
console.log(`Unexpected signaling state ${currentState} for answer from:`, userId);
return;
}
await peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
console.log('Set remote description (answer) for:', userId);
// Process any pending ICE candidates
const candidates = pendingCandidates.current.get(userId) || [];
for (const candidate of candidates) {
await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
}
pendingCandidates.current.delete(userId);
} catch (error) {
console.error('Error handling answer:', error);
}
};
const handleIceCandidate = async (candidate: RTCIceCandidateInit, userId: string) => {
try {
const peerConnection = peerConnections.current.get(userId);
if (!peerConnection) {
console.log('No peer connection found for ICE candidate from:', userId);
return;
}
if (peerConnection.remoteDescription && peerConnection.remoteDescription.type) {
console.log('Adding ICE candidate for:', userId);
await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
} else {
console.log('Queueing ICE candidate for:', userId);
if (!pendingCandidates.current.has(userId)) {
pendingCandidates.current.set(userId, []);
}
pendingCandidates.current.get(userId)?.push(candidate);
}
} catch (error) {
console.error('Error handling ICE candidate:', error);
}
};
const setupDataChannelListeners = (dataChannel: RTCDataChannel, userId: string) => {
dataChannel.onopen = () => {
console.log(`Data channel opened with ${userId}`);
};
dataChannel.onclose = () => {
console.log(`Data channel closed with ${userId}`);
};
dataChannel.onerror = (error) => {
console.error(`Data channel error with ${userId}:`, error);
};
dataChannel.onmessage = (event) => {
const { type, x, y } = JSON.parse(event.data);
handleRemoteDrawing(type, x, y);
};
};
const handleRemoteDrawing = (type: string, x: number, y: number) => {
if (!canvasRef.current) return;
const context = canvasRef.current.getContext('2d');
if (!context) return;
if (type === 'start') {
context.beginPath();
context.moveTo(x, y);
} else if (type === 'draw') {
context.lineTo(x, y);
context.stroke();
}
};
const startDrawing = ({ nativeEvent }: React.MouseEvent) => {
const { offsetX, offsetY } = nativeEvent;
const context = canvasRef.current?.getContext('2d');
if (!context) return;
context.beginPath();
context.moveTo(offsetX, offsetY);
setIsDrawing(true);
sendDrawingData('start', offsetX, offsetY);
};
const draw = ({ nativeEvent }: React.MouseEvent) => {
if (!isDrawing) return;
const { offsetX, offsetY } = nativeEvent;
const context = canvasRef.current?.getContext('2d');
if (!context) return;
context.lineTo(offsetX, offsetY);
context.stroke();
sendDrawingData('draw', offsetX, offsetY);
};
const stopDrawing = () => {
setIsDrawing(false);
};
const sendDrawingData = (type: string, x: number, y: number) => {
const message = JSON.stringify({ type, x, y });
dataChannels.current.forEach((dataChannel) => {
if (dataChannel.readyState === "open") {
dataChannel.send(message);
}
});
};
return (
<div className="p-4">
<canvas
ref={canvasRef}
width={800}
height={600}
className="border bg-gray-100"
onMouseDown={startDrawing}
onMouseMove={draw}
onMouseUp={stopDrawing}
onMouseLeave={stopDrawing}
/>
</div>
);
}
export default App;
-
๊ฐ ์ฌ์ฉ์ ๋ฐฉ ์
์ฅ
- ๊ฐ ์ฌ์ฉ์๋
useEffect
์์socketRef.current?.emit('joinRoom', 'canvas-room')
์ ํตํด"canvas-room"
์ ์ฐธ์ฌํ๋ค. -
socketRef.current
๋ ์๋ฒ์ ์ฐ๊ฒฐ๋ ์์ผ ํด๋ผ์ด์ธํธ๋ก,joinRoom
์ด๋ฒคํธ๋ฅผ ์๋ฒ์ ๋ณด๋ธ๋ค.
- ๊ฐ ์ฌ์ฉ์๋
-
ํ์ฌ ๋ฐฉ์ ์๋ ์ ์ ๋ค ์ ๋ณด ์์
- ๋ฐฉ์ ์
์ฅํ ํ, ๊ฐ ์ฌ์ฉ์๋
"room-users"
์ด๋ฒคํธ๋ก ํ์ฌ ๋ฐฉ์ ์๋ ๋ค๋ฅธ ์ฌ์ฉ์์ ID ๋ชฉ๋ก์ ๋ฐ๋๋ค. - ๊ฐ ์ฌ์ฉ์๋ ์์ ์ ์ ์ธํ ์ฌ์ฉ์๋ค๊ณผ์ ์ฐ๊ฒฐ์ ์ํด
createPeerConnection
์ ํธ์ถํ์ฌpeerConnection
์ ์์ฑํ๋ค.
- ๋ฐฉ์ ์
์ฅํ ํ, ๊ฐ ์ฌ์ฉ์๋
-
์๋ก์ด ์ฌ์ฉ์ ์
์ฅ ์ฒ๋ฆฌ
- ๊ธฐ์กด ์ฌ์ฉ์๋
"user-joined"
์ด๋ฒคํธ๋ฅผ ํตํด ์๋ก์ด ์ฌ์ฉ์๊ฐ ๋ฐฉ์ ๋ค์ด์์์ ๊ฐ์งํ๋ค. - ์๋ก์ด ์ฌ์ฉ์๋
createPeerConnection
ํจ์๋ก ๊ธฐ์กด ์ฌ์ฉ์๋ค๊ณผ ์ฐ๊ฒฐ ์ค์ ์ ์์ํ๋ค.
- ๊ธฐ์กด ์ฌ์ฉ์๋
-
Offer-Answer ๊ตํ (์๊ทธ๋๋ง ๊ณผ์ )
- ์๋ก์ด ์ฌ์ฉ์๊ฐ ๊ธฐ์กด ์ฌ์ฉ์์๊ฒ
createAndSendOffer
๋ฅผ ํตํด Offer๋ฅผ ์์ฑํ๊ณ ์ ์กํ๋ค. - ๊ธฐ์กด ์ฌ์ฉ์๋
"offer"
์ด๋ฒคํธ๋ก Offer๋ฅผ ๋ฐ์ผ๋ฉฐ,handleOffer
๋ฅผ ํธ์ถํ์ฌ ๋ฐ์ Offer์ ๋ํ Answer๋ฅผ ์์ฑํ๊ณ ์๋๋ฐฉ์๊ฒ ์ ์กํ๋ค. - ์๋ก์ด ์ฌ์ฉ์๋
"answer"
์ด๋ฒคํธ๋ฅผ ํตํด ๊ธฐ์กด ์ฌ์ฉ์์ Answer๋ฅผ ๋ฐ๊ณhandleAnswer
ํจ์๋ก ์ด๋ฅผ ์ฒ๋ฆฌํ์ฌ WebRTC ์ฐ๊ฒฐ์ ์๋ฃํ๋ค.
- ์๋ก์ด ์ฌ์ฉ์๊ฐ ๊ธฐ์กด ์ฌ์ฉ์์๊ฒ
-
ICE Candidate ๊ตํ
- ๋คํธ์ํฌ ๊ฒฝ๋ก ์ค์ ์ ์ํด ICE Candidate๊ฐ ๊ตํ๋๋ค.
- ๊ฐ Peer Connection์
onicecandidate
์ด๋ฒคํธ๋ฅผ ํตํด ์์ ์ ICE Candidate๋ฅผ ์์ฑํ์ฌ ์๋๋ฐฉ์๊ฒ ์ ์กํ๋ค. - ๊ฐ ์ฌ์ฉ์๋
"ice-candidate"
์ด๋ฒคํธ๋ฅผ ํตํด ๋ฐ์ ICE Candidate๋ฅผhandleIceCandidate
ํจ์๋ก Peer Connection์ ์ถ๊ฐํ๋ค.
-
Data Channel ์ค์ ๋ฐ ๋ฉ์์ง ์ ์ก
- ์ฐ๊ฒฐ์ด ์๋ฃ๋๋ฉด Data Channel์ ํตํด ๊ทธ๋ฆผ ๋ฐ์ดํฐ๊ฐ ์ ์ก๋๋ค.
- ์ฌ์ฉ์๊ฐ ์บ๋ฒ์ค์์ ๊ทธ๋ฆผ์ ๊ทธ๋ฆฌ๋ฉด
startDrawing
,draw
,stopDrawing
ํจ์๊ฐ ํธ์ถ๋๋ฉฐ,sendDrawingData
๋ก ๊ฐ ์์น ์ขํ๊ฐ Data Channel์ ํตํด ์ ๋ฌ๋๋ค. - ๋ฐ์ดํฐ๊ฐ ์์ ๋๋ฉด
onmessage
์ด๋ฒคํธ๊ฐ ํธ๋ฆฌ๊ฑฐ๋์ดhandleRemoteDrawing
์ด ํธ์ถ๋๊ณ , ์บ๋ฒ์ค์ ์ค์๊ฐ์ผ๋ก ๋ฐ์๋๋ค.
- 1. ๊ฐ๋ฐ ํ๊ฒฝ ์ธํ ๋ฐ ํ๋ก์ ํธ ๋ฌธ์ํ
- 2. ์ค์๊ฐ ํต์
- 3. ์ธํ๋ผ ๋ฐ CI/CD
- 4. ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์ด Canvas ๊ตฌํํ๊ธฐ
- 5. ์บ๋ฒ์ค ๋๊ธฐํ๋ฅผ ์ํ ์์ CRDT ๊ตฌํ๊ธฐ
-
6. ์ปดํฌ๋ํธ ํจํด๋ถํฐ ์น์์ผ๊น์ง, ํจ์จ์ ์ธ FE ์ค๊ณ
- ์ข์ ์ปดํฌ๋ํธ๋ ๋ฌด์์ธ๊ฐ? + Headless Pattern
- ํจ์จ์ ์ธ UI ์ปดํฌ๋ํธ ์คํ์ผ๋ง: Tailwind CSS + cn.ts
- Tailwind CSS๋ก ๋์์ธ ์์คํ ๋ฐ UI ์ปดํฌ๋ํธ ์ธํ
- ์น์์ผ ํด๋ผ์ด์ธํธ ๊ตฌํ๊ธฐ: React ํ๊ฒฝ์์ ํจ์จ์ ์ธ ์น์์ผ ์ํคํ ์ฒ
- ์น์์ผ ํด๋ผ์ด์ธํธ ์ฝ๋ ๋ถ์ ๋ฐ ๊ณต์
- 7. ํธ๋ฌ๋ธ ์ํ ๋ฐ ์ฑ๋ฅ/UX ๊ฐ์
- 1์ฃผ์ฐจ ๊ธฐ์ ๊ณต์
- 2์ฃผ์ฐจ ๋ฐ๋ชจ ๋ฐ์ด
- 3์ฃผ์ฐจ ๋ฐ๋ชจ ๋ฐ์ด
- 4์ฃผ์ฐจ ๋ฐ๋ชจ ๋ฐ์ด
- 5์ฃผ์ฐจ ๋ฐ๋ชจ ๋ฐ์ด
- WEEK 06 ์ฃผ๊ฐ ๊ณํ
- WEEK 06 ๋ฐ์ผ๋ฆฌ ์คํฌ๋ผ
- WEEK 06 ์ฃผ๊ฐ ํ๊ณ