(이 튜토리얼은 시리즈형태이므로 "채팅구현하기 1"을 보고 오셔야 튜토리얼의 원할한 진행이 가능합니다.)
튜토리얼 진행 시 사용된 코드들은 튜토리얼 하단부에 모두 전체파일로 제공되어 있습니다.
순서
- JOIN이란?
- 채팅방 그리기
- 채팅방 참여
- 채팅방 메세지 전송
- 전체 코드
실제 코딩에 앞서 먼저 이번 튜토리얼에서 사용하게될 socketio api 중 join에 대해 먼저 알아보자.
1. JOIN이란?
SocketIO 에서 JOIN에 대한 개념은 사실 여기에 보면 "아주 잘" 나와있다. (Socket.io - Rooms)
하지만 좀 간단하게 설명을 하자면, 혹은, 좀 더 한글로 설명을 하자면,
첫째로, JOIN에 대해 알려면 namespace에 대해 알아야한다. (사실상 이 튜토리얼에서는 namespace를 사용하지 않는다) SocketIO 에는 namespace라는 개념이 존재한다.
Namespace | Room | |
정의 | 한 Connection에서 각 각의 로직을 처리할 수 있게 해주는 커뮤니케이션 채널 | 각 각의 namespace에 속해 있는 소켓들의 subset |
정리 | Room 의 상위 개념 | Namespace 의 하위 개념 |
예시 | "채널" | "방" |
채널123 | 개발자 오세요 | |
채널235 | 개발자 오세요 |
간단하게 보고 넘어갈 수 있게 표로 정리해두었다.
:: namespace란 일종의 분기점이라고 생각하면 편한데, 한 connection(클라이언트-서버)에서 각 각의 namespace마다 각 각의 로직을 처리할 수 있게 해주는 커뮤니케이션 채널이다. (Socket.io - Namespaces)
각 각의 communication channel인 namepsace 에는 임의로 Room 이라는 것을 지정해서 쓸 수 있다. Room이라고 하는 개념은 주로, 소켓들을 room 단위(subset)로 나눠 해당 소켓 그룹에 패킷을 송출할 때 많이 쓰인다.
이 두가지 개념은 채팅방을 활용해서 예를 들 수가 있다.
온라인 채팅서비스에, 직장인 "채널"이 있고, 해당 채널엔 각 각의 사용자가 모여있는 "방"이 있을 것이다. 구체적으로 예를 들자면,
"채널123"에 "개발자 오세요"라는 방이 있다.
이 경우, "채널123"은 Namespace가 되고, "개발자 오세요"가 Room이 되는 것이다.
이제 JOIN에 대한 개념은 알았으니, 실제 코드를 작성해보자.
우선 채팅방이 어떻게 구성될지 프론트를 그려보자
2. 채팅방 그리기
튜토리얼1에서 만들었던, index.html, app.js 를 수정하여 진행한다.
<!-- index.html -->
<style>
#room-messages { list-style-type: none; margin: 0; padding: 0; }
#room-messages li { padding: 5px 10px; }
#room-messages li:nth-child(odd) { background: #eee; }
</style>
...
<!-- 방선택 -->
<div class="col-lg-8">
<div class="card">
<div class="card-header">
방선택
</div>
<div class="card-body">
<div class="input-group mb-1">
<select id="select-room" class="form-control">
<option value="none" selected="selected">방을 선택해주세요</option>
<!-- 수동으로 만들어진 채팅 방 -->
<option value="test-room">테스트 방</option>
</select>
<div class="input-group-append">
<button id="select-room-button" class="btn btn-success" placeholder="message">Select</button>
</div>
</div>
<form action="">
<div class="input-group mb-1">
<input type="text" class="form-control" id="room-message" autocomplete="off" />
<div class="input-group-append">
<button id="room-msg-send" class="btn btn-primary" placeholder="message">Send</button>
</div>
</div>
</form>
</div>
<div class="card-footer">
<ul id="room-messages"></ul>
</div>
</div>
</div>
...
튜토리얼 1번에 비어있던 방선택 부분을 위와 같이 채워서 채팅방을 그려보자.
여기서 알아야할 부분은,
- #select-room 이라는 select DOM 을 활용하여 사용자가 방을 선택하게 만들어두었다.
- #test-room 이라는 방을 html DOM 으로 미리 만들어 두었고,
- option value 로 방을 만들 수 있다.
- 메시지 창으로는 #room-message 로 채팅 유저의 텍스트를 받으며,
- #room-msg-send 버튼으로 메시지를 보낸다.
- 소켓을 통해서 받은 데이터(메세지)들은 #room_messages 에 prepend 된다.
그럼 이제 html파일을 참고하여 채팅방 참여를 구현해보자.
채팅방 참여를 구현하기 전, 우선 흐름을 알아보자.
꼭 이해하고 넘어가자 !
채팅방 참여
- 클라이언트에서 req_join_room프로토콜로 서버에 message를 보낸다.
- 서버에서 req_join_room프로토콜 request를 확인한다.
- 서버에서 해당 socket을 roomName방에 join 시킨다.
- 서버에서 해당 roomName방에 참여하고 있는 참여자(socket)에게 noti_join_room을 방출(emit)한다.
- 클라이언트에서 noti_join_room프로토콜을 확인한다.
- 클라이언트에서 다른 사용자가 join했음을 알린다.
채팅방 메세지 전송
- 클라이언트에서 req_room_message프로토콜로 서버에 message를 보낸다.
- 서버에서 req_room_message프로토콜 request를 확인한다.
- 서버에서 해당 socket이 속해있는 방에 noti_room_message프로토콜을 방출(emit)한다.
- 클라이언트에서 noti_room_message프로토콜을 확인한다.
- 클라이언트에서 noti_room_message 프로토콜 데이터를 활용하여 client UI에 메세지를 그린다.
3. 채팅방 참여
<!-- index.html -->
<script>
$('#select-room-button').click(() => {
let roomName = $('#select-room').val();
if(roomName === "none")
return alert("방을 선택해주세요.");
socket.emit('req_join_room', roomName);
});
</script>
#select-room-button이 클릭 되었을 때, #select-room으로 방의 이름을 정한 뒤,
서버에 req_join_room 프로토콜 패킷을 roomName과 함께 방출(emit)한다.
브라우저에서 req_join_room 패킷을 그럼 서버에선 어떻게 처리할까?
// app.js
let rooms = [];
// 방참여 요청
socket.on('req_join_room', async (msg) => {
let roomName = 'Room_' + msg.roomName;
if(!rooms.includes(roomName)) {
rooms.push(roomName);
}else{
// does nothing
}
socket.join(roomName);
io.to(roomName).emit('noti_join_room', "방에 입장하였습니다.");
});
let rooms 는 생성되는 모든 방을 담아두기 위해 만들어놓은 배열이고, 새로운 방이 생길 때 마다, rooms에 추가한다.
그냥 방을 추가해도 되지만, 방이름을 좀 더 명확하게 하기위해 앞에 Room_ 이라는 프리픽스를 붙여 방을 생성한다.
방이름을 설정하고 난 뒤,
여기서 join 이라는 개념을 사용하게 되는데, 해당 사용자를 roomName으로 방 참여(join)을 시킨다.
(이전 튜토리얼에서 설명했던 것 처럼 socket은 요청한 사용자를 나타낸다.)
socket.join(roomName);
소켓을 방에 join 시켜 놓은 뒤, noti_join_room 프로토콜을 방 참여자에게 방출한다.
io.to(roomName).emit('noti_join_room', "방에 입장하였습니다.");
여기서 noti_join_room 이라는 이름의 프로토콜을 사용했다. 이 프로토콜을 사용한 이유는,
보통 request-response 의 관계는 request를 한 client 에게 response라는 답을 주는 형태이다. 하지만 request를 한 client 외에 다른 client에게 "알려야"하기때문에, noti_ 라는 프로토콜을 이름으로 지정하였다.
그리고 또 여기서 중요하게 봐야할 부분은,
해당 방에만 송출할 때는 io.to(roomName).emit(...)을 활용한다.
그럼 noti_join_room을 client에서는 어떻게 처리할까?
noti_ (room emit)라 다를 거라고 처리하는 방법도 다를 거라고 생각할 수 있지만, 일반 소켓 처리하는 방법과 동일하다.
<!-- index.html -->
<script>
socket.on('noti_join_room', (res) => {
$('#room-messages').prepend($('<li>').text(res));
});
</script>
#room-messages에 서버에서 보낸 "방에 입장하였습니다." 가 표시되게 된다.
4. 채팅방 메세지 전송
사용자가 채팅방에 입장 후, 방 참여자들에게 메시지를 전송해보자.
<!-- index.html -->
<script>
$('#room-msg-send').click(() => {
socket.emit('req_room_message', $('#room-message').val());
$('#room-message').val('');
return false;
});
</script>
jQuery를 사용하여, #room-msg-send 버튼이 클릭되었을 때, socket에 방출 명령어를 입력한다.
보낼 프로토콜은 req_room_message이며, #room-message input 의 value 값을 data 로 보낸다.
그렇다면 서버에서는 어떻게 방참여자들에게 메시지를 보낼까?
// app.js
// 채팅방에 채팅 요청
socket.on('req_room_message', async(msg) => {
let userCurrentRoom = getUserCurrentRoom(socket);
io.to(userCurrentRoom).emit('noti_room_message', msg);
});
...
// 소켓이 현재 연결된 방의 이름
function getUserCurrentRoom(socket){
let currentRoom = '';
let socketRooms = Object.keys(socket.rooms);
for( let i = 0 ; i < socketRooms.length; i ++ ){
if(socketRooms[i].indexOf('Room_') !== -1){
currentRoom = socketRooms[i];
break;
}
}
return currentRoom;
}
서버에서는 해당 소켓에 참여중인 rooms 중 방 생성 시 prefix로 설정했던 Room_을 포함한 방에, 전달받은 메세지를 전달한다.
(메세지를 전달할 프로토콜은 noti_room_message이다.)
let userCurrentRoom = getUserCurrentRoom(socket);
io.to(userCurrentRoom).emit('noti_room_message', msg);
위에 설명에 나와있던 것과 동일하게 , 해당방에 참여하고 있는 사람에게 메세지를 전달할 때는,
io.to(userCurrentRoom).emit('noti_room_message', msg);
io.to(room).emit(msg) 를 사용하여 발송한다.
해당 메세지를 전달받은 클라이언트는
<!-- index.html -->
<script>
socket.on('noti_room_message', (res) => {
$('#room-messages').prepend($('<li>').text(res));
});
</script>
#room_messages 에 전달받은 메세지를 아래 그림과 같이 prepend한다
지금까지 socketio 기본중의 기본을 활용한 채팅서버를 구현해보았다. 사실상 이 이외에도 구현방법이 여럿 있는데, 기본에 충실하기 위해 간단하게 제작했다.
다음 튜토리얼3에서는
단순히 메세지 발송, 방참여 뿐만 아니라,
- 닉네임 기능 추가
- 텍스트에 발신자명 추가
- 지금까지의 코드를 리팩토링하여 메인터넌스(유지보수)가 용이하도록 코드를 수정
해보도록 하자.
5. 전체 코드
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Socket Tester</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<style>
#messages { list-style-type: none; margin: 0; padding: 0; }
#messages li { padding: 5px 10px; }
#messages li:nth-child(odd) { background: #eee; }
</style>
</head>
<body>
<div class="row">
<!-- 대기실 -->
<div class="col-lg-4">
<div class="card">
<div class="card-header">
대기실
</div>
<div class="card-body">
<form action="">
<div class="input-group mb-3">
<input type="text" class="form-control" id="m" autocomplete="off" />
<div class="input-group-append">
<button id="msg-send" class="btn btn-primary" placeholder="message">Send</button>
</div>
</div>
</form>
</div>
<div class="card-footer">
<ul id="messages"></ul>
</div>
</div>
</div>
<!-- 방선택 -->
<div class="col-lg-8">
<div class="card">
<div class="card-header">
방선택
</div>
<div class="card-body">
<div class="input-group mb-1">
<select id="select-room" class="form-control">
<option value="none" selected="selected">방을 선택해주세요</option>
<!-- 수동으로 만들어진 채팅 방 -->
<option value="test-room">테스트 방</option>
</select>
<div class="input-group-append">
<button id="select-room-button" class="btn btn-success" placeholder="message">Select</button>
</div>
</div>
<form action="">
<div class="input-group mb-1">
<input type="text" class="form-control" id="room-message" autocomplete="off" />
<div class="input-group-append">
<button id="room-msg-send" class="btn btn-primary" placeholder="message">Send</button>
</div>
</div>
</form>
</div>
<div class="card-footer">
<ul id="room-messages"></ul>
</div>
</div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-1.11.1.js"></script>
<script>
$(() => {
/** Socket Starts **/
const socket = io();
// 클라이언트에서 reuqest_message 프로토콜로 id='m' 의 input 값을 보낸다.
$('#msg-send').click(() => {
socket.emit('request_message', $('#m').val());
$('#m').val('');
return false;
});
$('#select-room-button').click(() => {
let roomName = $('#select-room').val();
if(roomName === "none")
return alert("방을 선택해주세요.");
socket.emit('req_join_room', roomName)
});
$('#room-msg-send').click(() => {
socket.emit('req_room_message', $('#room-message').val());
$('#room-message').val('');
return false;
});
socket.on('response_message', (res) => {
$('#messages').prepend($('<li>').text(res));
});
socket.on('noti_join_room', (res) => {
$('#room-messages').prepend($('<li>').text(res));
});
socket.on('noti_room_message', (res) => {
$('#room-messages').prepend($('<li>').text(res));
});
});
</script>
</body>
</html>
// app.js
const app = require('express')();
const http = require('http').createServer(app);
const io = require('socket.io')(http);
let rooms = [];
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
});
io.on('connection', (socket)=>{
socket.on('request_message', (msg) => {
// response_message로 접속중인 모든 사용자에게 msg 를 담은 정보를 방출한다.
io.emit('response_message', msg);
});
// 방참여 요청
socket.on('req_join_room', async (msg) => {
let roomName = 'Room_' + msg;
if(!rooms.includes(roomName)) {
rooms.push(roomName);
}else{
}
socket.join(roomName);
io.to(roomName).emit('noti_join_room', "방에 입장하였습니다.");
});
// 채팅방에 채팅 요청
socket.on('req_room_message', async(msg) => {
let userCurrentRoom = getUserCurrentRoom(socket);
io.to(userCurrentRoom).emit('noti_room_message', msg);
});
socket.on('disconnect', async () => {
console.log('user disconnected');
});
});
// TEST CODE GOES HERE
(async function(){
})();
function getUserCurrentRoom(socket){
let currentRoom = '';
let socketRooms = Object.keys(socket.rooms);
for( let i = 0 ; i < socketRooms.length; i ++ ){
if(socketRooms[i].indexOf('Room_') !== -1){
currentRoom = socketRooms[i];
break;
}
}
return currentRoom;
}
http.listen(3000, () => {
console.log('Connected at 3000');
});
'IT Study > Nodejs 채팅서버 튜토리얼' 카테고리의 다른 글
[Nodejs] 채팅구현하기 1 - SocketIO (처음부터 끝까지) (1) | 2020.11.24 |
---|