이번 포스팅에서는 Socket.IO를 이용해 익명 채팅을 만들어보겠다!
이전 포스팅에서 웹 소켓과 Socket.IO에 대한 기본적인 설명과 예제를 적어두었으니 보고 오면 좋다.
책 Node.js 교과서(개정 2판) 책의 12장의 내용을 참고했다.
+모든 코드는 github주소에 있다.
개발 환경
주의: 소켓은 버전에 따라 에러를 많이 내므로, 본 포스팅에 있는 버전과 일치하지 않는 경우 에러가 발생할 수 있음!
Git [package.json
]
{
"name": "gif-chat",
"version": "1.0.0",
"main": "app.js",
"scripts": {
"start": "nodemon app"
},
"author": "delay100",
"license": "ISC",
"dependencies": {
"axios": "^0.26.0",
"color-hash": "^2.0.1",
"cookie-parser": "^1.4.6",
"cookie-signature": "^1.2.0",
"dotenv": "^16.0.0",
"express": "^4.17.3",
"express-session": "^1.17.2",
"mongoose": "^6.2.3",
"morgan": "^1.10.0",
"multer": "^1.4.4",
"nunjucks": "^3.2.3",
"socket.io": "^2.4.1",
"ws": "^8.5.0"
},
"devDependencies": {
"nodemon": "^2.0.3"
},
"description": ""
}
보안을 위해 MongoDB의 ID와 PASSWORD는 .env
파일로 분리했다. (.env 파일이 없다면 생성해주자)
Git [schemas/index.js
] - 몽구스와 몽고디비 연결
// mongodb와 연결
const mongoose = require('mongoose');
const { MONGO_ID, MONGO_PASSWORD, NODE_ENV } = process.env; // 보안을 위해 정보 분리
const MONGO_URL = `mongodb://${MONGO_ID}:${MONGO_PASSWORD}@127.0.0.1:27017/admin`;
const connect = () => {
// 배포 환경이 아닌 경우(ex. 개발 환경) debug를 세팅함 - 몽구스가 생성하는 쿼리를 콘솔에 출력
if (NODE_ENV !== 'production') {
mongoose.set('debug', true);
}
// 몽구스와 몽고디비 연결
mongoose.connect(MONGO_URL, {
dbName: 'gifchat', // 접속을 시도하는 주소의 데이터베이스
useNewUrlParser: true, // 굳이 없어도 되는데 콘솔에 에러 뜨는 것 없애기1
// useCreateIndex: true, // 굳이 없어도 되는데 콘솔에 에러 뜨는 것 없애기2
}, (error) => {
if (error) {
console.log('몽고디비 연결 에러', error);
} else {
console.log('몽고디비 연결 성공');
}
});
};
// 이벤트 리스너
mongoose.connection.on('error', (error) => {
console.log('몽고디비 연결 에러', error); // 에러 발생 시 에러 내용을 기록하고,
});
mongoose.connection.on('disconnected', () =>{
console.log('몽고디비 연결이 끊겼습니다. 연결을 재시도합니다.'); // 연결 종료 시 재연결 시도
connect();
});
module.exports = connect;
=> useCreateIndex: true
내용은 주석을 해두었는데, 주석을 풀면 에러가 나므로 주석을 풀지 말 것!!
Git [schemas/room.js
] - 채팅방 스키마 구현
// 채팅방 스키마
const mongoose = require('mongoose');
const { Schema } = mongoose;
const roomSchema = new Schema({
title: { // 방 제목
type: String,
required: true,
},
max: { // 최대 수용 인원
type: Number,
required: true,
default: 10, // 기본적으로 10명
min: 2, // 최소 인원 2명
},
owner: { // 방장
type: String,
required: true,
},
password: String, // 비밀번호, 없어도 되므로 required 속성 필요x
createdAt: { // 생성 시간
type: Date,
default: Date.now,
},
});
module.exports = mongoose.model('Room', roomSchema);
Git [schemas/chat.js
] - 채팅 스키마 구현
// 채팅 스키마
const mongoose = require('mongoose');
const { Schema } = mongoose;
const { Types: { ObjectId }} = Schema;
const chatSchema = new Schema({
room: { // 채팅방 아이디
type: ObjectId,
required: true,
ref: 'Room', // Room 스키마와 연결해 Room 컬렉션의 ObjectId가 들어가게 됨
},
user: { // 채팅을 한 사람
type: String,
required: true,
},
// chat과 gif는 둘 중 하나만 저장하면 돼서 required 속성x
chat: String, // 채팅 내역
gif: String, // GIF 이미지 주소
createdAt: { // 채팅 시간
type: Date,
default: Date.now,
},
});
module.exports = mongoose.model('Chat', chatSchema);
Git [app.js
]
...
const connect = require('./schemas');
...
connect();
이렇게 세팅이 되었다면, MongoDB를 켜서 chats
와 rooms
컬렉션을 만들어주자!
MongoDB 명령어
DB 생성 - use [데이터베이스 명]
use gifchat
컬렉션 생성 - db.createCollection('[컬렉션명'])
> db.createCollection('[chats'])
> db.createCollection('[rooms'])
MongoDB를 다루기 어렵다면 링크한 글을 읽고 만들어보자.
부수적인 파일들을 한데 모았다.
Git [views/layout.html
]
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
<link rel="stylesheet" href="/main.css">
</head>
<body>
{% block content %}
{% endblock %}
{% block script %}
{% endblock %}
</body>
</html>
Git [views/error.html
]
{% extends 'layout.html' %}
{% block content %}
<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>
{% endblock %}
Git [public/main.css
]
* { box-sizing: border-box; }
.mine { text-align: right; }
.system { text-align: center; }
.mine img, .other img {
max-width: 300px;
display: inline-block;
border: 1px solid silver;
border-radius: 5px;
padding: 2px 5px;
}
.mine div:first-child, .other div:first-child { font-size: 12px; }
.mine div:last-child, .other div:last-child {
display: inline-block;
border: 1px solid silver;
border-radius: 5px;
padding: 2px 5px;
max-width: 300px;
}
#exit-btn { position: absolute; top: 20px; right: 20px; }
#chat-list { height: 500px; overflow: auto; padding: 5px; }
#chat-form { text-align: right; }
label[for='gif'], #chat, #chat-form [type='submit'] {
display: inline-block;
height: 30px;
vertical-align: top;
}
label[for='gif'] { cursor: pointer; padding: 5px; }
#gif { display: none; }
table, table th, table td {
text-align: center;
border: 1px solid silver;
border-collapse: collapse;
}
Git [routes/index.js
] 中 GET / 라우터 - 메인 화면 렌더링
const Room = require('../schemas/room');
...
// GET / 라우터 - 메인 화면 렌더링
router.get('/', async (req, res, next) => {
try {
const rooms = await Room.find({});
res.render('main', { rooms, title: 'GIF 채팅방'});
} catch (error) {
console.error(error);
next(error);
}
});
=2-1> render main
에 의해 views/main.html
이 실행됨
render 항목
- rooms: 모든 Room 정보
Git [views/main.html
] - http://127.0.0.1:8005/ 화면
{% extends 'layout.html' %}
{% block content %}
<h1>GIF 채팅방</h1>
<fieldset>
<legend>채팅방 목록</legend>
<table>
<thead>
<tr>
<th>방 제목</th>
<th>종류</th>
<th>허용 인원</th>
<th>방장</th>
</tr>
</thead>
<tbody>
{% for room in rooms %}
<tr data-id="{{room._id}}">
<td>{{room.title}}</td>
<td>{{'비밀방' if room.password else '공개방'}}</td>
<td>{{room.max}}</td>
<td style="color:{{room.owner}}">{{room.owner}}</td>
<td>
<button
data-password="{{'true' if room.password else 'false'}}"
data-id="{{room._id}}"
class="join-btn"
>입장
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="error-message">{{error}}</div>
<a href="/room">채팅방 생성</a>
</fieldset>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io.connect('http://127.0.0.1:8005/room', { // 네임 스페이스: 주소 뒤에 /room이 붙은 것을 말함, 서버에서 /room 네임스페이스를 통해 보낸 데이터만 받을 수 있음
path: '/socket.io',
});
// 방 추가 함수
function addBtnEvent(e) { // 방 입장 클릭 시
if (e.target.dataset.password === 'true') {
const password = prompt('비밀번호를 입력하세요');
location.href = '/room/' + e.target.dataset.id + '?password=' + password;
} else {
location.href = '/room/' + e.target.dataset.id;
}
}
document.querySelectorAll('.join-btn').forEach(function (btn) {
btn.addEventListener('click', addBtnEvent);
});
</script>
{% endblock %}
{% block script %}
<script>
window.onload = () => {
if (new URL(location.href).searchParams.get('error')) {
alert(new URL(location.href).searchParams.get('error'));
}
};
</script>
{% endblock %}
=2-2> 1. 입장
버튼, 2. 채팅방 생성
버튼
1.
입장
버튼
class="join-btn"
에 의해 아래의 javascript 코드 실행
Git [views/main.html
] 中 script 일부// 방 추가 및 입장 함수 function addBtnEvent(e) { if (e.target.dataset.password === 'true') { const password = prompt('비밀번호를 입력하세요'); location.href = '/room/' + e.target.dataset.id + '?password=' + password; } else { location.href = '/room/' + e.target.dataset.id; } } document.querySelectorAll('.join-btn').forEach(function (btn) { btn.addEventListener('click', addBtnEvent); });
=> querySelectorAll이 먼저 실행된 후 addBtnEvent가 실행됨
=2-2.1> location.href에 의해 routes/
index.js
의GET /room/:id
이 실행됨// GET /room/:id 라우터 - 렌더링 전에 방이 존재하는지, 비밀 방이면 비밀번호가 맞는지, 허용인원 초과하지 않았는지 검사 router.get('/room/:id', async (req, res, next) => { try { const room = await Room.findOne({ _id: req.params.id }); const io = req.app.get('io'); // socket.js에서 app.set('io', io)로 저장한 io 객체를 req.app.get('io')로 가져옴 if(!room) { return res.redirect('/?error=존재하지 않는 방입니다.'); } if(room.password && room.password !== req.query.password) { return res.redirect('/?error=비밀번호가 틀렸습니다.'); } const { rooms } = io.of('/chat').adapter; // io.of('/chat').adapter.rooms: 방 목록이 들어있음 if (rooms && rooms[req.params.id] && room.max <= rooms[req.params.id].length) { // io.of('/chat').adapter.rooms[req.params.id]: 해당 방의 소켓 목록이 나옴 // 소켓의 수를 세서 참가 인원의 수를 알아낼 수 있음 return res.redirect('/?error=허용 인원이 초과하였습니다.'); } const chats = await Chat.find({ room: room._id}).sort('createdAt'); // DB로부터 채팅내역을 가져옴 return res.render('chat', { room, title: room.title, chats, // chats: chats user: req.session.color, // member: rooms && rooms[req.params.id], }); } catch (error) { console.error(error); return next(error); } });
=2-2.1.> 1. render('chat')에 의해 views/
chat.html
실행 -> 4에서 설명render 항목
- room: 요청으로 들어온 room 정보
- title: room 제목
- chats: db로부터 가져온 채팅 내역
- user: 요청으로 들어온 세션의 유저(색 코드)
2.
채팅방 생성
버튼 -> 3에서 설명
->href="/room"
에 의해 routes/index.js
의GET /room
실행
main 화면 구현하기 中 2.2.2. 채팅방 생성
내용
2.
채팅방 생성
버튼
->href="/room"
에 의해 routes/index.js
의GET /room
실행
Git [routes/index.js
] 中 GET /room 라우터 - 채팅방 생성 화면 렌더링
// GET /room 라우터 - 채팅방 생성 화면 렌더링
router.get('/room', (req, res) => {
res.render('room', { title: 'GIF 채팅방 생성'});
});
=3-1> render room
에 의해 views/room.html
이 실행됨
Git [views/room.html
]
{% extends 'layout.html' %}
{% block content %}
<fieldset>
<legend>채팅방 생성</legend>
<form action="/room" method="post">
<div>
<input type="text" name="title" placeholder="방 제목">
</div>
<div>
<input type="number" name="max" placeholder="수용 인원(최소 2명)" min="2" value="10">
</div>
<div>
<input type="password" name="password" placeholder="비밀번호(없으면 공개방)">
</div>
<div>
<button type="submit">생성</button>
</div>
</form>
</fieldset>
{% endblock %}
=3-2> 1. 생성
버튼
1.
생성
버튼
- type이 submit이기 때문에 form의 action이 실행됨(method가 POST)
-> routes/index.js
에서 POST/room
실행Git [routes/
index.js
] 中 POST /room// POST /room 라우터 - 채팅방을 만들기 router.post('/room', async (req, res, next) => { try { const newRoom = await Room.create({ title: req.body.title, max: req.body.max, owner: req.session.color, password: req.body.password, }); const io = req.app.get('io'); // socket.js에서 app.set('io', io)로 저장한 io 객체를 req.app.get('io')로 가져옴 io.of('/room').emit('newRoom', newRoom); // /room 네임스페이스에 연결한 모든 클라이언트에 데이터를 보내는 메서드, of 메서드: Socket.IO에 다른 네임스페이스를 부여하는 메서드 // 네임스페이스가 없는 경우에는 io.emit 메서드로 모든 클라이언트에 데이터를 보낼 수 있음 // GET / 라우터에 접속한 모든 클라이언트가 새로 생성된 채팅방에 대한 데이터를 받을 수 있음 res.redirect(`/room/${newRoom._id}?password=${req.body.password}`); } catch (error) { console.error(error); next(error); } });
=3-2.1.> 1. 서버에서 클라이언트로 socket.IO 연결, 2. res.redirect에 의해 주소가 http://127.0.0.1:8005/room/생성된방id/?password=비밀번호 로 이동 -> 2.2.1에서 설명
1. 서버에서 클라이언트로 socket.IO 연결
Git [socket.js
] 中 room 네임스페이스const io = new SocketIO(server, { path: '/socket.io' }); // socket.io를 불러와 express와 연결 // SocketIO의 두 번째 인수로 옵션 객체를 넣어 서버에 관한 여러가지 설정 가능 // path: 클라이언트가 접속할 경로 설정(클라이언트에서도 이 경로와 일치하는 path를 넣어야 함) app.set('io', io); // 라우터에서 io 객체를 쓸 수 있게 저장, req.app.get('io')로 접근 가능 const room = io.of('/room'); // of 메서드: Socket.IO에 다른 네임스페이스를 부여하는 메서드, 같은 네임스페이스끼리만 데이터 전달 // 웹 소켓 연결 후 이벤트 리스너를 붙힘 // io(room, chat)와 socket객체가 Socket.IO의 핵심임 // room 네임스페이스 room.on('connection', (socket) => { // 이벤트리스너를 붙혀줌 // connection: 클라이언트가 접속했을 때 발생, 콜백으로 소켓 객체(socket) 제공 console.log('room 네임스페이스에 접속'); socket.on('disconnect', () => { console.log('room 네임스페이스 접속 해제'); }); });
채팅방을 생성했을 때, 채팅방에 입장했을 때를 구현해보자!
=2-2.1.> 1. render('chat')에 의해 views/
chat.html
실행 -> 4에서 설명render 항목
- room: 요청으로 들어온 room 정보
- title: room 제목
- chats: db로부터 가져온 채팅 내역
- user: 요청으로 들어온 세션의 유저(색 코드)
Git [views/chat.html
]
{% extends 'layout.html' %}
{% block content %}
<h1>{{title}}</h1>
<a href="/" id="exit-btn">방 나가기</a>
<!-- <div class="member_list">참여자 수: </div> -->
<fieldset>
<!-- 채팅 내용 -->
<legend>채팅 내용</legend>
<div id="chat-list">
{% for chat in chats %}
{% if chat.user === user %}
<div class="mine" style="color:{{chat.user}}"> <!-- mine: 내 메세지 -->
<div>{{chat.user}}</div>
{% if chat.gif %}
<img src="/gif/{{chat.gif}}">
{% else %}
<div>{{chat.chat}}</div>
{% endif %}
</div>
{% elif chat.user === 'system' %}
<div class="system"> <!-- system: 시스템 메세지 -->
<div>{{chat.chat}}</div>
</div>
{% else %}
<div class="other" style="color:{{chat.user}}"> <!-- other: 남의 메세지 -->
<div>{{chat.user}}</div>
{% if chat.gif %}
<img src="/gif/{{chat.gif}}">
{% else %}
<div>{{chat.chat}}</div>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
</fieldset>
<!-- 채팅 전송 -->
<form action="/chat" id="chat-form" method="post" enctype="multipart/formdata">
<label for="gif">GIF 올리기</label>
<input type="file" id="gif" name="gif" accept="img/gif">
<input type="text" id="chat" name="chat">
<button type="submit">전송</button>
</form>
<script>
// 채팅을 전송하는 폼에 submit 이벤트 리스너 추가
document.querySelector('#chat-form').addEventListener('submit', function (e) {
e.preventDefault();
if (e.target.chat.value) {
axios.post('/room/{{room._id}}/chat', {
chat: this.chat.value,
})
.then(() => {
e.target.chat.value = '';
})
.catch((err) => {
console.error(err);
});
}
});
</script>
{% endblock %}
=4-1> 1. 방 나가기
버튼, 2. 채팅 전송
버튼
1.
방 나가기
버튼
href="/"
에 의해 http://127.0.0.1:8005/ 로 이동 -> 2 참고
2. 채팅
전송
버튼
- type=submit에 의해 form의 action이 실행됨
- action은
/chat
인데 POST 방식
-> routes/index.js
의 POST/room/:id/chat
실행Git [routes/
index.js
] 中 /room/:id/chat// POST /room/:id/chat - 방 접속 시 기존 채팅 내역을 불러오도록 하는 라우터 // 채팅을 할 때마다 채팅 내용이 이 라우터로 전송되고, 라우터에서 다시 웹 소켓으로 메세지를 보냄 router.post('/room/:id/chat', async (req, res, next) => { try { const chat = await Chat.create({ // 채팅을 db에 저장 room: req.params.id, user: req.session.color, chat: req.body.chat, }); req.app.get('io').of('/chat').to(req.params.id).emit('chat', chat); // 같은 방(req.params.id)에 들어있는 소켓들에게 메시지 데이터를 전송 res.send('ok'); } catch (error) { console.error(error); next(error); } });
=4-2> 1. res.send('ok') - 응답으로 ok 보냄, 2. 라우터에서 서버로 socket.IO 연결
2. 라우터에서 서버로 socket.IO 연결
네임스페이스 명(of):/chat
방 아이디(to):req.params.id
이벤트 명 - 데이터 명(emit):chat
- chat
Git [views/chat.html
] 中 gif 전송 부분
<!-- 채팅 전송 -->
<form action="/chat" id="chat-form" method="post" enctype="multipart/formdata">
<label for="gif">GIF 올리기</label>
<input type="file" id="gif" name="gif" accept="img/gif">
<input type="text" id="chat" name="chat">
<button type="submit">전송</button>
</form>
<!-- gif 의 input이 바뀌면 script의 #gif 실행 -->
<script>
// 프런트 화면에서 이미지를 선택해 업로드하는 이벤트 리스너
document.querySelector('#gif').addEventListener('change', function (e) {
console.log(e.target.files);
const formData = new FormData();
formData.append('gif', e.target.files[0]);
axios.post('/room/{{room._id}}/gif', formData)
.then(() => {
e.target.file = null;
})
.catch((err) => {
console.error(err);
});
});
</script>
=5-1> axios.post로 인해 POST /room/방 아이디/gif
실행
Git [routes/index.js
] 中 /room/방 아이디/gif
try {
fs.readdirSync('uploads');
} catch (err) {
console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
fs.mkdirSync('uploads');
}
const upload = multer({
storage: multer.diskStorage({
destination(req, file, done) {
done(null, 'uploads/');
},
filename(req, file, done) {
const ext = path.extname(file.originalname);
done(null, path.basename(file.originalname, ext) + Date.now() + ext); // 파일명에 타임스탬프(Date.now())를 붙힘
},
}),
limits: { fileSize: 5 * 1024 * 1024}, // 용량을 5MB로 제한
});
// POST /room/{{room._id}}/gif - views/chat.html에서 보낸 gif내용 처리
router.post('/room/:id/gif', upload.single('gif'), async (req, res, next) => {
try {
const chat = await Chat.create({
room: req.params.id,
user: req.session.color,
gif: req.file.filename,
});
req.app.get('io').of('/chat').to(req.params.id).emit('chat', chat);
res.send('ok');
} catch (error) {
console.error(error);
next(error);
}
});
=5-2> 1. res.send('ok') - 응답으로 ok 보냄, 2. 라우터에서 서버로 socket.IO 연결
2. 라우터에서 서버로 socket.IO 연결
네임스페이스 명(of):/chat
방 아이디(to):req.params.id
이벤트 명 - 데이터 명(emit):chat
- chat
Git [socket.js
] 中 chat 네임스페이스
// chat 네임스페이스
chat.on('connection', (socket) => { // 이벤트리스너를 붙혀줌
console.log('chat 네임스페이스에 접속');
const req = socket.request;
const { headers: { referer }} = req; // socket.request.headers.referer: 현재 웹 페이지의 URL 가져올 수 있음, referer: 하이퍼링크를 통해서 각각의 사이트로 방문시 남는 흔적
const roomId = referer // roomId로 같은 채팅방에 있는 사람인지 구분
.split('/')[referer.split('/').length -1]
.replace(/\?.+/, ''); // split과 replace로 방의 id를 가져옴
socket.join(roomId); // 방의 id를 인수로 받음
// chat 네임스페이스에 접속 시 socket.join 메소드 실행 - 방에 들어가는 메서드
socket.to(roomId).emit('join', { // socket.to(방 아이디) 메서드: 특정 방에 데이터를 보낼 수 있음
user: 'system',
chat: `${req.session.color}님이 입장하셨습니다.`,
});
// 접속 해제 시
socket.on('disconnect', () => {
console.log('chat 네임스페이스 접속 해제');
socket.leave(roomId); // chat 네임스페이스에 접속 해제 시 socket.join 메소드 실행 - 방에서 나가는 메서드
// 연결이 끊기면 자동으로 방에서 나가지만, 확실히 하기 위해 추가
const currentRoom = socket.adapter.rooms[roomId]; // socket.adapter.rooms[방 아이디]: 참여 중인 소켓 정보가 들어 있음
const userCount = currentRoom ? currentRoom.length : 0;
if (userCount === 0) { // 방에 인원수가 0명인 경우
const signedCookie = req.signedCookies['connect.sid']; // req.signedCookies 내부의 쿠키들은 모두 복호화되어 있으므로 다시 암호화해서 요청에 담아보내야 함
const connectSID = cookie.sign(signedCookie, process.env.COOKIE_SECRET);
axios.delete(`http://localhost:8005/room/${roomId}`, { // 서버에서 axios 요청 시 요청자가 누구인지에 대한 정보가 없음 (브라우저는 자동으로 쿠키를 같이 넣어 보냄)
// express-session에서는 세션 쿠키인 req.signedCookies['connect.sid']를 보고 현재 세션이 누구에게 속해있는지 판단함
headers: {
Cookie: `connect.sid=s%3A${connectSID}`, // s%3A 뒷 내용이 암호화된 내용, DELETE /room/:id 라우터에서 요청자가 누군지 확인 가능
},
})
.then(() => {
console.log('방 제거 요청 성공');
})
.catch((error) => {
console.error(error);
});
} else { // 방에 인원수가 0명이 아닌 경우 - 방에 있는 사람에게 퇴장 메세지 보냄(system)
socket.to(roomId).emit('exit', {
user: 'system',
chat: `${req.session.color}님이 퇴장하셨습니다.`,
});
}
});
=5-3> 1. usercount가 0이면 delete /room/방 아이디
실행, 2. usercount가 0이 아니면 서버에서 클라이언트로 socket.IO 연결
1. usercount가 0이면
delete /room/방 아이디
실행
Git [routes/index.js
]
방 아이디(to):room
이벤트 명 - 데이터 명(emit):removeRoom
- removeRoom// DELETE /room/:id - 채팅방을 삭제하는 라우터 router.delete('/room/:id', async (req, res, next) => { try { await Room.remove({ _id: req.params.id }); await Chat.remove({ room: req.params.id }); res.send('ok'); setTimeout(() => { // 2초뒤에 req.app.get('io').of('/room').emit('removeRoom', req.params.id); // 웹 소켓으로 /room 네임스페이스에 방이 삭제되었음(removeRoom)을 알림 }, 2000); } catch (error) { console.error(error); next(error); } });
2. usercount가 0이 아니면 서버에서 클라이언트로 socket.IO 연결
방 아이디(to):roomId
이벤트 명 - 데이터 명(emit):exit
- exit
+참고) roomId = socket.request.headers.referer: 현재 웹 페이지의 URL 가져올 수 있음Git [views/
chat.html
] 中 클라이언트 측 socketexit
연결 부분// 사용자의 퇴장을 알리는 메시지 표시 socket.on('exit', function(data) { // 사용자의 채팅방 퇴장에 대한 데이터를 웹 소켓에 전송될 때 실행됨, 소켓에 exit 이벤트 리스너 연결 const div = document.createElement('div'); div.classList.add('system'); // div에 class=`system`속성 추가 const chat = document.createElement('div'); div.textContent = data.chat; div.appendChild(chat); document.querySelector('#chat-list').appendChild(div); });
** mongoose 사용 시 mongoDB서버를 꼭 켜줘야 함
입력(cmd)
C:\Program Files\MongoDB\Server\5.0\bin> mongod --auth
http://127.0.0.1:8005/room - 공개방 생성 중
http://127.0.0.1:8005/room - 공개방 생성 완료
http://127.0.0.1:8005/room/방id - 채팅 화면(GIF 가능)
잘못된 정보 수정 및 피드백 환영합니다!!