[node.js]socket.io를 활용한 채팅 구현

Donghun Seol·2022년 11월 13일
1

웹소켓

웹소켓이란?

웹소켓 이전

  • HTTP를 이용해서 폴링이라는 방식으로 실시간 전송을 구현했음

웹소켓 개요

  • HTML5에 처음 추가됨
  • 실시간 양방향 데이터 전송을 위한 기술

웹소켓 장점

  • 최초 연결만 성공하면 브라우저와 서버가 지속적으로 연결된다.
  • HTTP와 포트 공유 가능하며 폴링에 비해 성능도 비약적으로 개선됨
  • Socket.IO를 통해 편리하게 활용가능하다.

socket.io 활용

app.js

const webSocket = require('./socket') // 루트 폴더의 socket.js

/* express server configurations */
const server = app.listen(port, /*...*/);

webSocket(server, app, sessionMiddleware) 
// webSocket 함수안에서 서버를 실행하고, app객체와 세션활용을 위한 미들웨어를 전달한다.

socket.js

  1. 클라이언트에서 이 파일을 기존으로 서버와 소켓통신을 주고받는다.
  2. 네임스페이스로 이벤트의 전달범위를 설정한다.
  3. app객체에 io객체를 삽입하여 라우터에서 io를 활용할 수 있게 해준다. 일부 이벤트는 라우터의 컨트롤러 부분에서 직접 호출해주기도 하는데 이를 위해서 io객체를 미리 셋업해준다.
  4. socket.io 4버전이 되면서 미들웨어 활용방식이 달라졌는데, 달라진 내용 주의할것.
  5. socket.js에서 axios 요청을 보낼 때 요청자에 대한 정보가 들어있지 않으므로 요청헤더에 직접 쿠키를 넣는 부분을 상세히 알아두자
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'); 
  // 미들웨어 chat 소켓라우터에 넣기 io객체에 직접 넣으면 작동하지 않는다.
  chat.use((socket, next) => cookieParser(process.env.COOKIE_SECRET)(socket.request, {}, next));
  chat.use((socket, next) => sessionMiddleware(socket.request, {}, 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}님이 퇴장하셨습니다.`,
        });
      }
    });
    socket.on('chat', (data) => {
      socket.to(data.room).emit(data);
    });
  });
};

데이터 주고받기

라우터를 통해 구현된 채팅의 경우 한번의 채팅입력은 다음과 같은 절차로 처리된다.
1. 브라우저 이벤트리스너가 채팅입력을 감지하면 서버로 post 요청을 날린다.
2. 서버 라우터를 통해 req.body로 채팅입력을 받고, 채팅 내역을 db에 저장한다.
3. 라우터에서 socket.js에서 미리 app객체에 넣은 req.app.get('io')를 활용해서 'chat'이벤트를 emit한다.
4.req.app.get('io').of('/chat').to(req.params.id).emit('chat', chat) 부분이 중요한데, post요청하는 url에 포함된 param인 id를 기준으로 socket.io내부에서 라우팅하여 관련된 id에 join해 있는 소켓들에게만 이벤트를 방출하는 것
5. 브라우저 스크립트의 socket객체는 'chat'이벤트가 발생했음을 감지하고, chat이벤트에 포함된 데이터
객체인 chat을 파싱해서 화면에다 뿌린다.

chat.html 파일의 스크립트태그 안 자바스크립트
'chat'이벤트 리스너와 채팅입력 button에 대한 이벤트리스너가 있다.

 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 {
      console.log('gif obj detected')
      console.log(data)
      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) {
      console.log('채팅방 버튼 클릭!')
      axios
        .post('/room/{{room._id}}/chat', {
          chat: this.chat.value,
        })
        .then(() => {
          e.target.chat.value = ''
        })
        .catch((err) => {
          console.error(err)
        })
    }
  })

index.js(router) 라우터 안의 채팅 post요청을 처리하는 부분. 상술한 로직이 코드로 표현되어 있음

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 (err) {
    console.error(err);
    next(err);
  }
});
profile
I'm going from failure to failure without losing enthusiasm

1개의 댓글

comment-user-thumbnail
2022년 11월 14일

http 단에서 채팅을 ㄷㄷ 수고를 좀 덜겠어요.

답글 달기