0. 들어가며
앞서 채팅편을 보지 않으셨다면 먼저 보고 오시는 것을 추천합니다.
1. Socket IO
Socket IO는 실시간, 양방향, event 기반 통신을 제공해주는 framework이다.
webSocket과 같은 역할을 하는 것처럼 보이는데, 브라우저가 webSocket을 지원하지 않아도 socket IO는 다른 방법을 사용해서 기능을 제공한다.
좀 더 넓은 범위에서 사용할 수 있다는 뜻이다.
정리하면,
- websocket의 부가기능이 아닌, 필요에 따라 Socket IO가 websocket을 사용하거나 안 할 수 있다.
- 프론트와 백엔드 간 실시간 통신을 가능하게 해주는 프레임워크 혹은 라이브러리다.
- 탄력성이 있어 websocket을 이용한 연결에 실패해도 다른 방법으로 통신한다.
- websocket보다 기능이 더 많기 때문에 용량이 더 무겁다. ( 1mb 정도? )
2. Socket IO를 이용한 통신하기
yarn add socket.io
Socket IO를 사용하기 위해서 서버에 패키지를 설치해준다.
// import WebSocket from "ws";
=>
import SocketIO from "socket.io";
// ...
const server = http.createServer(app);
// const wss = new WebSocket.Server({ server });
=>
const io = SocketIO(server);
ws를 사용한 코드를 지우고, Socket IO를 사용해서 서버를 만들었다.
기존 Front-End는 ws를 기본적으로 제공하기 때문에 특별한 패키지를 설치할 필요가 없었지만,
Socket IO는 ws보다 많은 기능을 제공하기 때문에 ws 만으로는 통신을 할 수 없다.
doctype html
html(lang="en")
head
meta(charset="UTF-8")
meta(http-equiv="X-UA-Compatible", content="IE=edge")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
title Document
body
header
h1
main
script(src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.0/socket.io.js")
script(src="/public/js/socket.js")
Socket IO를 추가로 cdn 방식으로 Front-End 프로젝트에 불러왔다.
앞서 말한 것처럼 ws는 브라우저에 설치되어 있지만, Socket IO는 새롭게 추가를 해야 하기 때문에 좀 더 무겁다.
만약 서버에서 실행하는 ssr 프로젝트라면
script(src="/socket.io/socket.io.js")
백엔드에 socket.io를 설치함으로 기본적으로 제공하는 socket.io 패키지가 있어서 cdn으로 받아올 필요는 없다.
// socket.js
const socket = io(window.location.host);
프론트에서 서버와 통신하기 위해서 socket.js에 io에 매개변수로 url을 넣어주었다.
ws와는 다르게 따로 ws://같은 내용을 주소에 추가해줄 필요는 없고 서버 주소만 추가해주면 바로 통신할 수 있다.
만약 백엔드에서 실행할 경우 /socket.io/socket.io.js 경로에서 패키지를 가져왔다면,
const socket = io();
만 사용해도 자동으로 서버와 통신이 된다.
const io = SocketIO(server);
// ...
io.on("connection", (socket) => {
console.log(socket);
});
서버에서도 마찬가지로 Front-End와 통신하기 위해서 connection 키워드를 on 매서드에 넣어주면 연결될 때,
소켓 정보를 콘솔에 출력한다.
3. Socket IO를 사용해서 채팅방 만들기
doctype html
html(lang="en")
head
meta(charset="UTF-8")
meta(http-equiv="X-UA-Compatible", content="IE=edge")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
title Zoom Clone
body
header
h1 Zoom
main
div#welcome
form
input(placeholder="room name", required, type="text")
button Enter Room
//- script(src="/socket.io/socket.io.js")
script(src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.0/socket.io.js")
script(src="/public/js/socket.js")
간단한 채팅방 div를 만들어 주었다.
const socket = io(window.location.host);
const welcome = document.getElementById("welcome");
const form = welcome.querySelector("form");
function handleSubmit(e) {
e.preventDefault();
const input = form.querySelector("input");
socket.emit("room", { payload: input.value }, (msg) => {
console.log(msg);
});
input.value = "";
}
form.addEventListener("submit", handleSubmit);
서버에게 채팅방을 만들거나 만들어진 채팅방에 들어가기 위해서 form을 querySelector로 가져오고,
handleSubmit 함수를 만들었다.
여기서 특이한 부분이 있는데, ws를 사용할 땐
socket.send(" ... ");
사용해서 서버에게 메시지를 보냈는데, 방을 만들거나, 들어갈 경우 메시지는 필요 없기 때문에 emit 함수를 사용했다.
ws와 다르게 무조건 메시지를 보낼 필요가 없이 필요에 따라 event를 만들 수 있다.
예를 들어, "delete", "apple", "banana" 등등의 명칭으로도 만들 수 있다.
그리고 두 번째 매개변수로 서버에 넘길 데이터를 줄 수 있다.
여기서 또 ws와 다른 점이 있는데, ws는 object를 받을 수 없어 string으로 변환한 뒤에 넘겨줬는데,
Socket IO는 그럴 필요 없이 object를 바로 전달할 수 있다.
3번째로 emit()의 3번째 매개변수로 함수가 전달되고 있다.
socket.emit("room", { payload: input.value }, (msg) => {
console.log(msg);
});
이 함수는 서버에 그대로 전달되며, 필요에 따라 서버에서 호출하면 Front-End에서 실행이 된다.
정말 특이한 방법으로 작업이 완료될 때라던지, 언제든 서버에서 해당 함수를 호출해서 Front-End에서 실행할 수 있다.
const io = SocketIO(server);
io.on("connection", (socket) => {
socket.on("room", (msg) => {
console.log(msg);
setTimeout(() => {
done("Server Done!");
}, 1000);
});
});
서버에서도 "room"으로 Front-End에서 전달한 데이터를 받을 수 있다.
그리고 Front-End에서 넘겨준 3번째 함수를 호출하면, Front-End에서 호출된 함수가 실행된다.
정리하자면,
- 서버에 전달할 때 반드시 메시지를 보낼 필요 없이 필요에 따라 event를 만들어줄 수 있다.
- object를 전달할 때 string으로 변환할 필요 없이 바로 넣어서 전달할 수 있다.
- 필요에 따라 3번째 매개변수로 함수 등을 전달할 수 있는데, 서버에서 호출 시 Front-End에서 실행된다.
4. Socket IO를 사용해서 채팅방 안에서 통신하기
io.on("connection", (socket) => {
socket.on("room", (msg) => {
console.log(msg);
socket.join(msg);
setTimeout(() => {
done("Server Done!");
}, 1000);
});
});
Socket IO는 기본적으로 그룹(Room)을 만드는 기능을 제공한다. 그것도 아주 간단하게..
socket.join("명칭");
을 사용하면 간단하게 그룹을 만들 수 있다.
" 명칭 " 외에도 [ "명칭 1", "명칭 2" ] 방식으로 배열을 사용하면 여러 곳의 그룹에 동시에 들어갈 수 있다.
소켓이 접속한 그룹은
console.log(socket.rooms)
통해서 조회가 가능하다.
그리고 방을 나갈 수 있는데,
socket.leave("명칭");
사용해서 그룹을 나갈 수 있다.
// ...
body
header
h1 Zoom
main
div#welcome
form
input(placeholder="room name", required, type="text")
button Enter Room
div#room
h3 Title
ul
form
input(placeholder="message", required, type="text")
button Send
// ...
본격적으로 채팅방을 만들기 위해서 room이라는 div를 만들어주고 안에 입력 폼과 전송 버튼을 만들었다.
// ...
const room = document.getElementById("room");
room.hidden = true;
// ...
그리고 방에 들어가기 전까진 보이면 안 되기 때문에 hidden true 옵션을 주었다.
let roomName;
function showRoom() {
welcome.hidden = true;
room.hidden = false;
const h3 = room.querySelector("h3");
h3.innerText = `Room ${roomName}`;
}
function handleSubmit(e) {
e.preventDefault();
const input = form.querySelector("input");
socket.emit("room", { payload: input.value }, showRoom);
roomName = input.value;
input.value = "";
}
방을 접속했을 때는 room이 보이고, 채팅방 생성 및 입장 form이 사라져야 해서 showRoom을 만들고 넘겨줬다.
그리고 방 제목을 기억했다가 showRoom에서 방에 접속하면 Title을 변경해준다.
io.on("connection", (socket) => {
socket.on("room", (msg, showRoom) => {
console.log(msg);
socket.join(msg);
showRoom();
});
});
서버에선 방 생성 작업이 완료되면, showRoom을 호출해서 Front-End에 showRoom 함수가 실행된다.
5. Socket IO를 사용해서 채팅방에서 접속 시 메시지 보내기
io.on("connection", (socket) => {
socket.on("room", (msg, showRoom) => {
console.log(msg);
socket.join(msg);
showRoom();
socket.to(msg).emit("welcome");
});
});
생성된 방의 모든 사람에게 통신을 할 때는 to 함수를 사용하면 된다.
socket.to("그룹명").emit(' ~ ');
to 안에 그룹명을 적으면 해당 그룹 전체에게 특정한 작업을 할 수 있는데,
그룹명에 다른 사용자의 Socket ID를 넣는다면, 특정 사용자에게만 전달되는 개인 메시지 용도로 사용할 수 있다.
※ Socket ID는 socket.rooms를 하면 확인할 수 있다.
// ...
function addMessage(message) {
const ul = room.querySelector("ul");
const li = document.createElement("li");
li.innerText = message;
ul.appendChild(li);
}
socket.on("welcome", () => {
addMessage("Someone joined!");
});
Front-End도 마찬가지로 socket을 통해서 welcome 이벤트를 받아오고, 왔을 경우 메시지를 출력해준다.
6. Socket IO를 사용해서 메시지 보내기
io.on("connection", (socket) => {
socket.on("room", (msg, showRoom) => {
socket.join(msg);
showRoom();
socket.to(msg).emit("welcome");
});
socket.on("disconnecting", () => {
socket.rooms.forEach((room) => socket.to(room).emit("bye"));
});
});
socket의 " disconnecting " 이벤트는 사용자와 연결이 끊어지기 직전에 발생하는 이벤트이다.
" disconnect " 이벤트는 연결이 끊어지면 발생하는 이벤트로 둘은 다른 이벤트이다.
즉, 사용자와 연결이 끊어지기 직전에 호출되어서 접속한 그룹을 반복해서 접속한 방에 " bye " 이벤트를 발생시킨다.
// ...
socket.on("welcome", () => {
addMessage("Someone joined!");
});
socket.on("bye", () => {
addMessage("Someone left..");
});
welcome과 마찬가지로 bye 이벤트를 만들어 사용자들에게 뿌려준다.
// ...
function handleMessageSubmit(e) {
e.preventDefault();
const input = room.querySelector("input");
socket.emit("new_message", input.value, roomName, () => {
addMessage(`You: ${input.value}`);
input.value = "";
});
}
function showRoom() {
welcome.hidden = true;
room.hidden = false;
const h3 = room.querySelector("h3");
h3.innerText = `Room ${roomName}`;
const form = room.querySelector("form");
form.addEventListener("submit", handleMessageSubmit);
}
// ...
사용자가 메시지를 보낼 때 발생하는 이벤트를 처리한다.
" new_message "라는 이벤트를 만들고 작성한 내용을 넘겨준다. 그리고 socket.to 함수는 자신에게는 발생하지
않기 때문에 addMessage를 사용해 서버에게 전송하면 추가해준다.
또한 어느 방에서 보낸 메시지인지도 알아야 하기 때문에 방 이름도 함께 보내준다.
io.on("connection", (socket) => {
socket.on("room", (msg, showRoom) => {
socket.join(msg);
showRoom();
socket.to(msg).emit("welcome");
});
socket.on("disconnecting", () => {
socket.rooms.forEach((room) => socket.to(room).emit("bye"));
});
socket.on("new_message", (msg, room, done) => {
socket.to(room).emit("new_message", msg);
done();
});
});
서버에서 " new_message"에 대한 이벤트를 받아서 방에 있는 모든 사용자에게 메시지를 넘겨주고,
done 함수를 호출해서 전송한 사용자에게의 addMessage를 실행한다.
// ...
socket.on("bye", () => {
addMessage("Someone left..");
});
socket.on("new_message", (msg) => {
addMessage(msg);
});
이제 마지막으로 사용자에게 전달받은 " new_message " 이벤트에 대한 처리를 한다.
7. NickName 추가하기
div#room
h3 Title
ul
form#name
input(placeholder="nickname", required, type="text")
button Save
form#message
input(placeholder="message", required, type="text")
button Send
방 안에서 message 말고 nickname을 추가로 입력할 수 있는 form을 하나 만들어주고, 각 form에 id를 추가했다.
function handleMessageSubmit(e) {
e.preventDefault();
const input = room.querySelector("#message input");
socket.emit("new_message", input.value, roomName, () => {
addMessage(`You: ${input.value}`);
input.value = "";
});
}
function handleNickNameSubmit(e) {
e.preventDefault();
const input = room.querySelector("#name input");
socket.emit("nickname", input.value, roomName, () => {
input.value = "";
});
}
function showRoom() {
welcome.hidden = true;
room.hidden = false;
const h3 = room.querySelector("h3");
h3.innerText = `Room ${roomName}`;
const msgForm = room.querySelector("#message");
msgForm.addEventListener("submit", handleMessageSubmit);
const nameForm = room.querySelector("#name");
nameForm.addEventListener("submit", handleNickNameSubmit);
}
마찬가지로 form 이 추가된 만큼 거기에 맞춰 showRoom 함수를 변경하였다.
io.on("connection", (socket) => {
socket["nickname"] = "Anon";
// ...
socket.on("nickname", (nickname) => {
socket["nickname"] = nickname;
});
});
ws에서 사용한 것처럼 socket ["nickname"]을 전달받은 nickname으로 변경했다.
이렇게 변경하면 이제 사용자가 접속했을 때, 접속 종료할 때, 메시지 보낼 때 닉네임을 추가해서 전달할 수 있다.
socket.on("room", (msg, showRoom) => {
socket.join(msg);
showRoom();
socket.to(msg).emit("welcome", socket["nickname"]);
});
socket.on("disconnecting", () => {
socket.rooms.forEach((room) =>
socket.to(room).emit("bye", socket["nickname"])
);
});
socket.on("new_message", (msg, room, done) => {
socket.to(room).emit("new_message", `${socket["nickname"]}: ${msg}`);
done();
});
맞춰서 welcome과 bye에는 nickname을 바로 보내주고, message는 메시지에 직접 넣어서 처리할 수 있다.
8. 깃허브
https://github.com/SeoJaeWan/zoom-clone
'Node.js > 실험실' 카테고리의 다른 글
[Node.js] Express ORM 세팅해보기 (0) | 2022.10.09 |
---|---|
[Node.js] Express에 Webpack 구현하기 (1) | 2022.09.28 |
[Node.js] Express set "views" (2) | 2022.09.26 |
[Node.js] Zoom 클론코딩 - 채팅편 (1) | 2022.06.08 |
[Node.js] Zoom 클론코딩 - 세팅편 (0) | 2022.06.07 |