๐Ÿ”Ž์›น ์†Œ์ผ“์œผ๋กœ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „์†กํ•˜๊ธฐ

์„œ๊ฐ€ํฌยท2021๋…„ 11์›” 29์ผ
4

Node.js

๋ชฉ๋ก ๋ณด๊ธฐ
11/15
post-thumbnail

์›น ์†Œ์ผ“ ์ดํ•ดํ•˜๊ธฐ

1. ์›น ์†Œ์ผ“ ์ดํ•ดํ•˜๊ธฐ

์›น ์†Œ์ผ“: ์‹ค์‹œ๊ฐ„ ์–‘๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ์ „์†ก์„ ์œ„ํ•œ ๊ธฐ์ˆ 

  • ws ํ”„๋กœํ† ์ฝœ ์‚ฌ์šฉ -> ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ง€์›ํ•ด์•ผ ํ•จ
  • ์ตœ์‹  ๋ธŒ๋ผ์šฐ์ €๋Š” ๋Œ€๋ถ€๋ถ„ ์›น ์†Œ์ผ“์„ ์ง€์›ํ•จ
  • ๋…ธ๋“œ๋Š” ws๋‚˜ Socket.IO๊ฐ™์€ ํŒจํ‚ค์ง€๋ฅผ ํ†ตํ•ด ์›น ์†Œ์ผ“ ์‚ฌ์šฉ ๊ฐ€๋Šฅ

์›น ์†Œ์ผ“ ์ด์ „์—๋Š” ํด๋ง์ด๋ผ๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ–ˆ์Œ

  • HTTP๊ฐ€ ํด๋ผ์ด์–ธํŠธ์—์„œ ์„œ๋ฒ„๋กœ๋งŒ ์š”์ฒญ์ด ๊ฐ€๊ธฐ ๋•Œ๋ฌธ์— ์ฃผ๊ธฐ์ ์œผ๋กœ ์„œ๋ฒ„์— ์š”์ฒญ์„ ๋ณด๋‚ด ์—…๋ฐ์ดํŠธ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•จ
  • ์›น ์†Œ์ผ“์€ ์—ฐ๊ฒฐ๋„ ํ•œ ๋ฒˆ๋งŒ ๋งบ์œผ๋ฉด ๋˜๊ณ , HTTP์™€ ํฌํŠธ ๊ณต์œ ๋„ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ์„ฑ๋Šฅ๋„ ๋งค์šฐ ์ข‹์Œ

2. ์„œ๋ฒ„์„ผํŠธ ์ด๋ฒคํŠธ

SSE(Server Sent Events)

  • EventSource๋ผ๋Š” ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉ
  • ์ฒ˜์Œ์— ํ•œ ๋ฒˆ๋งŒ ์—ฐ๊ฒฐํ•˜๋ฉด ์„œ๋ฒ„๊ฐ€ ํด๋ผ์ด์–ธํŠธ์— ์ง€์†์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ด์คŒ
  • ํด๋ผ์ด์–ธํŠธ์—์„œ ์„œ๋ฒ„๋กœ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์—†์Œ

ws ๋ชจ๋“ˆ๋กœ ์›น ์†Œ์ผ“ ์‚ฌ์šฉํ•˜๊ธฐ

1. gif-chat ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ

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"
  }
}

2. ๊ธฐ๋ณธ ํŒŒ์ผ ์ž‘์„ฑ

ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•˜๊ณ  .env์™€ app.js, routes/index.js ํŒŒ์ผ ์ž‘์„ฑ

npm i

๐Ÿ”ป.env

COOKIE_SECRET=gifchat

3. ws ๋ชจ๋“ˆ ์„ค์น˜ํ•˜๊ธฐ

npm i ws๋กœ ์„ค์น˜

  • ์›น ์†Œ์ผ“์„ ์ต์Šคํ”„๋ ˆ์Šค์— ์—ฐ๊ฒฐํ•˜๊ธฐ
  • socket.js๋Š” ๋‚˜์ค‘์— ์ž‘์„ฑ

๐Ÿ”ป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);

4. socket.js ํŒŒ์ผ

