const webSocket = require('./socket') // 루트 폴더의 socket.js
/* express server configurations */
const server = app.listen(port, /*...*/);
webSocket(server, app, sessionMiddleware)
// webSocket 함수안에서 서버를 실행하고, app객체와 세션활용을 위한 미들웨어를 전달한다.
const SocketIO = require('socket.io');
const axios = require('axios');
const cookieParser = require('cookie-parser');
const cookie = require('cookie-signature');
module.exports = (server, app, sessionMiddleware) => {
const io = SocketIO(server, { path: '/socket.io' }); // 클라이언트에서 라이브러리에 접근할 수 있게 해주는 주소
app.set('io', io);
const room = io.of('/room');
const chat = io.of('/chat');
// 미들웨어 chat 소켓라우터에 넣기 io객체에 직접 넣으면 작동하지 않는다.
chat.use((socket, next) => cookieParser(process.env.COOKIE_SECRET)(socket.request, {}, next));
chat.use((socket, next) => sessionMiddleware(socket.request, {}, next));
room.on('connection', (socket) => {
console.log('room 네임스페이스에 접속');
socket.on('disconnect', () => {
console.log('room 네임스페이스 접속 해제');
});
});
chat.on('connection', (socket) => {
console.log('chat 네임스페이스에 접속');
const req = socket.request;
const { headers: { referer } } = req;
const roomId = referer
.split('/')[referer.split('/').length - 1]
.replace(/\?.+/, '');
socket.join(roomId);
socket.to(roomId).emit('join', {
user: 'system',
chat: `${req.session.color}님이 입장하셨습니다.`,
});
socket.on('disconnect', () => {
console.log('chat 네임스페이스 접속 해제');
socket.leave(roomId);
const currentRoom = socket.adapter.rooms[roomId];
const userCount = currentRoom ? currentRoom.length : 0;
if (userCount === 0) { // 유저가 0명이면 방 삭제
const signedCookie = cookie.sign(req.signedCookies['connect.sid'], process.env.COOKIE_SECRET);
const connectSID = `${signedCookie}`;
axios.delete(`http://localhost:8005/room/${roomId}`, {
headers: {
Cookie: `connect.sid=s%3A${connectSID}`
}
})
.then(() => {
console.log('방 제거 요청 성공');
})
.catch((error) => {
console.error(error);
});
} else {
socket.to(roomId).emit('exit', {
user: 'system',
chat: `${req.session.color}님이 퇴장하셨습니다.`,
});
}
});
socket.on('chat', (data) => {
socket.to(data.room).emit(data);
});
});
};
라우터를 통해 구현된 채팅의 경우 한번의 채팅입력은 다음과 같은 절차로 처리된다.
1. 브라우저 이벤트리스너가 채팅입력을 감지하면 서버로 post 요청을 날린다.
2. 서버 라우터를 통해 req.body로 채팅입력을 받고, 채팅 내역을 db에 저장한다.
3. 라우터에서 socket.js에서 미리 app객체에 넣은 req.app.get('io')
를 활용해서 'chat'이벤트를 emit한다.
4.req.app.get('io').of('/chat').to(req.params.id).emit('chat', chat)
부분이 중요한데, post요청하는 url에 포함된 param인 id를 기준으로 socket.io내부에서 라우팅하여 관련된 id에 join해 있는 소켓들에게만 이벤트를 방출하는 것
5. 브라우저 스크립트의 socket객체는 'chat'이벤트가 발생했음을 감지하고, chat이벤트에 포함된 데이터
객체인 chat을 파싱해서 화면에다 뿌린다.
chat.html 파일의 스크립트태그 안 자바스크립트
'chat'이벤트 리스너와 채팅입력 button에 대한 이벤트리스너가 있다.
socket.on('chat', function (data) {
const div = document.createElement('div')
if (data.user === '{{user}}') {
div.classList.add('mine')
} else {
div.classList.add('other')
}
const name = document.createElement('div')
name.textContent = data.user
div.appendChild(name)
if (data.chat) {
const chat = document.createElement('div')
chat.textContent = data.chat
div.appendChild(chat)
} else {
console.log('gif obj detected')
console.log(data)
const gif = document.createElement('img')
gif.src = '/gif/' + data.gif
div.appendChild(gif)
}
div.style.color = data.user
document.querySelector('#chat-list').appendChild(div)
})
document.querySelector('#chat-form').addEventListener('submit', function (e) {
e.preventDefault()
if (e.target.chat.value) {
console.log('채팅방 버튼 클릭!')
axios
.post('/room/{{room._id}}/chat', {
chat: this.chat.value,
})
.then(() => {
e.target.chat.value = ''
})
.catch((err) => {
console.error(err)
})
}
})
index.js(router) 라우터 안의 채팅 post요청을 처리하는 부분. 상술한 로직이 코드로 표현되어 있음
router.post('/room/:id/chat', async (req, res, next) => {
try {
const chat = await Chat.create({
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);
res.send('ok');
} catch (err) {
console.error(err);
next(err);
}
});
http 단에서 채팅을 ㄷㄷ 수고를 좀 덜겠어요.