์ƒ์„ธ ์ปจํ…์ธ 

๋ณธ๋ฌธ ์ œ๋ชฉ

[๋…ธ๋“œ 2ํŒ€] #12. ์›น ์†Œ์ผ“์œผ๋กœ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „์†กํ•˜๊ธฐ

24-25/Node.js 2

by sksmsyena 2025. 1. 17. 13:28

๋ณธ๋ฌธ

728x90

๐ŸŒŸํ‚ค์›Œ๋“œ: ์›น ์†Œ์ผ“, WS ํ”„๋กœํ† ์ฝœ, Socket.IO, ๋ฏธ๋“ค์›จ์–ด, ์ปจํŠธ๋กค๋Ÿฌ


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

1. ์›น ์†Œ์ผ“
- ์–‘๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ์ „์†ก์„ ์œ„ํ•œ ๊ธฐ์ˆ ์ด๋‹ค.
- WS ํ”„๋กœํ† ์ฝœ์„ ์‚ฌ์šฉํ•œ๋‹ค.

โ€ป ์„œ๋ฒ„์„ผํŠธ ์ด๋ฒคํŠธ(Server Sent Events): Event Source ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•ด ์ฒ˜์Œ์— ํ•œ ๋ฒˆ๋งŒ ์—ฐ๊ฒฐํ•˜๋ฉด ์„œ๋ฒ„๊ฐ€ ํด๋ผ์ด์–ธํŠธ์— ์ง€์†์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ธ๋‹ค. ์ฆ‰, ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ด๋Š” ๋‹จ๋ฐ˜ํ–ฅ ํ†ต์‹ 

 

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

์œ„์™€ ๊ฐ™์€ ๋””๋ ‰ํ„ฐ๋ฆฌ ๊ตฌ์กฐ๋กœ ํ”„๋กœ์ ํŠธ์™€ ํŒŒ์ผ์„ ์ž‘์„ฑํ•œ๋‹ค. 
- ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•˜๊ณ  .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 ํƒญ์— ๋“ค์–ด๊ฐ€๋ฉด ์›น ์†Œ์ผ“์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค. 
 

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

๊ตฌํ˜„ํ•˜๋ ค๋Š” ์„œ๋น„์Šค๊ฐ€ ๋ณต์žกํ•ด์ง„๋‹ค๋ฉด 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 ํ”„๋กœํ† ์ฝœ ์‚ฌ์šฉ)
โ€ป ์›น ์†Œ์ผ“์„ ์ง€์›ํ•˜๋Š” ๋ธŒ๋ผ์šฐ์ €๋Š” ์›น ์†Œ์ผ“ ๋ฐฉ์‹์œผ๋กœ, ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ธŒ๋ผ์šฐ์ €๋Š” ํด๋ง ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.

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

- ์‚ฌ๋žŒ๋“ค์ด ์ต๋ช…์œผ๋กœ ์ƒ์„ฑํ•œ๊ณ  ์ž์œ ๋กญ๊ฒŒ ์ฐธ์—ฌํ•˜๋ฉฐ 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 ์— ์ ‘์†ํ•˜๋ฉด ์‹คํ–‰๋˜๋Š” ํ™”๋ฉด
 

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

- ์‚ฌ์šฉ์ž์˜ ์ด๋ฆ„์„ ์ฑ„ํŒ…๋ฐฉ์— ํ‘œ์‹œํ•˜๊ณ , '~๋‹˜์ด ์ž…์žฅํ•˜์…จ์Šต๋‹ˆ๋‹ค' ๊ฐ™์€ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๊ธฐ

//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(๋ฐฉ ์•„์ด๋””)๋งŒ ํ•„์š”ํ•˜๋‹ค.

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

- ํ”„๋ŸฐํŠธ์—์„œ๋Š” ์„œ๋ฒ„์—์„œ ๋ณด๋‚ด๋Š” ์ฑ„ํŒ… ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์„ ์†Œ์ผ“ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๊ฐ€ ํ•„์š”ํ•˜๊ธฐ ๋•Œ๋ฌธ์— 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 ์ฐธ๊ณ 

7. ํ”„๋กœ์ ํŠธ ๋งˆ๋ฌด๋ฆฌํ•˜๊ธฐ

- 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 ๋ฏธ๋“ค์›จ์–ด๋กœ ์—ฐ๊ฒฐํ•œ๋‹ค.
 

ํ•ต์‹ฌ ์ •๋ฆฌ

  • ์›น ์†Œ์ผ“๊ณผ HTTP๋Š” ๊ฐ™์€ ํฌํŠธ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋”ฐ๋กœ ํฌํŠธ๋ฅผ ์„ค์ •ํ•  ํ•„์š” ์—†๋‹ค.
  • ์›น ์†Œ์ผ“์€ ์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์ด๋ฏ€๋กœ ์„œ๋ฒ„๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ํ”„๋ก ํŠธ์—”๋“œ ์ชฝ ์Šคํฌ๋ฆฝํŠธ๋„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.
  • Socket.IO๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์›น ์†Œ์ผ“์„ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ๊นŒ์ง€ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • Socket.IO ๋„ค์ž„์ŠคํŽ˜์ด์Šค์™€ ๋ฐฉ ๊ตฌ๋ถ„์„ ํ†ตํ•ด ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ํ•„์š”ํ•œ ์‚ฌ์šฉ์ž์—๊ฒŒ๋งŒ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค.
  • app.set('io', io)๋กœ ์†Œ์ผ“ ๊ฐ์ฒด๋ฅผ ์ต์Šคํ”„๋ ˆ์Šค์™€ ์—ฐ๊ฒฐํ•˜๊ณ , req.app.get('io')๋กœ ๋ผ์šฐํ„ฐ์—์„œ ์†Œ์ผ“ ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐฉ์‹
  • ์†Œ์ผ“ ํ†ต์‹ ๊ณผ ํ•จ๊ป˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์กฐ์ž‘์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ, ์†Œ์ผ“๋งŒ์œผ๋กœ ํ•ด๊ฒฐํ•˜๊ธฐ๋ณด๋‹ค๋Š” HTTP ๋ผ์šฐํ„ฐ๋ฅผ ๊ฑฐ์น˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

 


Quiz

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());
...

 
 


Answer

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

728x90

๊ด€๋ จ๊ธ€ ๋”๋ณด๊ธฐ