ws ๋ชจ๋“ˆ์„ ๋ถˆ๋Ÿฌ์˜ด

  • new Websocket.Server({ server }) ๋กœ ์ต์Šคํ”„๋ ˆ์Šค ์„œ๋ฒ„์™€ ์—ฐ๊ฒฐ
  • connection ์ด๋ฒคํŠธ๋Š” ์„œ๋ฒ„์™€ ์—ฐ๊ฒฐ๋  ๋•Œ ์‹คํ–‰๋˜๋Š” ์ด๋ฒคํŠธ
  • req.headers[โ€˜x-forwarded-forโ€™] || req.connection.remoteAddress๋Š” ํด๋ผ์ด์–ธํŠธ์˜ IP๋ฅผ ์•Œ์•„๋‚ด๋Š” ์œ ๋ช…ํ•œ ๋ฐฉ๋ฒ•
  • message, error, close ์ด๋ฒคํŠธ๋Š” ๊ฐ๊ฐ ๋ฉ”์‹œ์ง€๊ฐ€ ์˜ฌ ๋•Œ, ์—๋Ÿฌ ๋ฐœ์ƒํ•  ๋•Œ, ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์ข…๋ฃŒํ•  ๋•Œ ํ˜ธ์ถœ
  • ws.OPEN์€ ์—ฐ๊ฒฐ ์ƒํƒœ๊ฐ€ ์—ด๋ ค์žˆ๋‹ค๋Š” ๋œป(์—ฐ๊ฒฐ๋˜์—ˆ๋‹ค๋Š” ๋œป)
  • ws.send๋กœ ๋ฉ”์‹œ์ง€ ์ „์†ก(3์ดˆ๋งˆ๋‹ค ๋ณด๋‚ด๊ณ  ์žˆ์Œ)

๐Ÿ”ป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);
  });
};

5. ํ”„๋ŸฐํŠธ์—”๋“œ์—์„œ ๋ฉ”์‹œ์ง€ ๋‹ต์žฅํ•˜๊ธฐ

index.html๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ์Šคํฌ๋ฆฝํŠธ ์ž‘์„ฑ

  • new WebSocket์€ ์ตœ์‹  ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ง€์›
  • ์ธ์ˆ˜๋กœ ์„œ๋ฒ„์˜ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅ
  • onopen ์ด๋ฒคํŠธ๋ฆฌ์Šค๋„ˆ๋Š” ์„œ๋ฒ„์™€ ์—ฐ๊ฒฐ๋˜์—ˆ์„ ๋•Œ ํ˜ธ์ถœ
  • onmessage ์ด๋ฒคํŠธ๋ฆฌ์Šค๋„ˆ๋Š” ์„œ๋ฒ„์—์„œ ๋ฉ”์‹œ์ง€๊ฐ€ ์˜ฌ ๋•Œ ํ˜ธ์ถœ
  • event.data์— ์„œ๋ฒ„ ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ์ด ๋“ค์–ด ์žˆ์Œ
  • webSocket.send๋กœ ์„œ๋ฒ„๋กœ ๋ฉ”์‹œ์ง€ ์ „๋‹ฌ ๊ฐ€๋Šฅ

๐Ÿ”ป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>

6. ์„œ๋ฒ„ ์‹คํ–‰ํ•˜๊ธฐ

http://localhost:8005 ์ ‘์†

  • F12 console ํƒญ ์‹คํ–‰
  • ์ ‘์†ํ•˜๋Š” ์ˆœ๊ฐ„๋ถ€ํ„ฐ ๋…ธ๋“œ์˜ ์ฝ˜์†”๊ณผ ๋ธŒ๋ผ์šฐ์ €์˜ ์ฝ˜์†”์— 3์ดˆ๋งˆ๋‹ค ๋ฉ”์‹œ์ง€ ์ฐํž˜

7. Network ์š”์ฒญ ํ™•์ธํ•˜๊ธฐ

๊ฐœ๋ฐœ์ž ๋„๊ตฌ์˜ Network ํƒญ ์—ด๊ธฐ

  • websocket ์š”์ฒญ ํ•œ ๋ฒˆ์œผ๋กœ ์ง€์†์ ์œผ๋กœ ๋ฐ์ดํ„ฐ ์ฃผ๊ณ  ๋ฐ›์Œ

