์น ์์ผ: ์ค์๊ฐ ์๋ฐฉํฅ ๋ฐ์ดํฐ ์ ์ก์ ์ํ ๊ธฐ์
์น ์์ผ ์ด์ ์๋ ํด๋ง์ด๋ผ๋ ๋ฐฉ์์ ์ฌ์ฉํ์
SSE(Server Sent Events)
gif-chat ํด๋ ์์ฑ ํ package.json ์์ฑ
๐ปpacket.json
{
"name": "gif-chat",
"version": "0.0.1",
"description": "GIF ์น์์ผ ์ฑํ
๋ฐฉ",
"main": "app.js",
"scripts": {
"start": "nodemon app"
},
"author": "seokahi",
"license": "ISC",
"dependencies": {
"cookie-parser": "^1.4.4",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"express-session": "^1.17.0",
"morgan": "^1.9.1",
"nunjucks": "^3.2.0",
"ws": "^8.2.3"
},
"devDependencies": {
"nodemon": "^2.0.2"
}
}
ํจํค์ง๋ฅผ ์ค์นํ๊ณ .env์ app.js, routes/index.js ํ์ผ ์์ฑ
npm i
๐ป.env
COOKIE_SECRET=gifchat
npm i ws๋ก ์ค์น
๐ปapp.js
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');
dotenv.config();
const webSocket = require('./socket');
const indexRouter = require('./routes');
...
const server = app.listen(app.get('port'), () => {
console.log(app.get('port'), '๋ฒ ํฌํธ์์ ๋๊ธฐ์ค');
});
webSocket(server);
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.connection.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);
});
};
index.html๋ฅผ ์์ฑํ๊ณ ์คํฌ๋ฆฝํธ ์์ฑ
๐ปviews/index.pug
<!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>
http://localhost:8005 ์ ์
๊ฐ๋ฐ์ ๋๊ตฌ์ Network ํญ ์ด๊ธฐ
๋ค๋ฅธ ๋ธ๋ผ์ฐ์ ์์ http://localhost:8005์ ์ ์
๋ธ๋ผ์ฐ์ ๋ฅผ ํ๋ ์ข ๋ฃํ๊ธฐ
npm i socket.io
- ws ํจํค์ง ๋์ Socket.IO ์ฐ๊ฒฐ
- Socket.IO ํจํค์ง๋ฅผ ๋ถ๋ฌ์ ์ต์คํ๋ ์ค ์๋ฒ์ ์ฐ๊ฒฐ. ๋ ๋ฒ์งธ ์ธ์๋ ํด๋ผ์ด์ธํธ์ ์ฐ๊ฒฐํ ์ ์๋ ๊ฒฝ๋ก(/socket.io)
- connection ์ด๋ฒคํธ๋ ์๋ฒ์ ์ฐ๊ฒฐ๋์์ ๋ ํธ์ถ, ์ฝ๋ฐฑ์ผ๋ก ์์ผ ๊ฐ์ฒด(socket) ์ ๊ณต
- socket.request๋ก ์์ฒญ ๊ฐ์ฒด์ ์ ๊ทผ ๊ฐ๋ฅ, socket.id๋ก ์์ผ ๊ณ ์ ์์ด๋ ํ์ธ ๊ฐ๋ฅ
- disconnect ์ด๋ฒคํธ๋ ์ฐ๊ฒฐ ์ข ๋ฃ ์ ํธ์ถ, error๋ ์๋ฌ ๋ฐ์ ์ ํธ์ถ
- reply๋ ์ฌ์ฉ์๊ฐ ์ง์ ๋ง๋ค ์ด๋ฒคํธ๋ก ํด๋ผ์ด์ธํธ์์ reply ์ด๋ฒคํธ ๋ฐ์ ์ ์๋ฒ์ ์ ๋ฌ๋จ
- socket.emit์ผ๋ก ๋ฉ์์ง ์ ๋ฌ. ์ฒซ ๋ฒ์งธ ์ธ์๋ ์ด๋ฒคํธ ๋ช , ๋ ๋ฒ์งธ ์ธ์๊ฐ ๋ฉ์์ง
๐ป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.connection.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);
});
};
index.html ์์
๐ป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>
http://localhost:8005์ ์ ์
๐ปviews/index.html
...
<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>
ํ์ ํจํค์ง ์ค์น ํ ์คํค๋ง ์์ฑ
npm i mongoose multer axios color-hash
์ฑํ ๋ฐฉ ์คํค๋ง(room.js)์ ์ฑํ ์คํค๋ง(chat.js) ์์ฑ
๐ป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,
min: 2,
},
owner: {
type: String,
required: true,
},
password: String,
createdAt: {
type: Date,
default: Date.now,
},
});
module.exports = mongoose.model('Room', roomSchema);
๐ป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',
},
user: {
type: String,
required: true,
},
chat: String,
gif: String,
createdAt: {
type: Date,
default: Date.now,
},
});
module.exports = mongoose.model('Chat', chatSchema);
์คํค๋ง๋ฅผ index.js์ ์ฐ๊ฒฐ
๐ป.env
COOKIE_SECRET=gifchat
MONGO_ID=root
MONGO_PASSWORD=nodejsbook
๐ป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');
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();
...
๐ปshemas/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,
useCreateIndex: true,
}, (error) => {
if (error) {
console.log('๋ชฝ๊ณ ๋๋น ์ฐ๊ฒฐ ์๋ฌ', error);
} else {
console.log('๋ชฝ๊ณ ๋๋น ์ฐ๊ฒฐ ์ฑ๊ณต');
}
});
};
mongoose.connection.on('error', (error) => {
console.error('๋ชฝ๊ณ ๋๋น ์ฐ๊ฒฐ ์๋ฌ', error);
});
mongoose.connection.on('disconnected', () => {
console.error('๋ชฝ๊ณ ๋๋น ์ฐ๊ฒฐ์ด ๋๊ฒผ์ต๋๋ค. ์ฐ๊ฒฐ์ ์ฌ์๋ํฉ๋๋ค.');
connect();
});
module.exports = connect;
- views/layout.html, public/main.css, views/main.html, views/room.html, views/chat.html ์ฐธ๊ณ
- main.html์ ์ฝ๋์์ io.connect์ ์ฃผ์๊ฐ ๋ฌ๋ผ์ก๋ค๋ ์ ์ ์ฃผ๋ชฉ
- ์ฃผ์์ /room์ ๋ค์์คํ์ด์ค(๊ฐ์ ๋ค์์คํ์ด์ค๋ผ๋ฆฌ๋ง ๋ฐ์ดํฐ ์ ๋ฌ ๊ฐ๋ฅ)
- socket์๋ newRoom(์ ๋ฐฉ ์์ฑ ์ ๋ชฉ๋ก์ ๋ฐฉ ์ถ๊ฐ ์ด๋ฒคํธ)๊ณผ removeRoom(๋ฐฉ ํญํ ์ ๋ชฉ๋ก์์ ๋ฐฉ ์ ๊ฑฐ ์ด๋ฒคํธ) ์ด๋ฒคํธ ์ฐ๊ฒฐ
- chat.html์์๋ /chat ๋ค์์คํ์ด์ค์ ์ฐ๊ฒฐ
- join ์ด๋ฒคํธ(๋ฐฉ์ ์ฐธ๊ฐํ ๋ ๋ค์ด์๋ค๋ ์์คํ ๋ฉ์์ง ๋ฑ๋ก)์ exit ์ด๋ฒคํธ(๋ฐฉ์์ ๋๊ฐ ๋ ๋๊ฐ๋ค๋ ์์คํ ๋ฉ์์ง ๋ฑ๋ก) ์ฐ๊ฒฐ
socket.js ์์
- app.set(โioโ, io);๋ก ๋ผ์ฐํฐ์์ io ๊ฐ์ฒด๋ฅผ ์ธ ์ ์๊ฒ ์ ์ฅ(req.app.get(โioโ)๋ก ์ ๊ทผ ๊ฐ๋ฅ)
- io.of๋ ๋ค์์คํ์ด์ค์ ์ ๊ทผํ๋ ๋ฉ์๋
- ๊ฐ๊ฐ์ ๋ค์์คํ์ด์ค์ ์ด๋ฒคํธ๋ฅผ ๋ฐ๋ก ๊ฑธ์ด์ค ์ ์์
- req.headers.referer์ ์์ฒญ ์ฃผ์๊ฐ ๋ค์ด ์์
- ์์ฒญ ์ฃผ์์์ ๋ฐฉ ์์ด๋๋ฅผ ์ถ์ถํ์ฌ socket.join์ผ๋ก ๋ฐฉ ์ ์ฅ
- socket.leave๋ก ๋ฐฉ์์ ๋๊ฐ ์ ์์
- socket.join๊ณผ leave๋ Socket.IO์์ ์ค๋นํด๋ ๋ฉ์๋
๐ปsocket.js
const SocketIO = require('socket.io');
module.exports = (server, app) => {
const io = SocketIO(server, { path: '/socket.io' });
app.set('io', io);
const room = io.of('/room');
const chat = io.of('/chat');
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.on('disconnect', () => {
console.log('chat ๋ค์์คํ์ด์ค ์ ์ ํด์ ');
socket.leave(roomId);
});
});
};
Socket.IO์์๋ io ๊ฐ์ฒด ์๋์ ๋ค์์คํ์ด์ค์ ๋ฐฉ์ด ์์
์ต๋ช ์ฑํ ์ด๋ฏ๋ก ๋ฐฉ๋ฌธ์์๊ฒ ๊ณ ์ ์ปฌ๋ฌ ์์ด๋ ๋ถ์ฌ
๐ป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');
dotenv.config();
const webSocket = require('./socket');
const indexRouter = require('./routes');
const connect = require('./schemas');
...
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);
}
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);
app.js ์์ ํ Socket.IO ๋ฏธ๋ค์จ์ด๋ก ์ฐ๊ฒฐ
๐ป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');
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();
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(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) {
const colorHash = new ColorHash();
req.session.color = colorHash.hex(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 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');
io.use((socket, next) => {
cookieParser(process.env.COOKIE_SECRET)(socket.request, socket.request.res, next);
sessionMiddleware(socket.request, socket.request.res, 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}๋์ด ํด์ฅํ์
จ์ต๋๋ค.`,
});
}
});
});
};
to(๋ฐฉ์์ด๋).emit(์ด๋ฒคํธ, ๋ฉ์์ง)๋ก ํน์ ๋ฐฉ์ ๋ฐ์ดํฐ ์ ์ก
- ์ฌ์ฉ์๊ฐ 0๋ช ์ด๋ฉด ๋ฐฉ ํญํ ๊ธฐ๋ฅ๋ ์ถ๊ฐ
- socket.adapter.rooms[๋ฐฉ์์ด๋]์ ๋ฐฉ์ ๋ค์ด์๋ ์์ผ ์์ด๋ ๋ชฉ๋ก์ด ๋์ด
- .length๋ก ๋ฐฉ ์ธ์ ํ์ ๊ฐ๋ฅ(์ ํํ์ง๋ ์์)
- ๋ฐฉ ํญํ ๊ธฐ๋ฅ์ ์ต์คํ๋ ์ค ๋ผ์ฐํฐ๋ก ๋ฐ๋ก ๊ตฌํ
- axios๋ก ๋ผ์ฐํฐ์ ์์ฒญ์ ๋ณด๋
- ๋ฐ๋ก ๊ตฌํํ๋ ์ด์ ๋ DB ์์ ์ ๋ผ์ฐํฐ์์ ์ฒ๋ฆฌํ๋ ๊ฒ ํธํ๊ธฐ ๋๋ฌธ
๐ ์ socket.js ์ฝ๋ ์ฐธ๊ณ
routes/index.js ์์ฑ
- GET /: ๋ฉ์ธ ํ์ด์ง(๋ฐฉ ๋ชฉ๋ก) ์ ์ ๋ผ์ฐํฐ
- GET /room: ๋ฐฉ ์์ฑ ํ๋ฉด ๋ผ์ฐํฐ
- POST /room: ๋ฐฉ ์์ฑ ์์ฒญ ๋ผ์ฐํฐ
- GET /room/:id ๋ฐฉ ์ ์ฅ ๋ผ์ฐํฐ
- DELETE /room/:id ๋ฐฉ ์ ๊ฑฐ ๋ผ์ฐํฐ
- req.app.get(โioโ)๋ก io ๊ฐ์ฒด ๋ถ๋ฌ์ด
- io.of(๋ค์์คํ์ด์ค).adapter[๋ฐฉ์์ด๋]๋ก ๋ฐฉ์ ๋ค์ด์๋ ์์ผ ๋ด์ญ์ ํ์ธํ ์ ์์
- .length๋ก ๋ฐฉ ์ธ์ ํ์ธ ๊ฐ๋ฅ
- ๋ฐฉ ์ต๋ ์ธ์๋ณด๋ค ์์ ๊ฒฝ์ฐ์ ์ ์ ๊ฐ๋ฅํจ
๋ชฝ๊ณ ๋๋น์ ์๋ฒ ๋ชจ๋ ์คํ
๐ป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.on('join', function (data) {
const div = document.createElement('div');
div.classList.add('system');
const chat = document.createElement('div');
div.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');
div.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 %}
์ ์ ๊ฐ๋ฅํ ๊ฒฝ์ฐ ์ฑํ ์ ๋ถ๋ฌ์ ๋ ๋๋ง
๐ปroutes/index.js
const express = require('express');
const Room = require('../schemas/room');
const Chat = require('../schemas/chat');
const router = express.Router();
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);
}
});
router.get('/room', (req, res) => {
res.render('room', { title: 'GIF ์ฑํ
๋ฐฉ ์์ฑ' });
});
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');
io.of('/room').emit('newRoom', newRoom);
res.redirect(`/room/${newRoom._id}?password=${req.body.password}`);
} catch (error) {
console.error(error);
next(error);
}
});
router.get('/room/:id', async (req, res, next) => {
try {
const room = await Room.findOne({ _id: req.params.id });
const 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;
if (rooms && rooms[req.params.id] && room.max <= rooms[req.params.id].length) {
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);
}
});
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(() => {
req.app.get('io').of('/room').emit('removeRoom', req.params.id);
}, 2000);
} catch (error) {
console.error(error);
next(error);
}
});
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 (error) {
console.error(error);
next(error);
}
});
module.exports = router;
์ฑํ
์ DB์ ์ ์ฅ ํ ๋ฐฉ์ ๋ฟ๋ ค์ค
๐ปroutes/index.js
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 (error) {
console.error(error);
next(error);
}
});
module.exports = router;
์ฑํ
ํด๋ณด๊ธฐ
DB๋ฅผ ์ฐ์ง ์๊ณ ๋ ๋ฐ๋ก socket.emit์ผ๋ก ์ฑํ ์ ์ก ๊ฐ๋ฅ
views/chat.html
ํน์ ์ธ์๊ฒ ๋ฉ์์ง ๋ณด๋ด๊ธฐ(๊ท์๋ง, 1๋1 ์ฑํ
๋ฑ์ ์ฌ์ฉ)
๋๋ฅผ ์ ์ธํ ์ ์ฒด์๊ฒ ๋ฉ์์ง ๋ณด๋ด๊ธฐ
๐ป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.on('join', function (data) {
const div = document.createElement('div');
div.classList.add('system');
const chat = document.createElement('div');
div.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');
div.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 %}
๐ปroutes/index.js
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const Room = require('../schemas/room');
const Chat = require('../schemas/chat');
const router = express.Router();
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);
}
});
router.get('/room', (req, res) => {
res.render('room', { title: 'GIF ์ฑํ
๋ฐฉ ์์ฑ' });
});
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');
io.of('/room').emit('newRoom', newRoom);
res.redirect(`/room/${newRoom._id}?password=${req.body.password}`);
} catch (error) {
console.error(error);
next(error);
}
});
router.get('/room/:id', async (req, res, next) => {
try {
const room = await Room.findOne({ _id: req.params.id });
const 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;
if (rooms && rooms[req.params.id] && room.max <= rooms[req.params.id].length) {
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);
}
});
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(() => {
req.app.get('io').of('/room').emit('removeRoom', req.params.id);
}, 2000);
} catch (error) {
console.error(error);
next(error);
}
});
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 (error) {
console.error(error);
next(error);
}
});
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'), 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);
}
});
module.exports = router;
๐ป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());
...
๋ค์ ์ฅ์์๋ ์ต๋ช
์ ๋์ ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ค ๊ฐ์ ์ค์๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์
๐์ถ์ฒ๐
Node.js ๊ต๊ณผ์ - ๊ธฐ๋ณธ๋ถํฐ ํ๋ก์ ํธ ์ค์ต๊น์ง
https://www.inflearn.com/course/%EB%85%B8%EB%93%9C-%EA%B5%90%EA%B3%BC%EC%84%9C/dashboard