프로젝트를 하면서 핵심기능 중 하나인 실시간 채팅, 채팅방을 구현해보았다.
socket.io를 사용했고, 전에 알림이나, 소캣을 이용해 같은 페이지에 몇 명이 같이 보고 있는지를 보여주기 위해 소캣을 사용해 본 적은 있었다.(강의 내용) 하지만, 실시간 채팅은 한 번도 해본 적이 없었는데 생각보다 간단한 것 같다.
간단하게 소캣을 프론트와 백 둘 다 연결하고, 소캣 안에서 메시지를 주면 서버에서 받아서 모두에게 뿌려주면 끝이다.
말은 간단하지만, 적용이 정말 쉬운 건 아니다. 나 같은 경우 파일을 분리하는 곳에서 조금 애를 먹었고, 채팅방처럼 방에서 말한 건 방 사람들끼리 공유가 되어야 하고, 채팅 내용을 저장해서 잠깐 나갔다 들어와도 전에 나누었던 채팅들을 볼 수 있게 해야 했기 때문에 쉽지만은 않았다.
그래도 이번 기회에 배운 거를 정리해서 내 것으로 완벽하게 만들어 보려고 한다.
실제 배포과정에서는 프론트는 리액트와 연결해서 사용했지만, 나는 혼자 바닐라스크립트로 연습 정도만 했기 때문에 완벽하게 호환이 되지는 않는 점 참고해야 한다.
또한, 디비를 사용해서 프라이머리 키를 사용해야 해서 바닐라스크립트엔 하드코딩도 들어가 있으니 참고해야 한다.
실시간 채팅 / 채팅방 구현
1. socket.io 설치
npm install socket.io
2. app.js / socket.js / index.html 전체 코드
// app.js
const express = require('express');
const Router = require('./routes/index');
const webSocket = require('./socket');
require('dotenv').config();
const port = process.env.PORT;
const app = express();
const path = require('path');
app.use(express.static(path.join(__dirname, 'src')));
app.use(express.json());
app.use('/api', Router);
app.get('/', (req, res) => {
res.status(200).json({ massage: '연동 잘 됨.' });
});
const server = app.listen(port, () => {
console.log(port, '포트로 서버가 열렸어요!');
});
webSocket(server, app);
module.exports = app;
// socket.js
// const app = require('./app');
const socket = require('socket.io');
// const http = require('http');
const { Room, Chat, User, Participant } = require('./models');
// require('socket.io-client')('http://localhost:3000');
// const server = http.createServer(app);
module.exports = (server, app) => {
const io = socket(server, {
cors: {
origin: '*',
credentials: true,
},
});
app.set('socket.io', io);
// 소캣 연결
io.on('connection', (socket) => {
console.log('a user connected');
// 채팅방 목록? 접속(입장전)
socket.on('join-room', async (data) => {
let { roomKey, userKey } = data;
const enterUser = await Participant.findOne({
where: { roomKey, userKey },
include: [
{ model: User, attributes: ['nickname'] },
{ model: Room, attributes: ['title'] },
],
});
// 해당 채팅방 입장
socket.join(enterUser.Room.title);
const enterMsg = await Chat.findOne({
where: {
roomKey,
userKey: 12,
chat: `${enterUser.User.nickname}님이 입장했습니다.`,
},
});
// 처음입장이라면 환영 메세지가 없을테니
if (!enterMsg) {
await Chat.create({
roomKey,
userKey: 12, // 관리자 유저키
chat: `${enterUser.User.nickname}님이 입장했습니다.`,
});
// 관리자 환영메세지 보내기
let param = { nickname: enterUser.User.nickname };
io.to(enterUser.Room.title).emit('welcome', param);
} else {
// 재입장이라면 아무것도 없음
}
});
// 채팅 받아서 저장하고, 그 채팅 보내서 보여주기
socket.on('chat_message', async (data) => {
let { message, roomKey, userKey } = data;
const newChat = await Chat.create({
roomKey,
userKey,
chat: message,
});
const chatUser = await Participant.findOne({
where: { roomKey, userKey },
include: [
{ model: User, attributes: ['nickname'] },
{ model: Room, attributes: ['title'] },
],
});
// 채팅 보내주기
let param = {
message,
roomKey,
nickname: chatUser.User.nickname,
time: newChat.createdAt, // (9시간 차이나는 시간)
};
io.to(chatUser.Room.title).emit('message', param);
});
// 채팅방 나가기(채팅방에서 아에 퇴장)
socket.on('leave-room', async (data) => {
let { roomKey, userKey } = data;
const leaveUser = await Participant.findOne({
where: { roomKey, userKey },
include: [
{ model: User, attributes: ['nickname'] },
{ model: Room, attributes: ['title', 'userKey'] },
],
});
// 호스트가 나갔을 때
if (userKey === leaveUser.Room.userKey) {
let param = { nickname: leaveUser.User.nickname };
socket.broadcast.to(leaveUser.Room.title).emit('byeHost', param);
} else {
// 일반유저가 나갔을 때(호스트X)
await Chat.create({
roomKey,
userKey: 12, // 관리자 유저키
chat: `${leaveUser.User.nickname}님이 퇴장했습니다.`,
});
let param = { nickname: leaveUser.User.nickname };
io.to(leaveUser.Room.title).emit('bye', param);
}
});
});
};
// ./src/index.html
<!DOCTYPE html>
<html>
<head>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font: 13px Helvetica, Arial;
}
form {
background: #000;
padding: 3px;
position: fixed;
bottom: 0;
width: 100%;
}
form input {
border: 0;
padding: 10px;
width: 90%;
margin-right: 0.5%;
}
form button {
width: 9%;
background: rgb(130, 224, 255);
border: none;
padding: 10px;
}
#messages {
list-style-type: none;
margin: 0;
padding: 0;
}
#messages li {
padding: 5px 10px;
}
#messages li:nth-child(odd) {
background: #eee;
}
</style>
</head>
<body>
<select>
<option value="Room1">Room1</option>
<option value="Room2">Room2</option>
</select>
<ul id="messages"></ul>
<form action="">
<input id="m" autocomplete="off" />
<button>Send</button>
</form>
<script src="/socket.io/socket.io.js"></script>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script>
$(() => {
const name = prompt('What your name');
const socket = io();
let room = ['room1', 'room2'];
let num = 0;
// let param = { roomKey: 2, userKey: 5 };
socket.emit('join-room', (param = { roomKey: 2, userKey: 1 }));
$('select').change(() => {
socket.emit('leave-room', (param = { roomKey: 2, userKey: 1 }));
num++;
num = num % 2;
socket.emit('join-room', (param = { roomKey: 6, userKey: 1 }));
});
$('form').submit(() => {
param = { message: $('#m').val(), roomKey: 2, userKey: name };
socket.emit('chat_message', param);
$('#m').val('');
return false;
});
socket.on('message', (data) => {
console.log(data);
$('#messages').append(
$('<li>').text(data.nickname + ' : ' + data.message)
);
});
socket.on('bye', (num, name) => {
$('#messages').append(
$('<li>').text(name + ' leaved ' + room[num] + ' :(')
);
});
socket.on('byeHost', (data) => {
$('#messages').append(
$('<li>').text(data.nickname + ' leaved ' + ' :(')
);
});
socket.on('welcome', (num, name) => {
$('#messages').append(
$('<li>').text(name + ' joined ' + room[num] + ':)')
);
});
});
</script>
</body>
</html>
index.html의 script부분의 첫 번째 줄은 socket.io, 두 번째 줄은 jquery를 사용하기 위해서 적어주는 코드이다.
전체 코드를 기반으로 하나씩 정리해보겠다.
솔직히 말해서 아직 socket.js로 파일 분리해서 모듈을 불러와서 사용하고 있는 것 같은데 이 부분은 정확히 어떻게 동작이 되고 있는지는 잘 모르겠다. 여러 블로그를 보면서 시행착오를 거치면서 서버가 정상 동작하고, 배포했을 때 문제가 없어서 이대로 사용하고 있다.
소캣이 실행되는 부분보다는 로직과 메서드, 이벤트에 대해서 정리해보려고 한다.
일단 소캣 통신의 기본은 통신을 보낸다. 받는다 그걸 또 보낸다. 받는다
ex) 클라든 서버든 socket.emit.("event")로 보내면 socket.on."event"로 받는다.
기본 메서드
io.emit() : 연결되어있는 모든 클라이언트에게 전송한다.
socket.broadcast.emit() : 메시지를 전송한 클라이언트를 제외하고 나머지 모든 클라이언트에게 전송한다.
socket.emit() : 서버에 메시지를 전송한 클라이언트에게만 전송한다.
io.to(id). emit() : 귓속말(채팅방)
기본 이벤트
connection, disconnect처럼 기본 이벤트가 존재한다.
message처럼 직접 만든 이벤트도 존재한다.
message이벤트의 경우 이 이벤트가 발생했을 시 해당 이벤트에 속하는 부분들이 실행된다.
이벤트 명은 서버와 프론트가 같은 것을 사용해야 서로 통신이 가능하다.(api URL과 비슷한 느낌)
내가 구현한 socket.js의 경우
이렇게 채팅방을 구현하고, 그 안에서만 통신이 되게 했으며, 채팅 내용도 저장해서 api를 이용해 채팅방에 들어오면 지금까지 있었던 채팅 내용들을 불러와서 카카오톡처럼 최대한 구현해보았다. 아직 이해가지 않는 부분도 많지만, 실시간 채팅 자체만은 어렵지 않은 것 같다.
'coding > Node.js' 카테고리의 다른 글
로그 남기기 2 - morgan 사용 (0) | 2022.09.10 |
---|---|
로그 남기기 1 - winston 사용 (0) | 2022.09.09 |
소켓 / 웹소켓 (0) | 2022.09.05 |
에러 핸들러 (0) | 2022.09.03 |
Sequelize Op like - 검색 기능 (0) | 2022.08.23 |