8. ๋‹ค๋ฅธ ๋ธŒ๋ผ์šฐ์ €๋กœ๋„ ์—ฐ๊ฒฐํ•˜๊ธฐ

๋‹ค๋ฅธ ๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:8005์— ์ ‘์†

  • ์ ‘์†ํ•œ ๋ธŒ๋ผ์šฐ์ €(ํด๋ผ์ด์–ธํŠธ)๊ฐ€ ๋‘ ๊ฐœ๋ผ, ์„œ๋ฒ„๊ฐ€ ๋ฐ›๋Š” ๋ฉ”์‹œ์ง€์˜ ์–‘๋„ ๋‘ ๋ฐฐ๊ฐ€ ๋จ

9. ํด๋ผ์ด์–ธํŠธ ํ•˜๋‚˜ ์ข…๋ฃŒํ•˜๊ธฐ

๋ธŒ๋ผ์šฐ์ €๋ฅผ ํ•˜๋‚˜ ์ข…๋ฃŒํ•˜๊ธฐ

  • ์ฝ˜์†”์— ์ ‘์† ํ•ด์ œ ๋ฉ”์‹œ์ง€๊ฐ€ ๋œจ๊ณ , ๋ฉ”์‹œ์ง€์˜ ์–‘์ด ํ•˜๋‚˜๊ฐ€ ๋จ
  • ํŽธ์˜์„ฑ์„ ์œ„ํ•ด ws ๋ชจ๋“ˆ ๋Œ€์‹  Socket.IO ๋ชจ๋“ˆ ์‚ฌ์šฉ

Socket.IO ์‚ฌ์šฉํ•˜๊ธฐ

1. Socket.IO ์„ค์น˜ํ•˜๊ธฐ

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);
  });
};

2. ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ฉ”์‹œ์ง€ ์ฃผ๊ณ  ๋ฐ›๊ธฐ

index.html ์ˆ˜์ •

  • /socket.io/socket.io.js ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋„ฃ์–ด์ฃผ์–ด์•ผ ํ•จ(io ๊ฐ์ฒด ์ œ๊ณต)
  • connect ๋ฉ”์„œ๋“œ๋กœ ์„œ๋ฒ„ ์ฃผ์†Œ๋กœ ์—ฐ๊ฒฐํ•˜๊ณ  ์„œ๋ฒ„์˜ ์„ค์ •๊ณผ ๊ฐ™์€ path ์ž…๋ ฅ(/socket.io)
  • ์„œ๋ฒ„ ์ฃผ์†Œ๊ฐ€ http ํ”„๋กœํ† ์ฝœ์ž„์— ์œ ์˜
  • news ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋กœ ์„œ๋ฒ„์—์„œ ์˜ค๋Š” news ์ด๋ฒคํŠธ ๋Œ€๊ธฐ
  • socket.emit(โ€˜replyโ€™, ๋ฉ”์‹œ์ง€)๋กœ 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>

3. ์„œ๋ฒ„ ์‹คํ–‰ํ•˜๊ธฐ

http://localhost:8005์— ์ ‘์†

  • ๊ฐœ๋ฐœ์ž ๋„๊ตฌ Network ํƒญ์„ ๋ณด๋ฉด ์›น์†Œ์ผ“๊ณผ ํด๋ง ์—ฐ๊ฒฐ ๋‘˜ ๋‹ค ์žˆ์Œ ํ™•์ธ ๊ฐ€๋Šฅ
  • Socket.IO๋Š” ๋จผ์ € ํด๋ง ๋ฐฉ์‹์œผ๋กœ ์—ฐ๊ฒฐ ํ›„(์›น ์†Œ์ผ“์„ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์œ„ํ•ด), ์›น ์†Œ์ผ“์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ์›น ์†Œ์ผ“์œผ๋กœ ์—…๊ทธ๋ ˆ์ด๋“œ
  • ์›น ์†Œ์ผ“๋งŒ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด transports ์˜ต์…˜์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฃผ๋ฉด ๋จ

๐Ÿ”ป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>

