๐ํค์๋: ์น ์์ผ, WS ํ๋กํ ์ฝ, Socket.IO, ๋ฏธ๋ค์จ์ด, ์ปจํธ๋กค๋ฌ
1. ์น ์์ผ
- ์๋ฐฉํฅ ๋ฐ์ดํฐ ์ ์ก์ ์ํ ๊ธฐ์ ์ด๋ค.
- WS ํ๋กํ ์ฝ์ ์ฌ์ฉํ๋ค.
โป ์๋ฒ์ผํธ ์ด๋ฒคํธ(Server Sent Events): Event Source ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํด ์ฒ์์ ํ ๋ฒ๋ง ์ฐ๊ฒฐํ๋ฉด ์๋ฒ๊ฐ ํด๋ผ์ด์ธํธ์ ์ง์์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ธ๋ค. ์ฆ, ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ด๋ ๋จ๋ฐํฅ ํต์
์์ ๊ฐ์ ๋๋ ํฐ๋ฆฌ ๊ตฌ์กฐ๋ก ํ๋ก์ ํธ์ ํ์ผ์ ์์ฑํ๋ค.
- ํจํค์ง๋ฅผ ์ค์นํ๊ณ .env, app.js, routes/index.js, package.json ํ์ผ์ ์์ฑํ๋ค.
- ๊ทธ ํ ws ๋ชจ๋์ ์ค์นํด ๋
ธ๋์ ์น ์์ผ์ ๊ตฌํํ๋ค. (npm i ws@8)
- ์น ์์ผ์ ์ต์คํ๋ ์ค ์๋ฒ์ ์ฐ๊ฒฐํ๋ค.
- socket.js ํ์ผ์ ์์ฑํ๊ณ ws ๋ชจ๋์ ๋ถ๋ฌ์ ์ต์คํ๋ ์ค ์๋ฒ๋ฅผ ์น ์์ผ ์๋ฒ์ ์ฐ๊ฒฐํ๋ค.
- ์ฐ๊ฒฐ ํ ์น ์์ผ ์๋ฒ(wss)์ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ๋ถ์ธ๋ค. (์น ์์ผ์ ์ด๋ฒคํธ ๊ธฐ๋ฐ์ผ๋ก ์๋)
- ์ต์คํ๋ ์ค ์๋ฒ์ ์ฐ๊ฒฐํ ํ, ์น ์์ผ ๊ฐ์ฒด(ws)์ ์ด๋ฒคํธ ๋ฆฌ์ค๋ ์ธ ๊ฐ๋ฅผ ์ฐ๊ฒฐํ๋ค.
//socket.js
const WebSocket = require('ws');
module.exports = (server) => {
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws, req) => { // ์น์์ผ ์ฐ๊ฒฐ ์
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
console.log('์๋ก์ด ํด๋ผ์ด์ธํธ ์ ์', ip);
ws.on('message', (message) => { // ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ๋ฉ์์ง
console.log(message.toString());
});
ws.on('error', (error) => { // ์๋ฌ ์
console.error(error);
});
ws.on('close', () => { // ์ฐ๊ฒฐ ์ข
๋ฃ ์
console.log('ํด๋ผ์ด์ธํธ ์ ์ ํด์ ', ip);
clearInterval(ws.interval);
});
ws.interval = setInterval(() => { // 3์ด๋ง๋ค ํด๋ผ์ด์ธํธ๋ก ๋ฉ์์ง ์ ์ก
if (ws.readyState === ws.OPEN) {
ws.send('์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ๋ฉ์์ง๋ฅผ ๋ณด๋
๋๋ค.');
}
}, 3000);
});
};
- readyState ์ํ 4๊ฐ์ง: CONNECTING(์ฐ๊ฒฐ ์ค), OPEN(์ด๋ฆผ), CLOSING(๋ซ๋ ์ค), CLOSED(๋ซํ)
- OPEN์ผ ๋ ์๋ฌ ์์ด ws.send ๋ฉ์๋๋ก ํ๋์ ํด๋ผ์ด์ธํธ์ ๋ฉ์์ง๋ฅผ ๋ณด๋ธ๋ค.
- CLOSED ์ด๋ฒคํธ์์ setInterval์ clearInterval๋ก ์ ๋ฆฌํด์ผํ๋ค. (์ด ๋ถ๋ถ์ด ์๋ค๋ฉด ๋ฉ๋ชจ๋ฆฌ ๋์๊ฐ ๋ฐ์ํจ)
- ์น ์์ผ์ ์๋ฐฉํฅ ํต์ ์ด๊ธฐ ๋๋ฌธ์ html ํ์ผ์ ์์ฑํด์ผ ํ๋ค.
//views/index.js
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>GIF ์ฑํ
๋ฐฉ</title>
</head>
<body>
<div>F12๋ฅผ ๋๋ฌ console ํญ๊ณผ network ํญ์ ํ์ธํ์ธ์.</div>
<script>
const webSocket = new WebSocket("ws://localhost:8005");
webSocket.onopen = function () {
console.log('์๋ฒ์ ์น์์ผ ์ฐ๊ฒฐ ์ฑ๊ณต!');
};
webSocket.onmessage = function (event) {
console.log(event.data);
webSocket.send('ํด๋ผ์ด์ธํธ์์ ์๋ฒ๋ก ๋ต์ฅ์ ๋ณด๋
๋๋ค');
};
</script>
</body>
</html>
- WebSocket ์์ฑ์์ ์ฐ๊ฒฐํ ์๋ฒ ์ฃผ์๋ฅผ ๋ฃ๊ณ webSocket ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ค. (ํด๋ผ์ด์ธํธ๋ ์ด๋ฒคํธ ๊ธฐ๋ฐ์ผ๋ก ๋์)
- ๊ฐ๋ฐ์ ๋๊ตฌ(F12)๋ฅผ ์ผ Network ํญ์ ๋ค์ด๊ฐ๋ฉด ์น ์์ผ์ ์ ์ ์๋ค.
๊ตฌํํ๋ ค๋ ์๋น์ค๊ฐ ๋ณต์กํด์ง๋ค๋ฉด ws ํจํค์ง๊ฐ ์๋ Socket.IO๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ํธํ๋ค. (ํธ์ ๊ธด์์ด ๋ง์ด ์ถ๊ฐ๋์ด ์์)
- Socket.IO ์ค์น
$ npm i socket.io@4
//socket.js
const SocketIO = require('socket.io');
module.exports = (server) => {
const io = SocketIO(server, { path: '/socket.io' });
io.on('connection', (socket) => { // ์น์์ผ ์ฐ๊ฒฐ ์
const req = socket.request;
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
console.log('์๋ก์ด ํด๋ผ์ด์ธํธ ์ ์!', ip, socket.id, req.ip);
socket.on('disconnect', () => { // ์ฐ๊ฒฐ ์ข
๋ฃ ์
console.log('ํด๋ผ์ด์ธํธ ์ ์ ํด์ ', ip, socket.id);
clearInterval(socket.interval);
});
socket.on('error', (error) => { // ์๋ฌ ์
console.error(error);
});
socket.on('reply', (data) => { // ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ๋ฉ์์ง
console.log(data);
});
socket.interval = setInterval(() => { // 3์ด๋ง๋ค ํด๋ผ์ด์ธํธ๋ก ๋ฉ์์ง ์ ์ก
socket.emit('news', 'Hello Socket.IO');
}, 3000);
});
};
- socket.io ํจํค์ง๋ฅผ ๋ถ๋ฌ์์ ์ต์คํ๋ ์ค ์๋ฒ์ ์ฐ๊ฒฐํ๋ค.
- ์ฐ๊ฒฐ ํ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ๋ถ์ธ๋ค. (connection ์ด๋ฒคํธ๋ ํด๋ผ์ด์ธํธ๊ฐ ์ ์ํ์ ๋ ๋ฐ์ํ๊ณ , ์ฝ๋ฐฑ์ผ๋ก ์์ผ ๊ฐ์ฒด๋ฅผ ์ ๊ณต)
- socket์๋ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ๋ถ์ธ๋ค. (disconnect๋ ํด๋ผ์ด์ธํธ๊ฐ ์ฐ๊ฒฐ์ ๋์์ ๋ ๋ฐ์ํ๊ณ , error๋ ํต์ ๊ณผ์ ์ค์ ์๋ฌ๊ฐ ๋์์ ๋ ๋ฐ์ํจ)
โป ws๋ชจ๋๊ณผ ๋ค๋ฅธ์ : ์ด๋ฒคํธ ๋ช
์ ์ฌ์ฉํ๋ค. (reply๋ผ๋ ์ด๋ฒคํธ๋ช
์ผ๋ก ํด๋ผ์ด์ธํธ์์ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ผ ๋ ์๋ฒ์์ ๋ฐ๋ ๋ถ๋ถ)
//views/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>GIF ์ฑํ
๋ฐฉ</title>
</head>
<body>
<div>F12๋ฅผ ๋๋ฌ console ํญ๊ณผ network ํญ์ ํ์ธํ์ธ์.</div>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io.connect('http://localhost:8005', {
path: '/socket.io',
transports: ['websocket'],
});
socket.on('news', function (data) {
console.log(data);
socket.emit('reply', 'Hello Node.JS');
});
</script>
</body>
</html>
-> Socket.IO์ ๋ง๊ฒ ํด๋ผ์ด์ธํธ ๋ถ๋ถ๋ ๋ฐ๊ฟ์ค๋ค.
- /socket.io/socket.io.js๋ Socket.IO์์ ํด๋ผ์ด์ธํธ๋ก ์ ๊ณตํ๋ ์คํฌ๋ฆฝํธ์ด๋ค. (์ค์ ํ์ผ์ด ์๋)
->์ด ์คํฌ๋ฆฝํธ๋ฅผ ํตํด ์๋ฒ์ ์ ์ฌํ API๋ก ์น ์์ผ ํต์ ์ด ๊ฐ๋ฅํ๋ค.
โป ws๋ชจ๋๊ณผ ๋ค๋ฅธ์ : ws ํ๋กํ ์ฝ์ด ์๋๋ผ http ํ๋กํ ์ฝ์ ์ฌ์ฉํ๋ ์ ์ด ๋ค๋ฅด๋ค.
-> Socket.IO๋ ๋จผ์ ํด๋ง ๋ฐฉ์์ผ๋ก ์๋ฒ์ ์ฐ๊ฒฐํ๋ค. (HTTP ํ๋กํ ์ฝ ์ฌ์ฉ)
โป ์น ์์ผ์ ์ง์ํ๋ ๋ธ๋ผ์ฐ์ ๋ ์น ์์ผ ๋ฐฉ์์ผ๋ก, ์ง์ํ์ง ์๋ ๋ธ๋ผ์ฐ์ ๋ ํด๋ง ๋ฐฉ์์ผ๋ก ์ฌ์ฉ ๊ฐ๋ฅํ๋ค.
- ์ฌ๋๋ค์ด ์ต๋ช
์ผ๋ก ์์ฑํ๊ณ ์์ ๋กญ๊ฒ ์ฐธ์ฌํ๋ฉฐ GIF ํ์ผ์ ์ฌ๋ฆด ์ ์๋ ์ฑํ
๋ฐฉ ๋ง๋ค๊ธฐ.
- ๋ชฝ๊ณ ๋๋น์ ๋ชฝ๊ณ ๋๋น ODM์ธ ๋ชฝ๊ตฌ์ค๋ฅผ ์ฌ์ฉํ๋ค. ($ npm i mongoose multer color-hash@2)
- ์ฑํ
๋ฐฉ ์คํค๋ง, ์ฑํ
์คํค๋ง๋ฅผ ์์ฑํ๋ค.
//schemas/index.js
const mongoose = require('mongoose');
const { MONGO_ID, MONGO_PASSWORD, NODE_ENV } = process.env;
const MONGO_URL = `mongodb://${MONGO_ID}:${MONGO_PASSWORD}@localhost:27017/admin`;
const connect = () => {
if (NODE_ENV !== 'production') {
mongoose.set('debug', true);
}
mongoose.connect(MONGO_URL, {
dbName: 'gifchat',
useNewUrlParser: true,
}).then(() => {
console.log("๋ชฝ๊ณ ๋๋น ์ฐ๊ฒฐ ์ฑ๊ณต");
}).catch((err) => {
console.error("๋ชฝ๊ณ ๋๋น ์ฐ๊ฒฐ ์๋ฌ", err);
});
};
mongoose.connection.on('error', (error) => {
console.error('๋ชฝ๊ณ ๋๋น ์ฐ๊ฒฐ ์๋ฌ', error);
});
mongoose.connection.on('disconnected', () => {
console.error('๋ชฝ๊ณ ๋๋น ์ฐ๊ฒฐ์ด ๋๊ฒผ์ต๋๋ค. ์ฐ๊ฒฐ์ ์ฌ์๋ํฉ๋๋ค.');
connect();
});
module.exports = connect;
-> ๋ชฝ๊ณ ๋๋น์ ์ฐ๊ฒฐํ๋ ์ฝ๋๋ฅผ ์์ฑํ๋ค.
//app.js
...
dotenv.config();
const webSocket = require('./socket');
const indexRouter = require('./routes');
const connect = require('./schemas');
const app = express();
app.set('port', process.env.PORT || 8005);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
connect();
...
-> ์๋ฒ๋ฅผ ์คํํ ๋ ๋ชฝ๊ณ ๋๋น์ ๋ฐ๋ก ์ ์ํ ์ ์๋๋ก ์๋ฒ์ ๋ชฝ๊ตฌ๋ฅผ ์ฐ๊ฒฐํ๋ค.
- ๊ทธ ํ ํ๋ฉด์ ๋ ์ด์์์ ๋ด๋นํ๋ ํ์ผ ๋ค์ html๋ก ์์ฑํ๊ณ main.css๋ ์ถ๊ฐํด์ค๋ค.
- ์ฑํ
๋ฉ์์ง๋ ๋ด ๋ฉ์์ง(mine), ์์คํ
๋ฉ์์ง(system), ๋จ์ ๋ฉ์์ง(other)๋ก ๊ตฌ๋ถํ๋ค.
- ์คํฌ๋ฆฝํธ ๋ถ๋ถ์ socket.io ์ฐ๊ฒฐ ๋ถ๋ถ, socket.io ์ด๋ฒคํธ ๋ฆฌ์ค๋, ํผ ์ ์ก ๋ถ๋ถ์ผ๋ก ๊ตฌ๋ถ๋๋ค.
//app.js
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
}));
app.use((req, res, next) => {
if (!req.session.color) {
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);
-> ์ธ์ ์์ด๋(req.sessionID)๋ฅผ ์ฌ์ฉํ๊ณ , ์์ ๊ณผ ๋จ์ ๊ตฌ๋ณํ๊ธฐ ์ํด color-hash ํจํค์ง๋ฅผ ์ฌ์ฉํ๋ค.
//socket.js
const SocketIO = require('socket.io');
module.exports = (server, app) => {
const io = SocketIO(server, { path: '/socket.io' });
app.set('io', io); ---1
const room = io.of('/room'); ----2
const chat = io.of('/chat');
room.on('connection', (socket) => { ----3
console.log('room ๋ค์์คํ์ด์ค์ ์ ์');
socket.on('disconnect', () => {
console.log('room ๋ค์์คํ์ด์ค ์ ์ ํด์ ');
});
});
chat.on('connection', (socket) => { ----4
console.log('chat ๋ค์์คํ์ด์ค์ ์ ์');
socket.on('join', (data) => { // data๋ ๋ธ๋ผ์ฐ์ ์์ ๋ณด๋ธ ๋ฐฉ ์์ด๋
socket.join(data); // ๋ค์์คํ์ด์ค ์๋ ์กด์ฌํ๋ ๋ฐฉ์ ์ ์
});
socket.on('disconnect', () => {
console.log('chat ๋ค์์คํ์ด์ค ์ ์ ํด์ ');
});
});
};
-> ์๋ฒ์ socket.js์ ์น ์์ผ ์ด๋ฒคํธ๋ฅผ ์ฐ๊ฒฐํ๋ค.
-> app.set('io',io)๋ก ๋ผ์ฐํฐ์์ io ๊ฐ์ฒด๋ฅผ ์ธ ์ ์๊ฒ ์ ์ฅํด ๋๋ค. req.app.get('io')๋ก ์ ๊ทผ์ด ๊ฐ๋ฅํ๋ค.
-> Socket.IO์ ๋ค์์คํ์ด์ค๋ฅผ ๋ถ์ฌํ๋ of ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ค. (๊ฐ์ ๋ค์์คํ์ด์ค๋ผ๋ฆฌ๋ง ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๊ฒ ํจ)
-> /room ๋ค์์คํ์ด์ค์ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ๋ถ์ฌ์ค๋ค. io์ ๋ง์ฐฌ๊ฐ์ง๋ก ๋ค์์คํ์ด์ค ์ฐ๊ฒฐ ์ ๋ฐ์ํ๋ connection๊ณผ ํด์ ์ ๋ฐ์ํ๋ disconnect ์ด๋ฒคํธ๊ฐ ์๋ค.(3)
-> /chat ๋ค์์คํ์ด์ค์ ๋ถ์ธ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ Socket.IO์์ ์ ๊ณตํ๋ ์ด๋ฒคํธ๊ฐ ์๋๋ผ ์ฌ์ฉ์๊ฐ ์ง์ ๋ง๋ ์ด๋ฒคํธ์ด๋ค.(4)
- Socket.IO์๋ ๋ค์์คํ์ด์ค๋ณด๋ค ๋ ์ธ๋ถ์ ์ธ ๊ฐ๋
์ธ '๋ฐฉ(room)'์ด ์๋ค.
- ๊ฐ์ ๋ฐฉ์ ๋ค์ด ์๋ ์์ผ๋ผ๋ฆฌ๋ง ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์ ์ ์๋ค.
ex) socket.emit('join', ๋ฐฉ์์ด๋)๋ฅผ ํธ์ถํ๋ฉด socket.js์ join ์ด๋ฒคํธ์์ data ๋งค๊ฐ๋ณ์๋ก ๋ฐฉ ์์ด๋๋ฅผ ์ ๋ฌ๋ฐ์ ๋ฐฉ์ ์ ์ํ ๊ฒ์ด๋ค. ์ฐ๊ฒฐ์ด ๋๊ธฐ๋ฉด (disconnect ์ด๋ฒคํธ) ์๋์ผ๋ก ๋ฐฉ์์ ๋๊ฐ๋ค.
//controllers/index.js
const Room = require('../schemas/room');
const Chat = require('../schemas/chat');
exports.renderMain = async (req, res, next) => {
try {
const rooms = await Room.find({});
res.render('main', { rooms, title: 'GIF ์ฑํ
๋ฐฉ' });
} catch (error) {
console.error(error);
next(error);
}
};
exports.renderRoom = (req, res) => { ---1
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) => { ---2
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), rooms.get(req.params.id));
if (room.max <= rooms.get(req.params.id)?.size) {
return res.redirect('/?error=ํ์ฉ ์ธ์์ด ์ด๊ณผํ์์ต๋๋ค.');
}
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) => { ---3
try {
await Room.deleteOne({ _id: req.params.id });
await Chat.deleteMany({ room: req.params.id });
res.send('ok');
} catch (error) {
console.error(error);
next(error);
}
};
- ์ปจํธ๋กค๋ฌ์์๋ ๋ชฝ๊ณ ๋๋น์ ์น ์์ผ ๋ชจ๋์ ์ ๊ทผํ ์ ์๋ค.
-> createRoom ์ปจํธ๋กค๋ฌ๋ ์ฑํ
๋ฐฉ์ ๋ง๋๋ ์ปจํธ๋กค๋ฌ์ด๋ค. (1)
-> enterRoom ์ปจํธ๋กค๋ฌ๋ ์ฑํ
๋ฐฉ์ ์ ์ํด ์ฑํ
๋ฐฉ ํ๋ฉด์ ๋ ๋๋งํ๋ ์ปจํธ๋กค๋ฌ์ด๋ค. ๋ ๋๋ง ์ ์ ๋ฐฉ์ด ์กด์ฌํ๋์ง, ๋น๋ฐ๋ฐฉ์ผ ๊ฒฝ์ฐ์๋ ๋น๋ฐ๋ฒํธ๊ฐ ๋ง๋์ง, ํ์ฉ ์ธ์์ ์ด๊ณผํ์ง๋ ์์๋์ง ๊ฒ์ฌํ๋ค. (2)
-> removeRoom ์ปจํธ๋กค๋ฌ๋ ์ฑํ
๋ฐฉ์ ์ญ์ ํ๋ ์ปจํธ๋กค๋ฌ์ด๋ค. ์ฑํ
๋ฐฉ๊ณผ ์ฑํ
๋ด์ญ์ ํจ๊ป ์ญ์ ํ๋ค. (3)
โป gif-chat ์๋ฒ๋ฅผ ์์ํ๊ธฐ ์ ์ ๋ชฝ๊ณ ๋๋น๋ฅผ ๋จผ์ ์คํํด์ผ ํ๋ค.
- http://localhost:8005 ์ ์ ์ํ๋ฉด ์คํ๋๋ ํ๋ฉด
- ์ฌ์ฉ์์ ์ด๋ฆ์ ์ฑํ ๋ฐฉ์ ํ์ํ๊ณ , '~๋์ด ์ ์ฅํ์ จ์ต๋๋ค' ๊ฐ์ ๋ฉ์์ง๋ฅผ ๋ณด๋ด๊ธฐ
//app.js
...
connect();
const sessionMiddleware = session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
});
...
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(sessionMiddleware);
...
const server = app.listen(app.get('port'), () => {
console.log(app.get('port'), '๋ฒ ํฌํธ์์ ๋๊ธฐ์ค');
});
webSocket(server, app, sessionMiddleware);
-> app.js์ socket.js ๊ฐ์ express-session ๋ฏธ๋ค์จ์ด๋ฅผ ๊ณต์ ํ๊ธฐ ์ํด ๋ณ์๋ก ๋ถ๋ฆฌํ๋ค.
//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); --1
chat.use(wrap(sessionMiddleware));
room.on('connection', (socket) => {
console.log('room ๋ค์์คํ์ด์ค์ ์ ์');
socket.on('disconnect', () => {
console.log('room ๋ค์์คํ์ด์ค ์ ์ ํด์ ');
});
});
chat.on('connection', (socket) => { ---2
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 () => { ---3
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}๋์ด ํด์ฅํ์
จ์ต๋๋ค.`,
});
}
});
});
};
-> chat.use ๋ฉ์๋์ ๋ฏธ๋ค์จ์ด๋ฅผ ์ฅ์ฐฉํ ์ ์๋ค. ์ด ๋ฏธ๋ค์จ์ด๋ chat ๋ค์์คํ์ด์ค์ ์น ์์ผ์ด ์ฐ๊ฒฐ๋ ๋๋ง๋ค ์คํ๋๋ค.
wrap ํจ์๋ ๋ฏธ๋ค์จ์ด์ ์ต์คํ๋ ์ค์ฒ๋ผ req, res, next๋ฅผ ์ ๊ณตํด์ฃผ๋ ํจ์์ด๋ค. (1)
-> socket.to (๋ฐฉ ์์ด๋) ๋ฉ์๋๋ก ํน์ ๋ฐฉ์ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ผ ์ ์๋ค. ๋ฐฉ์ ์ฐธ์ฌํ ๋ ๋ฐฉ์ ๋๊ตฐ๊ฐ ์
์ฅํ๋ค๋ ์์คํ
๋ฉ์์ง๋ฅผ ๋ณด๋ธ๋ค. (2)
-> ์ ์ ํด์ ์์๋ ํ์ฌ ๋ฐฉ์ ์ฌ๋ ์์ ๋ฐ๋ผ ๋์์ด ๋ฌ๋ผ์ง๋ค. socket.request.headers.referer์ ๋ธ๋ผ์ฐ์ ์ฃผ์๊ฐ ๋ค์ด ์๊ณ , ์ฌ๊ธฐ์ URL ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํด ๋ฐฉ ์์ด๋๋ฅผ ์ถ์ถํด๋ผ ์ ์๋ค.(3)
โป ๋ฐฉ์ ์ ๊ฑฐํ ๋ removeRoom์ด ์ปจํธ๋กค๋ฌ๊ฐ ์๋๋ผ ์๋น์ค์ด๋ค. req, res, next๊ฐ ์๊ธฐ ๋๋ฌธ์ ์ปจํธ๋กค๋ฌ๊ฐ ์๋๋ผ ์๋น์ค๋ฅผ ์ฌ์ฉํ๋ค.
//services/index.js
const Room = require('../schemas/room');
const Chat = require('../schemas/chat');
exports.removeRoom = async (roomId) => {
try {
await Room.deleteOne({ _id: roomId });
await Chat.deleteMany({ room: roomId });
} catch (error) {
throw error;
}
};
-> removeRoom ์๋น์ค๋ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ๋ค.
//controllers/index.js
const Room = require('../schemas/room');
const Chat = require('../schemas/chat');
const { removeRoom: removeRoomService } = require('../services');
...
exports.removeRoom = async (req, res, next) => {
try {
await removeRoomService(req.params.id);
res.send('ok');
} catch (error) {
console.error(error);
next(error);
}
};
-> removeRoom ์ปจํธ๋กค๋ฌ๋ removeRoom ์๋น์ค๋ฅผ ๊ฐ์ ธ์ ์ฌ์ฉํ๋ค.
-> removeRoom ์๋น์ค๋ฅผ ํธ์ถํ๋ ๋ฐ๋ roomd(๋ฐฉ ์์ด๋)๋ง ํ์ํ๋ค.
- ํ๋ฐํธ์์๋ ์๋ฒ์์ ๋ณด๋ด๋ ์ฑํ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ์์ผ ์ด๋ฒคํธ ๋ฆฌ์ค๋๊ฐ ํ์ํ๊ธฐ ๋๋ฌธ์ chat.html ํ์ผ์ ์ถ๊ฐํด์ค๋ค.
//views/chat.html
...
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);
});
}
});
</script>
{% endblock %}
-> socket์ chat ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ์ถ๊ฐํ๋ค. (chat ์ด๋ฒคํธ๋ ์ฑํ
๋ฉ์์ง๊ฐ ์น ์์ผ์ผ๋ก ์ ์ก๋ ๋ ํธ์ถ๋จ)
-> ์ฑํ
๋ฉ์์ง ๋ฐ์ก์(data.user)์ ๋ฐ๋ผ ๋ด ๋ฉ์์ง(mine ํด๋์ค)์ธ์ง ๋จ์ ๋ฉ์์ง(other ํด๋์ค) ์ธ์ง ํ์ธํ ํ ๊ทธ์ ๋ง๊ฒ ๋ ๋๋ง ํ๋ค.
//controllers/index.js
...
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), rooms.get(req.params.id));
if (room.max <= rooms.get(req.params.id)?.size) {
return res.redirect('/?error=ํ์ฉ ์ธ์์ด ์ด๊ณผํ์์ต๋๋ค.');
}
const chats = await Chat.find({ room: room._id }).sort('createdAt');
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);
}
}
//routes/index.js
const express = require('express');
const {
renderMain, renderRoom, createRoom, enterRoom, removeRoom, sendChat,
} = require('../controllers');
const router = express.Router();
...
router.delete('/room/:id', removeRoom);
router.post('/room/:id/chat', sendChat);
module.exports = router;
-> enterRoom ์ปจํธ๋กค๋ฌ์์ ๋ฐฉ ์ ์ ์ ๊ธฐ์กด ์ฑํ
๋ด์ญ์ ๋ถ๋ฌ์ค๋๋ก ์์ ํ๋ค.
-> ๋ฐฉ์ ์ ์ํ ๋๋ DB๋ก๋ถํฐ ์ฑํ
๋ด์ญ์ ๊ฐ์ ธ์ค๊ณ , ์ ์ ํ์๋ ์น ์์ผ์ผ๋ก ์๋ก์ด ์ฑํ
๋ฉ์์ง๋ฅผ ๋ฐ๋๋ค.
- ์ฑํ
์ ํ ๋๋ง๋ค ์ฑํ
๋ด์ฉ์ด POST /room/:id/chat ๋ผ์ฐํฐ๋ก ์ ์ก๋๊ณ , ๋ผ์ฐํฐ์์ ๋ค์ ์น ์์ผ์ผ๋ก ๋ฉ์์ง๋ฅผ ๋ณด๋ธ๋ค.
โป ์ฑ
602p ์น ์์ผ๋ง์ผ๋ก ์ฑํ
๊ตฌํํ๊ธฐ, ๊ธฐํ Socket.IO API ์ฐธ๊ณ
- GIF ์ด๋ฏธ์ง ์ ์ก ๊ตฌํํ๊ธฐ
//views/chat.html
...
document.querySelector('#chat-form').addEventListener('submit', function (e) {
...
});
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>
-> ํ๋ฐํธ ํ๋ฉด์์ ์ด๋ฏธ์ง๋ฅผ ์ ํํด ์ ๋ก๋ํ๋ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ์ถ๊ฐํ๋ค.
//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');
...
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;
-> POST /room/{{room._id}}/gif ์ฃผ์์ ์์ํ๋ ๋ผ์ฐํฐ๋ฅผ ์์ฑํ๋ค.
//controllers/index.js
...
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);
}
};
-> uploads ํด๋์ ์ฌ์ง์ ์ ์ฅํ๊ณ , ํ์ผ๋ช
์ ํ์์คํฌํ๋ฅผ ๋ถ์ด๊ณ , ์ฉ๋์ 5MB๋ก ์ ํํ๋ค.
-> ํ์ผ์ด ์
๋ก๋๋ ํ์๋ ๋ด์ฉ์ ๋ฐ์ดํฐ ๋ฒ ์ด์ค์ ์ ์ฅํ๊ณ , ๋ฐฉ ์์ ์๋ ๋ชจ๋ ์์ผ์๊ฒ ์ฑํ
๋ฐ์ดํฐ๋ฅผ ๋ณด๋ธ๋ค.
//app.js
...
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());
...
-> ์ด๋ฏธ์ง๋ฅผ ์ ๊ณตํ uploads ํด๋๋ฅผ express.static ๋ฏธ๋ค์จ์ด๋ก ์ฐ๊ฒฐํ๋ค.
1. ์น ์์ผ๊ณผ ( )๋ ๊ฐ์ ํฌํธ๋ฅผ ์ฌ์ฉํ ์ ์์ผ๋ฏ๋ก ๋ฐ๋ก ํฌํธ๋ฅผ ์ค์ ํ์ง ์์๋ ๋๋ค.
2. ์น ์์ผ์ ( )ํต์ ์ด๋ค.
3. ๊ตฌํํ๋ ค๋ ์๋น์ค๊ฐ ๋ณต์กํด์ง๋ค๋ฉด ws ํจํค์ง๊ฐ ์๋ ( )๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ํธํ๋ค.
4. ์น ์์ผ์ ์ง์ํ์ง ์๋ ๋ธ๋ผ์ฐ์ ๋ ( )์ผ๋ก ์ฌ์ฉ ๊ฐ๋ฅํ๋ค.
5. Socket.IO์๋ ๋ค์์คํ์ด์ค๋ณด๋ค ๋ ์ธ๋ถ์ ์ธ ๊ฐ๋
์ธ ( )์ด ์๋ค.
6. ์ปจํธ๋กค๋ฌ์์๋ ( )์ ์น ์์ผ ๋ชจ๋์ ์ ๊ทผํ ์ ์๋ค.
7. ํ๋ฐํธ์์๋ ์๋ฒ์์ ๋ณด๋ด๋ ์ฑํ
๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ( )๊ฐ ํ์ํ๊ธฐ
8. removeRoom ์ปจํธ๋กค๋ฌ๋ removeRoom ์๋น์ค๋ฅผ ๊ฐ์ ธ์ ์ฌ์ฉํ๋ค. ๋น์นธ์ ๋ค์ด๊ฐ ์ฝ๋๋ฅผ ์์ฑํ์์ค.
//controllers/index.js
const Room = require('../schemas/room');
const Chat = require('../schemas/chat');
const { removeRoom: removeRoomService } = require('../services');
...
exports.removeRoom = async (req, res, next) => {
try {
(1)
(2)
} catch (error) {
console.error(error);
next(error);
}
};
9. ์ด๋ฏธ์ง๋ฅผ ์ ๊ณตํ uploads ํด๋๋ฅผ express.static ๋ฏธ๋ค์จ์ด๋ก ์ฐ๊ฒฐํด์ผ ํ๋ค. ๋น์นธ์ ๋ค์ด๊ฐ ์ฝ๋๋ฅผ ์์ฑํ์์ค.
//app.js
...
app.use(express.static(path.join(__dirname, 'public')));
(1)
app.use(express.json());
...
1. HTTP
2. ์๋ฐฉํฅ
3. Socket.IO
4. ํด๋ง ๋ฐฉ์
5. ๋ฐฉ(room)
6. ๋ชฝ๊ณ ๋๋น
7. ์์ผ ์ด๋ฒคํธ ๋ฆฌ์ค๋
8.
//controllers/index.js
const Room = require('../schemas/room');
const Chat = require('../schemas/chat');
const { removeRoom: removeRoomService } = require('../services');
...
exports.removeRoom = async (req, res, next) => {
try {
await removeRoomService(req.params.id); ---(1)
res.send('ok'); ---(2)
} catch (error) {
console.error(error);
next(error);
}
};
9.
//app.js
...
app.use(express.static(path.join(__dirname, 'public')));
app.use('/gif', express.static(path.join(__dirname, 'uploads'))); ---(1)
app.use(express.json());
...
์ถ์ฒ) ์กฐํ์ , ใNode.js ๊ต๊ณผ์ ๊ฐ์ 3ํใ, ๊ธธ๋ฒ(2022), 10์ฅ
Corner node.js 2ํ
Editor : Igumi
[๋ ธ๋ 2ํ] #12. ์น ์์ผ์ผ๋ก ์ค์๊ฐ ๋ฐ์ดํฐ ์ ์กํ๊ธฐ (1) | 2025.01.17 |
---|---|
[๋ ธ๋ 2ํ] #11. ์น API ์๋ฒ ๋ง๋ค๊ธฐ (1) | 2025.01.10 |
[๋ ธ๋ 2ํ] #10. ์ต์คํ๋ ์ค๋ก SNS ์๋น์ค ๋ง๋ค๊ธฐ (0) | 2025.01.03 |
[๋ ธ๋ 2ํ] #9. ๋ชฝ๊ณ ๋๋น (3) | 2024.12.27 |
[๋ ธ๋ 2ํ] #8. MySQL (3) | 2024.11.29 |