본문 바로가기

웹 프로그래밍/BackEnd

[Express, nodejs] SPA에서 socket.io로 채팅 구현하기

프로젝트를 진행하면서 SPA로 웹페이지를 만들고 그 안에 채팅기능을 넣는 경험을 해보았다.

저번 Websocket vs socket.io의 비교에 이어서 오늘은 구현을 짧게 정리해보려고 한다.

 

* 백엔드와 프론트 부분으로 나누어서 코드를 설명할 예정이다.

 

먼저 백엔드부분

express-generator를 사용하면 자동으로 폴더 구조가 만들어질 것이다. 

그중에서 실행되는 파일인 bin/www파일에 socket.io를 연결해 줄 것이다.

 

 

/bin/www

let server = http.createServer(app);

이렇게 www파일 내부를 보면 서버를 만들어 주는 부분이 있을것이다.

만들어진 서버를 socket.io에 연결해 사용할 것이다.

 

 

let io = require('socket.io')(server,{ cors: { origin: "*" } });

이와 같이 sockrt.io를 서버에 연결했다.

뒷부분의 cors는 추가적인 옵션이다.(모든 도메인의 요청을 허락한다는..!)

이렇게 하면 서버에 socket.io의 연결이 되었다.

 

그렇다면 메세지를 어떻게 주고 받을까?

/bin/www 의 전체 코드

io.sockets.on('connection', (socket) => {
  console.log('socket connect');
  socket.on('disconnect', () => {
    console.log('user disconnected');
  });

  socket.on('leaveRoom', (roomId) => {
    console.log(roomId)
    socket.leave(roomId);
  });

  socket.on('joinRoom', (roomId) => {
    console.log("join",roomId)
    socket.join(roomId);
    io.to(roomId).emit('joinRoom', roomId);
  });

  socket.on('chat message', (message,roomId) => {
    socket.broadcast.to(roomId).emit('chat message',message);
    // io.to(roomId).emit('chat message',message);
  });
  
});

위는 메시지를 주고 받는 서버의 과정이다.

하나하나 뜯어서 살펴보자

 

 

