socket.io 에서 sql db 사용
741
작성한 질문수 12
1명만 존재하는 채팅방에서 나갔을때 채팅방이 화면에 렌더링 되는 오류입니다.
아래는 몽고디비를 사용했을 때 입니다.
chat 네임스페이스 접속 해제를 한 후 DB 에서 채팅방을 제거했기 때문에 GET "/" 의 response 에는 기존의 채팅방이 포함되어 있지않습니다. (정상)
아래는 SQL 로 전환했을 때입니다.
SQL 을 사용 했을때는 GET "/" 서버 response 가 chat 네임스페이스 접속 해제보다 빠릅니다. 그러므로 아무도 남아있지않은 채팅방이 GET "/" 의 response 에 포함되어 있고 화면에 렌더링 되는 오류가 발생합니다.
또한 여러번 시도하면 스페이스의 접속과 해제순서가 바뀌어 운좋게 오류가 없을 때도 있습니다.
질문
DB 종류 의 차이가 HTTP 요청과 SOCKET 연결/해제 순서를 바꿀 수 있나요?
이를 해결하기 위한 조언을 부탁 드려요
답변 1
0
sql 코드를 어떻게 하셨나요? chat 네임스페이스 접속 해제 로그는 db랑 상관이 없는 위치인데 이 부분이 다르게 나온다는 것은 이해가 되지 않네요.
0
//controllers/index.js
const express = require('express');
const { Room } = require('../models');
const { Chat } = require('../models');
const { removeRoom: removeRoomService } = require('../services');
exports.renderMain = async (req, res, next) => {
try {
const rooms = await Room.findAll();
res.render('main', { rooms, title: 'GIF 채팅방' });
} catch (error) {
console.error(error);
next(error);
}
};
exports.renderRoom = (req, res) => {
res.render('room', { title: 'GIF 채팅방 생성' });
};
exports.createRoom = 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');
io.of('/room').emit('newRoom', newRoom);
if (req.body.password) { // 비밀번호가 있는 방이면
res.redirect(`/room/${newRoom.id}?password=${req.body.password}`);
} else {
res.redirect(`/room/${newRoom.id}`);
}
} catch (error) {
console.error(error);
next(error);
}
};
exports.enterRoom = async (req, res, next) => {
try {
const room = await Room.findOne({ id: req.params.id });
if (!room) {
return res.redirect('/?error=존재하지 않는 방입니다.');
}
if (room.password && room.password !== req.query.password) {
return res.redirect('/?error=비밀번호가 틀렸습니다.');
}
const io = req.app.get('io');
const { rooms } = io.of('/chat').adapter;
console.log(rooms, rooms.get(req.params.id));
if (room.max <= rooms.get(req.params.id)?.size) {
return res.redirect('/?error=허용 인원이 초과하였습니다.');
}
const chats = await Chat.findAll({ where: {RoomId: room.id },
order: [
['createdAt', 'DESC'],
],
})
return res.render('chat', {
room,
title: room.title,
chats,
user: req.session.color,
});
} catch (error) {
console.error(error);
return next(error);
}
};
exports.removeRoom = async (req, res, next) => {
try {
await removeRoomService(req.params.id);
res.send('ok');
} catch (error) {
console.error(error);
next(error);
}
};
exports.sendChat = 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 (error) {
console.error(error);
next(error);
}
};
exports.sendGif = 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);
}
};
// routes/index.js
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const {
renderMain, renderRoom, createRoom, enterRoom, removeRoom, sendChat, sendGif,
} = require('../controllers');
const router = express.Router();
router.get('/', renderMain);
router.get('/room', renderRoom);
router.post('/room', createRoom);
router.get('/room/:id', enterRoom);
router.delete('/room/:id', removeRoom);
router.post('/room/:id/chat', sendChat);
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);
},
}),
limits: { fileSize: 5 * 1024 * 1024 },
});
router.post('/room/:id/gif', upload.single('gif'), sendGif);
module.exports = router;
// views/chat.html
{% extends 'layout.html' %}
{% block content %}
<h1>{{title}}</h1>
<a href="/" id="exit-btn">방 나가기</a>
<fieldset>
<legend>채팅 내용</legend>
<div id="chat-list">
{% for chat in chats %}
{% if chat.user === user %}
<div class="mine" style="color: {{chat.user}}">
<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">
<div>{{chat.chat}}</div>
</div>
{% else %}
<div class="other" style="color: {{chat.user}}">
<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/form-data">
<label for="gif">GIF 올리기</label>
<input type="file" id="gif" name="gif" accept="image/gif">
<input type="text" id="chat" name="chat">
<button type="submit">전송</button>
</form>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io.connect('http://localhost:8005/chat', {
path: '/socket.io',
});
socket.emit('join', new URL(location).pathname.split('/').at(-1));
socket.on('join', function (data) {
const div = document.createElement('div');
div.classList.add('system');
const chat = document.createElement('div');
chat.textContent = data.chat;
div.appendChild(chat);
document.querySelector('#chat-list').appendChild(div);
});
socket.on('exit', function (data) {
const div = document.createElement('div');
div.classList.add('system');
const chat = document.createElement('div');
chat.textContent = data.chat;
div.appendChild(chat);
document.querySelector('#chat-list').appendChild(div);
});
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 {
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) {
axios.post('/room/{{room.id}}/chat', {
chat: this.chat.value,
})
.then(() => {
e.target.chat.value = '';
})
.catch((err) => {
console.error(err);
});
}
});
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>
{% endblock %}
// views/main.html
{% 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://localhost:8005/room', { // 네임스페이스
path: '/socket.io',
});
socket.on('newRoom', function (data) { // 새 방 이벤트 시 새 방 생성
const tr = document.createElement('tr');
let td = document.createElement('td');
td.textContent = data.title;
tr.appendChild(td);
td = document.createElement('td');
td.textContent = data.password ? '비밀방' : '공개방';
tr.appendChild(td);
td = document.createElement('td');
td.textContent = data.max;
tr.appendChild(td);
td = document.createElement('td');
td.style.color = data.owner;
td.textContent = data.owner;
tr.appendChild(td);
td = document.createElement('td');
const button = document.createElement('button');
button.textContent = '입장';
button.dataset.password = data.password ? 'true' : 'false';
button.dataset.id = data.id;
button.addEventListener('click', addBtnEvent);
td.appendChild(button);
tr.appendChild(td);
tr.dataset.id = data.id;
document.querySelector('table tbody').appendChild(tr); // 화면에 추가
});
socket.on('removeRoom', function (data) { // 방 제거 이벤트 시 id가 일치하는 방 제거
document.querySelectorAll('tbody tr').forEach(function (tr) {
if (tr.dataset.id === data) {
tr.parentNode.removeChild(tr);
}
});
});
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 %}
// app.js
const express = require('express');
const path = require('path');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
const ColorHash = require('color-hash').default;
dotenv.config();
const webSocket = require('./socket');
const indexRouter = require('./routes');
const { sequelize } = require('./models');
const app = express();
app.set('port', process.env.PORT || 8005);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
sequelize.sync({ force: true })
.then(() => {
console.log('데이터베이스 연결 성공');
})
.catch((err) => {
console.error(err);
});
const sessionMiddleware = session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
});
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/gif', express.static(path.join(__dirname, 'uploads')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(sessionMiddleware);
app.use((req, res, next) => {
if (!req.session.color) {
console.log('hi')
const colorHash = new ColorHash();
req.session.color = colorHash.hex(req.sessionID);
console.log(req.session.color, req.sessionID);
}
next();
});
app.use('/', indexRouter);
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404;
next(error);
});
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
res.status(err.status || 500);
res.render('error');
});
const server = app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기중');
});
webSocket(server, app, sessionMiddleware);
// socket.js
const SocketIO = require('socket.io');
const { removeRoom } = require('./services');
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');
const wrap = middleware => (socket, next) => middleware(socket.request, {}, next);
chat.use(wrap(sessionMiddleware));
room.on('connection', (socket) => {
console.log('room 네임스페이스에 접속');
socket.on('disconnect', () => {
console.log('room 네임스페이스 접속 해제');
});
});
chat.on('connection', (socket) => {
console.log(socket.id)
console.log('chat 네임스페이스에 접속');
socket.on('join', (data) => {
socket.join(data);
socket.to(data).emit('join', {
user: 'system',
chat: `${socket.request.session.color}님이 입장하셨습니다.`,
});
});
socket.on('disconnect', async () => {
console.log('chat 네임스페이스 접속 해제');
const { referer } = socket.request.headers; // 브라우저 주소가 들어있음
const roomId = new URL(referer).pathname.split('/').at(-1);
const currentRoom = chat.adapter.rooms.get(roomId);
const userCount = currentRoom?.size || 0;
if (userCount === 0) { // 유저가 0명이면 방 삭제
await removeRoom(roomId); // 컨트롤러 대신 서비스를 사용
room.emit('removeRoom', roomId);
console.log('방 제거 요청 성공');
} else {
socket.to(roomId).emit('exit', {
user: 'system',
chat: `${socket.request.session.color}님이 퇴장하셨습니다.`,
});
}
});
});
};
// service/index.js
const { Room } = require('../models');
const { Chat } = require('../models');
exports.removeRoom = async (roomId) => {
try {
await Room.destroy({ where: {id: roomId} });
await Chat.destroy({ where: { RoomId : roomId} });
} catch (error) {
throw error;
}
};
0
db 구조 입니다.
//models/chat.js
const Sequelize = require('sequelize');
class Chat extends Sequelize.Model {
static initiate(sequelize) {
Chat.init({
user: {
type: Sequelize.STRING,
allowNull: false,
},
chat: {
type: Sequelize.STRING,
allowNull: true,
},
gif: {
type: Sequelize.STRING,
allowNull: true,
},
}, {
sequelize,
modelName: 'Chat',
tableName: 'chats',
paranoid: false,
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
});
}
static associate(db) {
db.Chat.belongsTo(db.Room);
}
}
module.exports = Chat;
// models/room.js
const Sequelize = require('sequelize');
class Room extends Sequelize.Model {
static initiate(sequelize) {
Room.init({
title: {
type: Sequelize.STRING,
allowNull: false,
},
max: {
type: Sequelize.INTEGER,
allowNull: false,
validate: {
min: 2,
max: 10
}
},
owner: {
type: Sequelize.STRING,
allowNull: false,
defaultValue: 'local',
},
password: {
type: Sequelize.STRING(100),
allowNull: true,
},
}, {
sequelize,
modelName: 'Room',
tableName: 'rooms',
paranoid: false,
charset: 'utf8',
collate: 'utf8_general_ci',
});
}
static associate(db) {
db.Room.hasMany(db.Chat);
}
};
module.exports = Room;
// models/index.js
const Sequelize = require('sequelize');
const fs = require('fs');
const path = require('path');
const env = process.env.NODE_ENV || 'development';
const config = require('../config/config')[env];
const db = {};
const sequelize = new Sequelize(
config.database, config.username, config.password, config,
);
db.sequelize = sequelize;
const basename = path.basename(__filename);
fs
.readdirSync(__dirname) // 현재 폴더의 모든 파일을 조회
.filter(file => { // 숨김 파일, index.js, js 확장자가 아닌 파일 필터링
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
})
.forEach(file => { // 해당 파일의 모델 불러와서 init
const model = require(path.join(__dirname, file));
console.log(file, model.name);
db[model.name] = model;
model.initiate(sequelize);
});
Object.keys(db).forEach(modelName => { // associate 호출
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
module.exports = db;
0
방 제거 요청이 GET / 보다 먼저 나오거나, 아니면 root 네임스페이스에 접속한 이후에 방 제거 요청이 가면 문제가 없는데 하필 그 사이에 나와버리네요. 디비 드라이버에 따른 처리 속도 차이일 수도 있을 것 같습니다.
확실한 방법은 아니긴 한데 socket.emit을 타이머로 1~2초 뒤에 호출하면 문제는 없을 겁니다.
깃헙 질문
0
82
2
강의 1-1 수업노트의 로드맵 링크가 작동하지 않습니다.
0
76
1
aws - lightsail 이용 관련
0
58
1
4강 http 서버 만들때 ESM방식으로 해도 될까요?
0
81
2
모듈 사용 시 단점이 있나요?
0
81
1
node.js 버전 및 typescript 적용 문의
0
95
2
12.7. 방장기능(강퇴) 질문드립니다.
0
80
2
12.7 socket.js코드 그대로 뱃겨서 했는데, socket.request.session.color가안나오네요
0
69
1
12.7 코드 그대로 뱃겨서 햇는데 스샷같이 오류가뜹니다.
0
75
2
12.7.1스스로 해보기 질문되나요
0
95
3
시퀄라이즈 실습하기 질문드립니다.
0
196
9
<7-5. 시퀄라이즈 사용하기>수업 질문 드립니다.
0
97
2
크롬에서 user id를 인풋에 입력하고 등록하면 404 에러처리 페이지가 뜹니다.
0
113
2
구매 결제관련 질문입니다 !
0
123
1
다수의 supertest 가 실행될 때 force:true로 인한 DB 초기화 문제
0
133
2
node 설치 방법이 전혀다르게 바뀐것 같습니다.
0
152
2
12강 깃허브에 있는 12.7 chat.html 복붙했는데 css오류
0
122
2
무료/프리미엄 동시 소유 시 질문
0
117
1
비주얼 스튜디오 코드로 계속 진행해도 괜찮을까요?
0
135
2
10강 cors에러 localhost:4000으로 접속했을때 에러
1
159
2
webstorm 해결할 수 없는 변수 문제
0
155
2
혹시 몽고DB 쓸거면 MySQL 강의 스킵해도 되나요?
0
149
2
LightSail 실행 중 오류 질문드립니다!
0
204
2
RedisStore 사용법 질문
0
133
2