์‹ค์‹œ๊ฐ„ GIF ์ฑ„ํŒ…๋ฐฉ ๋งŒ๋“ค๊ธฐ

1. ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ๊ฐ–์ถ”๊ธฐ

ํ•„์š” ํŒจํ‚ค์ง€ ์„ค์น˜ ํ›„ ์Šคํ‚ค๋งˆ ์ž‘์„ฑ

npm i mongoose multer axios color-hash

  • color-hash๋Š” ์ต๋ช… ๋‹‰๋„ค์ž„์— ์ปฌ๋Ÿฌ๋ฅผ ์ค„ ๋•Œ ์‚ฌ์šฉ

2. ์Šคํ‚ค๋งˆ ์ƒ์„ฑํ•˜๊ธฐ

์ฑ„ํŒ…๋ฐฉ ์Šคํ‚ค๋งˆ(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);

3. ์Šคํ‚ค๋งˆ ์—ฐ๊ฒฐํ•˜๊ธฐ

์Šคํ‚ค๋งˆ๋ฅผ index.js์™€ ์—ฐ๊ฒฐ

  • ์ต์Šคํ”„๋ ˆ์Šค์™€ ๋ชฝ๊ตฌ์Šค๋ฅผ ์—ฐ๊ฒฐ
  • .env ํŒŒ์ผ์— ๋น„๋ฐ€ํ‚ค ์ž…๋ ฅ

๐Ÿ”ป.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;

4. ํ”„๋ŸฐํŠธ์—”๋“œ ํŒŒ์ผ ์ž‘์„ฑ

  • 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 ์ด๋ฒคํŠธ(๋ฐฉ์—์„œ ๋‚˜๊ฐˆ ๋•Œ ๋‚˜๊ฐ”๋‹ค๋Š” ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ ๋“ฑ๋ก) ์—ฐ๊ฒฐ

5. socket.js์— ์†Œ์ผ“ ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ

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);
    });
  });
};

6. ๋ฐฉ ๊ฐœ๋… ์ดํ•ดํ•˜๊ธฐ

Socket.IO์—์„œ๋Š” io ๊ฐ์ฒด ์•„๋ž˜์— ๋„ค์ž„์ŠคํŽ˜์ด์Šค์™€ ๋ฐฉ์ด ์žˆ์Œ

  • ๊ธฐ๋ณธ ๋„ค์ž„์ŠคํŽ˜์ด์Šค๋Š” /
  • ๋ฐฉ์€ ๋„ค์ž„์ŠคํŽ˜์ด์Šค์˜ ํ•˜์œ„ ๊ฐœ๋…
  • ๊ฐ™์€ ๋„ค์ž„์ŠคํŽ˜์ด์Šค, ๊ฐ™์€ ๋ฐฉ ์•ˆ์—์„œ๋งŒ ์†Œํ†ตํ•  ์ˆ˜ ์žˆ์Œ

7. color-hash ์ ์šฉํ•˜๊ธฐ

์ต๋ช… ์ฑ„ํŒ…์ด๋ฏ€๋กœ ๋ฐฉ๋ฌธ์ž์—๊ฒŒ ๊ณ ์œ  ์ปฌ๋Ÿฌ ์•„์ด๋”” ๋ถ€์—ฌ

  • ์„ธ์…˜์— ์ปฌ๋Ÿฌ ์•„์ด๋”” ์ €์žฅ(req.session.color)

๐Ÿ”ป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);

๋ฏธ๋“ค์›จ์–ด์™€ ์†Œ์ผ“ ์—ฐ๊ฒฐํ•˜๊ธฐ

1. socket.io์—์„œ ์„ธ์…˜ ์‚ฌ์šฉํ•˜๊ธฐ

