Get started | Socket.IO
In this guide we’ll create a basic chat application. It requires almost no basic prior knowledge of Node.JS or Socket.IO, so it’s ideal for users of all knowledge levels.
socket.io
Socket.IO 공식문서 사이트에 가면 빠르게 채팅을 만들어 보는 예시가 있다.
처음에 이대로 만들어보고 조금씩 발전시켜나가는 방향으로 구현해봤다.
<실제 구현 모습>
1. socket.io 서버 연결
http 서버를 생성하고 socket.io 서버에 넣어 socket을 사용하기 위한 'io'를 만들었다.
"localhost:3000" 으로 들어가면 보여줄 html 파일을 정적파일로 설정했다.
const express = require("express");
const app = express();
const http = require("http");
const server = http.createServer(app);
const { Server } = require("socket.io");
const io = new Server(server);
app.get("/", (req, res) => {
res.sendFile(__dirname + "/index.html");
});
server.listen(3000, () => {
console.log("listening on *:3000");
});
2. 채팅방에 유저 입장과 퇴장
입장시 프롬프트로 유저 이름을 설정합니다.
그리고 채팅방을 선택합니다.
프론트에서 중요한 코드만 뽑아서 보면
'const socket = io();' -> socket.io를 사용하기 위해 socket에 연결하고
'socket.emit("connection name", data)' -> socket.emit으로 소켓에 메시지를 보낸다.
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
socket.emit("new join room", preJoinRoom, newJoinRoom, name);
</script>
3. 서버에서 소켓메시지 확인하고 채팅방 입장하기
'채팅방'을 구현하기 위해서는 아래를 이해해야한다.
socket.io에서는 'namespace'->'room'->'socket'으로 포함관계다.
namespace는 하나의 주제나 채널로 생각하고 그 안에 room이 있고 거기에 입장하는 유저가 socket이다.
-프론트에서 socket.emit("new join room", data)으로 보낸 데이터를 받기 위해 socket.on("new join room")으로 접근
-socket.join(room_name) 을 통해서 socket이 room에 들어간걸 처리한다.
-유저당 1개의 소켓으로 생각하고 socket.name(유저이름), socket.room(방이름)을 설정해준다.
io.on("connection", (socket) => {
socket.on("new join room", (preJoinRoom, newJoinRoom, name) => {
socket.name = name;
socket.join(newJoinRoom);
socket.room = newJoinRoom;
});
});
4. 서버에서 채팅방 참여 인원수와 목록 구하기
* console.log(io.sockets)을 해보자
-유저 닉네임은 'sfsdf'고 'Room2'에 입장했다.
<ref *1> Namespace {
_events: [Object: null prototype] { connection: [Function (anonymous)] },
_eventsCount: 1,
_maxListeners: undefined,
sockets: Map(1) {
'TrDsusi1Kbx0cWQZAAAB' => Socket {
_events: [Object: null prototype],
_eventsCount: 3,
_maxListeners: undefined,
nsp: [Circular *1],
client: [Client],
data: {},
connected: true,
acks: Map(0) {},
fns: [],
flags: {},
server: [Server],
adapter: [Adapter],
id: 'TrDsusi1Kbx0cWQZAAAB',
handshake: [Object],
name: 'sfsdf',
room: 'Room2',
[Symbol(kCapture)]: false
}
},
_fns: [],
_ids: 0,
server: Server {
_events: [Object: null prototype] {},
_eventsCount: 0,
_maxListeners: undefined,
_nsps: Map(1) { '/' => [Circular *1] },
parentNsps: Map(0) {},
_path: '/socket.io',
clientPathRegex: /^\/socket\.io\/socket\.io(\.msgpack|\.esm)?(\.min)?\.js(\.map)?(?:\?|$)/,
_connectTimeout: 45000,
_serveClient: true,
_parser: {
protocol: 5,
PacketType: [Object],
Encoder: [class Encoder],
Decoder: [class Decoder extends Emitter]
},
encoder: Encoder { replacer: undefined },
_adapter: [class Adapter extends EventEmitter],
sockets: [Circular *1],
opts: {},
eio: Server {
_events: [Object: null prototype],
_eventsCount: 1,
_maxListeners: undefined,
clients: [Object],
clientsCount: 1,
opts: [Object],
ws: [WebSocketServer],
[Symbol(kCapture)]: false
},
httpServer: Server {
maxHeaderSize: undefined,
insecureHTTPParser: undefined,
_events: [Object: null prototype],
_eventsCount: 5,
_maxListeners: undefined,
_connections: 3,
_handle: [TCP],
_usingWorkers: false,
_workers: [],
_unref: false,
allowHalfOpen: true,
pauseOnConnect: false,
noDelay: false,
keepAlive: false,
keepAliveInitialDelay: 0,
httpAllowHalfOpen: false,
timeout: 0,
keepAliveTimeout: 5000,
maxHeadersCount: null,
maxRequestsPerSocket: 0,
headersTimeout: 60000,
requestTimeout: 0,
_connectionKey: '6::::3000',
[Symbol(IncomingMessage)]: [Function: IncomingMessage],
[Symbol(ServerResponse)]: [Function: ServerResponse],
[Symbol(kCapture)]: false,
[Symbol(async_id_symbol)]: 8
},
engine: Server {
_events: [Object: null prototype],
_eventsCount: 1,
_maxListeners: undefined,
clients: [Object],
clientsCount: 1,
opts: [Object],
ws: [WebSocketServer],
[Symbol(kCapture)]: false
},
[Symbol(kCapture)]: false
},
name: '/',
adapter: Adapter {
_events: [Object: null prototype] {},
_eventsCount: 0,
_maxListeners: undefined,
nsp: [Circular *1],
rooms: Map(2) { 'TrDsusi1Kbx0cWQZAAAB' => [Set], 'Room2' => [Set] },
sids: Map(1) { 'TrDsusi1Kbx0cWQZAAAB' => [Set] },
encoder: Encoder { replacer: undefined },
[Symbol(kCapture)]: false
},
[Symbol(kCapture)]: false
}
-위에 console.log(io.sockets)을 본다면 'io'-'sockets'-'adapter'-rooms'-Map이 있는 걸 볼 수 있다. 이걸 통해서 현재 입장한 채팅방에 인원과 유저를 확인할 수 있다.
-clients로 해당방에 대한 Set을 가져온게 'clients' 그리고 참여유저 수를 구한게 'roomClientsNum'이다.
-유저목록을 구하기 위해서는 'clients'에 있는 소켓아이디를 사용한다.
'io'-'sockets'-'sockets' 으로 접근하면 Map구조로 소켓아이디가 키값이기때문에 그걸통해서 유저이름을 구한다.
io.on("connection", (socket) => {
socket.on("new join room", (preJoinRoom, newJoinRoom, name) => {
socket.name = name;
socket.join(newJoinRoom);
socket.room = newJoinRoom;
let clients = io.sockets.adapter.rooms.get(newJoinRoom);
const roomClientsNum = clients ? clients.size : 0;
const currentChatRoomUserList = [];
clients.forEach((element) => {
currentChatRoomUserList.push(io.sockets.sockets.get(element).name);
});
});
});
5. 서버에서 채팅방 이동 처리하기
-해당 채팅방에만 메시지를 보내기 위해 io.to(room_name).emit("connection name", data)를 사용한다.
새로운 채팅방에 입장시 그 방에 공지를 하고 나온 채팅방에 퇴장 공지를 한다.
-기존 채팅방을 나가는 경우 socket.leave(room_name)을 해준다.
io.on("connection", (socket) => {
io.to(newJoinRoom).emit(
"notice",
currentChatRoomUserList,
roomClientsNum,
socket.name,
" 님이 들어오셨습니다"
);
if (preJoinRoom !== "") {
socket.leave(preJoinRoom);
let clients = io.sockets.adapter.rooms.get(preJoinRoom);
const roomClientsNum = clients ? clients.size : 0;
const currentChatRoomUserList = [];
if (clients) {
clients.forEach((element) => {
currentChatRoomUserList.push(io.sockets.sockets.get(element).name);
});
}
io.to(preJoinRoom).emit(
"notice",
currentChatRoomUserList,
roomClientsNum,
socket.name,
" 님이 나가셨습니다"
);
}
});
});
6. 채팅메시지를 유저가 참여한 채팅방에만 보내주기
-입장과 퇴장공지처럼 io.to(room_name).emit("connection name", data)로 채팅메시지를 보내준다.
io.on("connection", (socket) => {
socket.on("chat message", (msg) => {
io.to(socket.room).emit("chat message", socket.name, msg);
});
});
7. 채팅 종료 처리하기
-채팅방을 나가면 socket.on("disconnection")이 처리되기 때문에 여기에다가 작성한다.
-유저가 나간경우 채팅방 유저목록을 재업로드하고 퇴장공지를 해준다.
io.on("connection", (socket) => {
socket.on("disconnect", () => {
let clients = io.sockets.adapter.rooms.get(socket.room);
const roomClientsNum = clients ? clients.size : 0;
const currentChatRoomUserList = [];
if (clients) {
clients.forEach((element) => {
currentChatRoomUserList.push(io.sockets.sockets.get(element).name);
});
}
io.emit(
"notice",
currentChatRoomUserList,
roomClientsNum,
socket.name,
"님이 나가셨습니다"
);
});
});
8. 프론트에서 채팅메시지 보여주기
-입력한 메시지를 전송할 때 socket.emit("chat message", msg)
-입퇴장 공지 받기 socket.on("notice", data)
-해당채팅방의 메시지를 받을 때 socket.on("chat message") *이때는 방을 써주지 않아도 해당 방 메시지만 받을 수 있음
<script>
// 메시지 입력 후 보내기
form.addEventListener("submit", (e) => {
e.preventDefault();
if (input.value) {
socket.emit("chat message", input.value);
input.value = "";
}
});
// 참여자 입출여부 공지
socket.on("notice", (currentChatRoomUserList, userNum, name, msg) => {
chatUserNum.textContent = `chat user num : ${userNum}`;
chatUserList.textContent = `chat user list : ${currentChatRoomUserList}`;
const notice = "Notice : " + name + msg;
createNewMessage(notice);
});
// 실시간 채팅 박스 생성
socket.on("chat message", (name, msg) => {
const message = name + " : " + msg;
createNewMessage(message);
});
// 메시지 새로 생성
function createNewMessage(msg) {
const item = document.createElement("li");
item.textContent = msg;
messages.appendChild(item);
}
</script>
최종 코드
🌟중간에 설명을 위해 생략한 부분도 있기 때문에 최종 코드를 확인해주세요
<server.js>
const express = require("express");
const app = express();
const http = require("http");
const server = http.createServer(app);
const { Server } = require("socket.io");
const io = new Server(server);
app.get("/", (req, res) => {
res.sendFile(__dirname + "/index.html");
});
io.on("connection", (socket) => {
socket.on("new join room", (preJoinRoom, newJoinRoom, name) => {
socket.name = name;
socket.join(newJoinRoom);
socket.room = newJoinRoom;
let clients = io.sockets.adapter.rooms.get(newJoinRoom);
const { currentChatRoomUserList, roomClientsNum } = getRoomInfo(clients);
io.to(newJoinRoom).emit(
"notice",
currentChatRoomUserList,
roomClientsNum,
socket.name,
" 님이 들어오셨습니다"
);
if (preJoinRoom !== "") {
socket.leave(preJoinRoom);
let clients = io.sockets.adapter.rooms.get(preJoinRoom);
const { currentChatRoomUserList, roomClientsNum } = getRoomInfo(clients);
io.to(preJoinRoom).emit(
"notice",
currentChatRoomUserList,
roomClientsNum,
socket.name,
" 님이 나가셨습니다"
);
}
});
socket.on("chat message", (msg) => {
io.to(socket.room).emit("chat message", socket.name, msg);
});
socket.on("disconnect", () => {
let clients = io.sockets.adapter.rooms.get(socket.room);
const { currentChatRoomUserList, roomClientsNum } = getRoomInfo(clients);
io.emit(
"notice",
currentChatRoomUserList,
roomClientsNum,
socket.name,
"님이 나가셨습니다"
);
});
});
server.listen(3000, () => {
console.log("listening on *:3000");
});
function getRoomInfo(clients) {
const roomClientsNum = clients ? clients.size : 0;
const currentChatRoomUserList = [];
if (clients) {
clients.forEach((element) => {
currentChatRoomUserList.push(io.sockets.sockets.get(element).name);
});
}
return { roomClientsNum, currentChatRoomUserList };
}
<index.html>
<!DOCTYPE html>
<html>
<head>
<title>Socket.IO chat</title>
<style>
#body {
margin: 0;
padding-bottom: 3rem;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif;
}
#form {
background: rgba(0, 0, 0, 0.15);
padding: 0.25rem;
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
height: 3rem;
box-sizing: border-box;
backdrop-filter: blur(10px);
}
#input {
border: none;
padding: 0 1rem;
flex-grow: 1;
border-radius: 2rem;
margin: 0.25rem;
}
#input:focus {
outline: none;
}
#form > button {
background: #333;
border: none;
padding: 0 1rem;
margin: 0.25rem;
border-radius: 3px;
outline: none;
color: #fff;
}
#messages {
list-style-type: none;
margin: 0;
padding: 0;
}
#messages > li {
padding: 0.5rem 1rem;
}
#messages > li:nth-child(odd) {
background: #efefef;
}
.chat-wrapper {
margin: 0 auto;
width: 80%;
}
h1,
h2 {
align-items: center;
text-align: center;
}
#ul {
margin-bottom: 50px;
}
#form {
height: 50px;
position: fixed;
}
.select {
text-align: center;
}
</style>
</head>
<body>
<div>
<h1>Chat Room</h1>
<h2 id="room-name">room name</h2>
<h2 id="user-num">chat user num :</h2>
<h2 id="user-list">chat user list :</h2>
</div>
<div class="select">
<select id="selectBox" onchange="changeSelection()">
<option selected disabled>참여할 방 번호를 선택하세요</option>
<option>Room1</option>
<option>Room2</option>
<option>Room3</option>
<option>Room4</option>
</select>
</div>
<div class="chat-wrapper" id="chat-wrapper">
<ul id="messages"></ul>
<form id="form" action="">
<input id="input" autocomplete="off" /><button>Send</button>
</form>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
let preJoinRoom = "";
const name = prompt("what's your name");
const socket = io();
const messages = document.getElementById("messages");
const form = document.getElementById("form");
const input = document.getElementById("input");
const selectedElement = document.getElementById("selectBox");
const roomName = document.getElementById("room-name");
const chatUserNum = document.getElementById("user-num");
const chatUserList = document.getElementById("user-list");
// 메시지 입력 후 보내기
form.addEventListener("submit", (e) => {
e.preventDefault();
if (input.value) {
socket.emit("chat message", input.value);
input.value = "";
}
});
// 참여자 입출여부 공지
socket.on("notice", (currentChatRoomUserList, userNum, name, msg) => {
chatUserNum.textContent = `chat user num : ${userNum}`;
chatUserList.textContent = `chat user list : ${currentChatRoomUserList}`;
const notice = "Notice : " + name + msg;
createNewMessage(notice);
});
// 실시간 채팅 박스 생성
socket.on("chat message", (name, msg) => {
const message = name + " : " + msg;
createNewMessage(message);
});
// 메시지 새로 생성
function createNewMessage(msg) {
const item = document.createElement("li");
item.textContent = msg;
messages.appendChild(item);
}
// 채팅방 선택 및 변경
function changeSelection() {
// 선택한 채팅방 이름
let newJoinRoom =
selectedElement.options[selectedElement.selectedIndex].text;
// 상단 채팅방 이름 업데이트
roomName.textContent = newJoinRoom;
// 기존방이 아닌 새로운 방을 선택한 경우
if (preJoinRoom !== newJoinRoom) {
socket.emit("new join room", preJoinRoom, newJoinRoom, name);
}
preJoinRoom = newJoinRoom;
}
</script>
</body>
</html>
아직 간단하게만 만들어본거라 Socket.IO를 완벽히 안다고는 말할 수 없다.
그래도 이만큼 만들어보면서 많이 생각해보고 들여다보면서 익히는 좋은 계기였다!
'BackEnd > Node.js' 카테고리의 다른 글
[Node.js] NestJS - HTTP module (return type Observable -> Promise) (0) | 2022.11.20 |
---|---|
[Node.js] 모듈 exports하고 imports해서 다른모듈 사용하기 (0) | 2022.07.01 |