상세 컨텐츠

본문 제목

[Node.js] 12장(2) 미들웨어와 소켓 연결하기

21-22/21-22 Node.js

by Kimpeep 2022. 1. 10. 13:31

본문

728x90

5. 미들웨어와 소켓 연결하기

입퇴장하는 사람의 ID를 보여주고 싶은데, 그 정보는 req.session.color에 저장되어 있으므로 Socket.IO에서

지금은 접근이 불가합니다. 이를 해결하기 위해 우리는 Socket.IO도 미들웨어 사용하다는 점을 이용해봅시다.

 

(1) app.js

Socket.IO도 미들웨어 사용이 가능하므로 express-session을 Socket에 공유하면 됩니다.

기존에는 app.use(session ~) 과 같은 형태로 세션을 use해주기만 했다면,

이제는 아래의 코드처럼 sessionMiddleware에 세션을 넣고 webSocket에도 담아주면 됩니다.

//app.js

// 미들웨어 설정
const sessionMiddleware = session({
  resave: false,
  saveUninitialized: false,
  secret: process.env.COOKIE_SECRET,
  cookie: {
    httpOnly: true,
    secure: false,
  },
});

...

app.use(sessionMiddleware);

...

webSocket(server, app, sessionMiddleware);

(2) socket.js

const axios = require('axios');

// 1. 미들웨어 장착, 모든 웹 소켓 연결 시마다 실행됨
io.use((socket, next) => {
	// 미들웨어 요청 객체, 미들웨어 응답 객체, next
    sessionMiddleware(socket.request, socket.request.res, next);
});

chat.on('connection', (socket) => {
	...
    
    // 2. socket.to(방아이디) : 특정 방에 데이터 전송
    // Socket.IO와 미들웨어 연결 성공 -> 세션 객체인 req.session.color에 접근 가능
    socket.to(roomId).emit('join', {
        user: 'system',
        chat:`${req.session.color}님이 입장하셨습니다.`,
    });
    
    ...
    // 3. 접속 해제 시 현재 방의 사람수를 구해 참여자 수가 0명이면 방을 제거하는 요청을 보냄
    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 {
      	// 참여자수가 0이 아니면 퇴장 메시지만 보냄
        socket.to(roomId).emit('exit', {
          user: 'system',
          chat: `${req.session.color}님이 퇴장하셨습니다.`,
        });
      }
    
    
}

(3) routes/index.js

라우터에서는 몽고DB와 웹소켓에 모두 접근 가능

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'); // app.set('io', io)로 설정
    
    io.of('/room').emit('newRoom', newRoom); // room 네임스페이스에 연결한 모든 클라이언트에게 전달
    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=허용 인원이 초과하였습니다.');
    }
    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(() => {
    	// 삭제한 후 2초 뒤에 /room으로 방 삭제를 알림
      req.app.get('io').of('/room').emit('removeRoom', req.params.id);
    }, 2000);
  } catch (error) {
    console.error(error);
    next(error);
  }
});

module.exports = router;

6. 채팅 구현하기

(1) views/chat.html

chat이벤트는 채팅 메시지가 웹 소켓으로 전송될 때 호출됩니다. 채팅 메시지 발송자(data.user)가 본인인지 아닌지에 따라 다르게 렌더링합니다. 

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
...
	// chat리스너 추가
    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) 라우터를 거치지 않고 채팅을 구현하는 법

  • views/chat.html
          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);
        //       });
                socket.emit('chat', {
                    room: '{{room._id}}',
                    user: '{{user}}',
                    chat: e.target.chat.value
                });
                e.target.chat.value='';
           }
  • socket.js
    chat.on('connection', (socket) => {
    	...
    	socket.on('chat', (data)=>{
        socket.to(data.room).emit(data);
        });
    });
     

chat.html에서 보내고 socket.js에서 on메소드를 통해 받는 모습

 

(4) 기타 Socket.IO API

  • 특정인에게 메시지 보내기
    • socket.to(소켓 아이디).emit(이벤트, 데이터);
  • 나를 제외한 모두에게 메시지 보내기
    • socket.broadcast.emit(이벤트, 데이터);
    • socket.broadcast.to(방아이디).emit(이벤트, 데이터);

 

7. 프로젝트 마무리하기

(1) views/chat.html

프런트 화면에서 이미지를 선택해 업로드하는 이벤트 리스너 추가

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

 

(2) routes/index.js

위에서 POST /room/{{room._id}}/gif 명령을 보냈으므로 응답할 라우터 작성

try {
  fs.readdirSync('uploads');
} catch (err) {
  console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
  fs.mkdirSync('uploads');
}
// 추가
const multer = require('multer');
const path = require('path');
const fs = require('fs');

// 9장의 이미지 업로드 방식
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);
  }
});

 

(3) app.js

이미지를 제공할 uploads 폴더를 express.static 미들웨어로 연결

app.use('/gif', express.static(path.join(__dirname, 'uploads')));
728x90

관련글 더보기