app.js ์ˆ˜์ • ํ›„ Socket.IO ๋ฏธ๋“ค์›จ์–ด๋กœ ์—ฐ๊ฒฐ

  • io.use๋กœ ์ต์Šคํ”„๋ ˆ์Šค ๋ฏธ๋“ค์›จ์–ด๋ฅผ 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}๋‹˜์ด ํ‡ด์žฅํ•˜์…จ์Šต๋‹ˆ๋‹ค.`,
        });
      }
    });
  });
};

2. ๋ฐฉ ์ž…์žฅ, ํ‡ด์žฅ ๋ฉ”์‹œ์ง€ ์ „์†กํ•˜๊ธฐ

to(๋ฐฉ์•„์ด๋””).emit(์ด๋ฒคํŠธ, ๋ฉ”์‹œ์ง€)๋กœ ํŠน์ • ๋ฐฉ์— ๋ฐ์ดํ„ฐ ์ „์†ก

  • ์‚ฌ์šฉ์ž๊ฐ€ 0๋ช…์ด๋ฉด ๋ฐฉ ํญํŒŒ ๊ธฐ๋Šฅ๋„ ์ถ”๊ฐ€
  • socket.adapter.rooms[๋ฐฉ์•„์ด๋””]์— ๋ฐฉ์— ๋“ค์–ด์žˆ๋Š” ์†Œ์ผ“ ์•„์ด๋”” ๋ชฉ๋ก์ด ๋‚˜์˜ด
  • .length๋กœ ๋ฐฉ ์ธ์› ํŒŒ์•… ๊ฐ€๋Šฅ(์ •ํ™•ํ•˜์ง€๋Š” ์•Š์Œ)
  • ๋ฐฉ ํญํŒŒ ๊ธฐ๋Šฅ์€ ์ต์Šคํ”„๋ ˆ์Šค ๋ผ์šฐํ„ฐ๋กœ ๋”ฐ๋กœ ๊ตฌํ˜„
  • axios๋กœ ๋ผ์šฐํ„ฐ์— ์š”์ฒญ์„ ๋ณด๋ƒ„
  • ๋”ฐ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ์ด์œ ๋Š” DB ์ž‘์—…์„ ๋ผ์šฐํ„ฐ์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒŒ ํŽธํ•˜๊ธฐ ๋•Œ๋ฌธ
    ๐Ÿ‘‰ ์œ„ socket.js ์ฝ”๋“œ ์ฐธ๊ณ 

3. ๋ผ์šฐํ„ฐ ์ž‘์„ฑํ•˜๊ธฐ

routes/index.js ์ž‘์„ฑ

  • GET /: ๋ฉ”์ธ ํŽ˜์ด์ง€(๋ฐฉ ๋ชฉ๋ก) ์ ‘์† ๋ผ์šฐํ„ฐ
  • GET /room: ๋ฐฉ ์ƒ์„ฑ ํ™”๋ฉด ๋ผ์šฐํ„ฐ
  • POST /room: ๋ฐฉ ์ƒ์„ฑ ์š”์ฒญ ๋ผ์šฐํ„ฐ
  • GET /room/:id ๋ฐฉ ์ž…์žฅ ๋ผ์šฐํ„ฐ
  • DELETE /room/:id ๋ฐฉ ์ œ๊ฑฐ ๋ผ์šฐํ„ฐ
  • req.app.get(โ€˜ioโ€™)๋กœ io ๊ฐ์ฒด ๋ถˆ๋Ÿฌ์˜ด
  • io.of(๋„ค์ž„์ŠคํŽ˜์ด์Šค).adapter[๋ฐฉ์•„์ด๋””]๋กœ ๋ฐฉ์— ๋“ค์–ด์žˆ๋Š” ์†Œ์ผ“ ๋‚ด์—ญ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Œ
  • .length๋กœ ๋ฐฉ ์ธ์› ํ™•์ธ ๊ฐ€๋Šฅ
  • ๋ฐฉ ์ตœ๋Œ€ ์ธ์›๋ณด๋‹ค ์ž‘์€ ๊ฒฝ์šฐ์— ์ ‘์† ๊ฐ€๋Šฅํ•จ

4. ๋ฐฉ ์ƒ์„ฑํ•˜๊ธฐ

๋ชฝ๊ณ ๋””๋น„์™€ ์„œ๋ฒ„ ๋ชจ๋‘ ์‹คํ–‰

  • ๋ธŒ๋ผ์šฐ์ € ๋‘ ๊ฐœ๋ฅผ ๋„์›Œ http://localhost:8005์— ์ ‘์†**
  • ๋ช…์ด ์ ‘์†ํ•œ ๊ฒƒ๊ณผ ๊ฐ™์€ ํšจ๊ณผ
  • ์ƒ์„ฑํ•ด๋ณด๊ธฐ

์ฑ„ํŒ… ๊ตฌํ˜„ํ•˜๊ธฐ

1. ์ฑ„ํŒ… ์†Œ์ผ“ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋ถ™์ด๊ธฐ

  • chat ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์ถ”๊ฐ€ํ•จ. ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€๊ฐ€ ์›น ์†Œ์ผ“์œผ๋กœ ์ „์†ก๋  ๋•Œ ํ˜ธ์ถœ๋จ
  • event.data.user(์ฑ„ํŒ… ๋ฐœ์†ก์ž)์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒŒ ๋ Œ๋”๋ง

๐Ÿ”ป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 %}

2. ๋ฐฉ์— ์ ‘์†ํ•˜๋Š” ๋ผ์šฐํ„ฐ ๋งŒ๋“ค๊ธฐ

์ ‘์† ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ ์ฑ„ํŒ…์„ ๋ถˆ๋Ÿฌ์™€ ๋ Œ๋”๋ง

๐Ÿ”ป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;

3. ์ฑ„ํŒ… ๋ผ์šฐํ„ฐ ๋งŒ๋“ค๊ธฐ

์ฑ„ํŒ…์„ 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;

4. ์ฑ„ํŒ… ํ™”๋ฉด

์ฑ„ํŒ… ํ•ด๋ณด๊ธฐ

5. ์›น ์†Œ์ผ“๋งŒ์œผ๋กœ ์ฑ„ํŒ… ๊ตฌํ˜„ํ•˜๊ธฐ

DB๋ฅผ ์“ฐ์ง€ ์•Š๊ณ ๋„ ๋ฐ”๋กœ socket.emit์œผ๋กœ ์ฑ„ํŒ… ์ „์†ก ๊ฐ€๋Šฅ

  • chat.html, app.js ์ˆ˜์ •ํ•˜๊ธฐ

views/chat.html

6. ๊ธฐํƒ€ Socket.IO API

ํŠน์ •์ธ์—๊ฒŒ ๋ฉ”์‹œ์ง€ ๋ณด๋‚ด๊ธฐ(๊ท“์†๋ง, 1๋Œ€1 ์ฑ„ํŒ… ๋“ฑ์— ์‚ฌ์šฉ)

๋‚˜๋ฅผ ์ œ์™ธํ•œ ์ „์ฒด์—๊ฒŒ ๋ฉ”์‹œ์ง€ ๋ณด๋‚ด๊ธฐ

7. GIF ์ „์†ก ๊ตฌํ˜„

๐Ÿ”ป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;
  • ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ์ด๊ธฐ ๋•Œ๋ฌธ์— multer ์‚ฌ์šฉ
  • ์ด๋ฏธ์ง€ ์ €์žฅ ํ›„ ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ chat ๋ฐ์ดํ„ฐ์— ๋ฟŒ๋ฆผ
  • ์ด๋ฏธ์ง€๋ฅผ ์ œ๊ณตํ•  static ํด๋” ์—ฐ๊ฒฐ

๐Ÿ”ป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());
...

8. GIF ์ฑ„ํŒ… ํ™”๋ฉด

๋‹ค์Œ ์žฅ์—์„œ๋Š” ์ต๋ช…์ œ ๋Œ€์‹  ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๋“ค ๊ฐ„์— ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ  ๋ฐ›์Œ

๐Ÿ˜ƒ์ถœ์ฒ˜๐Ÿ˜ƒ
Node.js ๊ต๊ณผ์„œ - ๊ธฐ๋ณธ๋ถ€ํ„ฐ ํ”„๋กœ์ ํŠธ ์‹ค์Šต๊นŒ์ง€
https://www.inflearn.com/course/%EB%85%B8%EB%93%9C-%EA%B5%90%EA%B3%BC%EC%84%9C/dashboard

0๊ฐœ์˜ ๋Œ“๊ธ€