io.sockets.on('connection', (socket) => {
  console.log('socket connect');
}

일단 이부분은 socket.io에 연결이 되면 socket connect라는 출력을 해주는 역할을 한다.

이를 통해 서버가 연결되었는지 확인 할 수 있다.

앞으로 on은 받는것, emit은 보내는 것이라고 이해하면 쉽다!

 

 

socket.on('disconnect', () => {
    console.log('user disconnected');
  });

connection 내부의 이 부분은 socket.io의 연결이 끊어지면 user disconnected 출력을 한다.

 

 

여기서부터는 프론트에서 어떤 요청을 보내는지에 따라 다른 역할을 한다고 보면 된다.
socket.on('leaveRoom', (roomId) => {
    console.log(roomId)
    socket.leave(roomId);
  });

이 부분은 leaveRoom이라는 이벤트를 받았을때 실행되고 해당 방번호를 출력하고 해당 방과의 연결을 끊는 역할을 한다.

 

 

socket.on('joinRoom', (roomId) => {
    console.log("join",roomId)
    socket.join(roomId);
    io.to(roomId).emit('joinRoom', roomId);
  });

이 부분은 위와 반대로 joinRoom이라는 이벤트를 받았을때 해당 방에 join 을 해주고 방에 속해있는 사용자에게 joinRoom이라는 이벤트를 발생시키는 역할을 한다.

socket.io에서는 room별로 다른 아이디를 만들어 따로 따로 이벤트 관리를 할 수 있다.

 

 

socket.on('chat message', (message,roomId) => {
    socket.broadcast.to(roomId).emit('chat message',message);
    // io.to(roomId).emit('chat message',message);
  });

마지막으로 chat message 라는 이벤트를 받았을때는 파라미터로 받은 roomId에 해당하는 방만 broadcast를 시켜준다.

broadcast는 해당 이벤트를 보낸 당사자를 제외한 모두에게 이벤트를 발생시키는 것이다.

 

이제 백엔드의 설정은 마무리 되었다!

 

프론트 엔드의 socket.io부분만 남았다!!

 

 

프론트엔드 

여기서부터는 프로젝트마다 폴더 구조나 사용방식이 많이 다를수도 있다.

일단 나는 socket.js라는 파일을 분리하고, export시킨 다음 다른 파일에서 필요할때 가져다 쓰는 방식을 적용했다.

 

socket.js의 전체코드

import {io} from 'socket.io-client';

export const socket = io.connect('http://localhost:8000');

export const sendMessage = (e,roomId)=>{
    
    const message = document.querySelector('.send');
    socket.emit('chat message', message.value,roomId);
    document.querySelector('.message-list')?.appendChild(makeMessage(message.value,true));
    message.value='';

}
export const socketOnMessage = ()=>{
    socket.on('chat message',(message)=>{
        document.querySelector('.message-list').appendChild(makeMessage(message,false));
    });
}

export const makeMessage = (message,isMine) => {
    const msgBox = document.createElement('div');
    msgBox.className = isMine? "my-message": "other-message";
    msgBox.innerText = message;
    return msgBox;
}

이것도 하나하나 뜯어보자

 

 

import {io} from 'socket.io-client';

export const socket = io.connect('http://localhost:8000');

제일 처음 이 부분은 socket.io를 불러오고 연결하는 부분이다. 

프론트 엔드에서는 socket.io-client를 설치해 사용했을때 오류 없이 진행되었다.

현재 백엔드의 포트 넘버가 8000이기 때문에 localhost의 8000번 포트로 socket.io를 연결해주었다.

 

 

export const sendMessage = (e,roomId)=>{
    
    const message = document.querySelector('.send');//input 태그
    socket.emit('chat message', message.value,roomId);
    document.querySelector('.message-list')?.appendChild(makeMessage(message.value,true));//메세지 출력태그
    message.value='';

}

이 부분은 메세지를 보내는 부분으로 이 함수를 호출하게 되면 socket.emit을 통해 chat message라는 이벤트를 발생시키고 파라미터로 message.value와 roomId를 넘겨주게 된다.

그 다음에 메시지를 화면에 추가시켜주고 입력이 완료된 input태그의 값은 초기화 시켜준다.

 

 

export const socketOnMessage = ()=>{
    socket.on('chat message',(message)=>{
        document.querySelector('.message-list').appendChild(makeMessage(message,false));
    });
}

이 부분은 메세지 이벤트를 받는 부분으로 chat message라는 이벤트를 받게되면 화면에 내용을 출력하는 역할이다.

이 부분은 다른 사용자가 메시지를 보낼 때 당사자를 제외한 모든 방 인원들에게 broadcast로 이벤트가 전달 되므로 이때 상대방의 메세지를 화면에 출력하는 역할을 한다.

 

 

export const makeMessage = (message,isMine) => {
    const msgBox = document.createElement('div');
    msgBox.className = isMine? "my-message": "other-message";
    msgBox.innerText = message;
    return msgBox;
}

 이 부분은 socket.io와 관련된 부분이 아니지만 메시지 박스(div 태그)를 만들어주는 역할을 한다.

 

 

이로써 socket.io를 이용한 채팅구현이 완료되었다. 
프론트의 나머지 부분에서 각자 프로젝트에 맞게 위의 세 함수를 사용하면 된다!!
import {socket,makeMessage,sendMessage,socketOnMessage} from '../../util/socket';


socket.emit('joinRoom',json);

이와 같이 다른 파일에서 import받은 다음 이벤트를 날리거나 받으면 된다.

 

 

 

+ 추가로 SPA를 구현하면 history API를 이용해 라우터를 구현하는데 window.history.back();popstate를 이용해 이전으로 넘어 갔다 다시 채팅방에 들어오게 되면  chat message의 on이벤트가 계속 쌓여 있어 두번,세번...이 화면에 나타난다.

이때 socket.removeAllListeners('chat message'); 를 이용하면 chat message에 달려있는 모든 on이벤트를 삭제해주어 화면에 메세지가 한개만 출력된다.

 

 

최종구현 결과

 

 

끝!

반응형
SMALL

'웹 프로그래밍 > BackEnd' 카테고리의 다른 글

[Docker] Docker란?  (0) 2021.11.11
[Express] pm2로 로컬에서 배포하기  (0) 2021.10.20
[네트워크] Websocket과 socket.io  (0) 2021.10.06
[Backend] OAuth란?(+ 동작방법)  (3) 2021.09.30
[Backend] ORM과 Sequelize  (0) 2021.09.29