socket.io로 실시간 채팅 구현 1탄

요들레이후·2023년 6월 20일
2

프로젝트

목록 보기
4/5
post-thumbnail

우리의 프로젝트는 원래 존재했던 엘리스랩의 좌석 예약 시스템에 대해 불편함을 느끼고 기존의 웹사이트를 뜯어 고친 사이트이다.
기존에는 엘리스랩 관리자와 채팅을 할 수 있는 기능이 없었고, 디코로 문의사항을 주고 받았었어야 했다.
이에 불편함을 느껴 실시간 채팅 기능을 구현하게 되는데...

1탄에서는 websocket, socket.io의 개념과 선언 방식, API 정리에 대해 서술해보겠다.

2탄에서는 경험했던 이벤트 기획, 구현과정 중 경험했던 이슈와 해결방안에 대해 서술하고 결과 코드를 작성할 예정이다.

1. socket이 뭐람?

socket을 사용하여 실시간 채팅을 구현 썰을 풀기 전에 socket에 대해 설명해보겠다.

HTTP vs socket 통신

  • HTTP통신
    • http통신을 통해 클라이언트에서 서버로 요청을 보내고 서버가 응답하는 방식으로 통신이 이루어진다.
    • JSON, Image, HTML 파일 등 다양한 파일을 전송 받을 수 있다.
    • 응답을 받은 후 Connection이 끊어지는 것이 기본 동작이다. (필요하면 Keep Alive옵션을 주어 일정 시간동안 Connection을 유지하는 것이 가능하다)
  • socket통신
    • 소켓이라는 단어가 두 프로그램이 서로 데이터를 주고 받을 때 양쪽에 생성되는 통신 단자이다.
    • 서버와 클라리언트가 양방향 연결이 이루어지는 통신. 서로에게 데이터 전달이 가능하다.
    • 계속해서 Connection을 들고 있어 스트리밍이나 실시간 채팅 등 실시간으로 데이터를 주고 받아야 하는 경우 사용 적합하다.

우리는 지속적인 연결을 유지하여 실시간 채팅을 구현할 것이니 당연히 socket통신을 선택하였다.

webSocket vs socket.io

  • webSocket
    • HTML5 표준안의 일부 기술
    • 소켓을 이용하여 서버와 클라이언트 사이에서 자유롭게 데이터를 주고 받는 양방향 통신을 가능하게 한다.
    • 매우 빠르게 작동하며 통신할 때 아주 적은 데이터를 이용, 이벤트를 단순히 듣고 보내는 것만 가능
  • socket.io
    • 표준 기술이 아니고 라이브러리
    • node.js기반으로 만들어진 기술, 자바스크립트로 구현되어있으며 현존하는 대부분의 실시간 웹 기술들을 추상화해 놓아 브라우저 종류에 상관없이 실시간 웹 구현할 수 있도록 한 기술
    • 방 개념 이용해 일부 클라이언트에게만 데이터 전송하는 브로드캐스팅 가능

우리는 연결된 사용자들을 세밀하게 관리해야 하는 서비스이다. 관리자와 이용자의 n:1 채팅방이고 각각의 1:1 채팅방을 구현했어야 했기에 유지보수 측면에서 이점을 얻기 위해 socket.io를 사용하기로 결정

데이터 전송이 많은 경우는 빠르고 비용이 적은 표준 WebSocket을 이용하는 게 바람직하다고 한다.
웹소켓과 socket.io
스택오버플로우 질문

2. socket.io 사용법

이벤트를 주고 받기 위해선 socket.io의 API 사용법에 대해 알아야한다.
용도에 따라 사용할 메서드가 달라지기 때문이다.
초반에 아무것도 모르고 socket.on, socket.emit으로만 했다가 큰코다쳤다.

우선 다음은 기본적으로 서버와 프론트에서의 socket 선언 방식이다.

  • 서버

1. npm 설치

npm install socket.io@version

2. HTTP 서버와 함께 사용할 때 선언 방식
socket.io 서버만 사용할 때의 선언 방식과 HTTP서버와 함께 사용할 때 선언방식이 나뉘어져 있다. 그에 대한 방식은 공식문서에 잘 나와있다.

cors 설정하는 방법도 공식문서에 잘 나와있다.
Handling CORS

const app = express();
const server = http.createServer(app);  // HTTP 서버를 생성

const io = new Server(server, {   // 소켓 서버 인스턴스 생성, 첫 번째 인자로 HTTP 서버 전달
  cors: {  // 두번째 인자는 서버 구성 옵션 객체, CORS활성화
    origin: true,
    methods: ['GET', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true,
  },
});

server.listen(PORT, () => {
  console.log(`Socket server on!!`);
}); // 서버 시작하고 지정된 포트에서 수신 대기
  • 클라이언트

1. npm 설치

npm install socket.io-client

2. 선언 방식
클라이언트 선언 방식은 비교적 간단하다.
우선 설치한 라이브러리를 불러온다음, 클라이언트를 생성한다.

공식문서에 클라이언트 선언 방식을 보면, 첫번째 인자 url밖에 없을 것이다.

처음에 url만 입력했었을 때 연결이 제대로 되지 않았고, 두번째 인자에 여러 설정값들을 제대로 입력하고 나니 연결이 제대로 되었다.

그에 대한 내용 또한 Troubleshooting connection issues
여기 공식문서에 친절하게 설명이 되어있다.

socket.io는 첫 연결 때 Polling( http로 실시간 데이터 전송 방법)으로 연결하고 웹소켓을 사용할 수 있는 환경이면 websocket으로 업그레이드한다.

만약 처음부터 Polling 방식이 아닌 WebSocket 방식으로 연결하려면 transports: ['websocket']를 추가한다.

나는 path설정과 transports 설정을 해주었다.

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

const socket = io(`${process.env.REACT_APP_SOCKET_ENDPOINT}`, {
  path: '/socket.io/',    // socket.io 경로 설정, Socket.IO는 기본적으로 "/socket.io/" 경로를 사용하여 서버와 클라이언트 간의 통신을 처리한다.
  transports: ['websocket'], // websocket을 사용하여 통신하겠다고 지정
});

3. socket.io Events

서버와 클라이언트 사이에 이벤트를 보내는 방법이다.

  • 기본 방식
// server-side
io.on("connection", (socket) => {
  socket.emit("hello", "world");
});

// client-side
socket.on("hello", (arg) => {
  console.log(arg); // world
});

이런식으로 on, emit 방법으로 이벤트를 주고 받게 된다.
공통적으로 어느 사이드에서 이벤트를 보낼 때는 emit을 이용하고, 이벤트를 받을 때는 on을 사용한다.

// BAD
socket.emit("hello", JSON.stringify({ name: "John" }));

// GOOD
socket.emit("hello", { name: "John" });

JSON형식으로 바꿔서 보낼 필요가 없다. 그냥 object 형식으로 보내면 된다.

// server-side
io.on("connection", (socket) => {
  socket.on("update item", (arg1, arg2, callback) => {
    console.log(arg1); // 1
    console.log(arg2); // { name: "updated" }
    callback({
      status: "ok"
    });
  });
});

// client-side
socket.emit("update item", "1", { name: "updated" }, (response) => {
  console.log(response.status); // ok
});

이벤트를 보낼 때 인자의 첫 번째는 이벤트 명이, 마지막 인자에는 콜백함수가 들어갈 수 있다.
첫 번째 이벤트 명은 무조건 들어가야하고 콜백함수는 선택이지만 무조건 맨 마지막 인자에 넣어야한다.
이벤트와 콜백 함수 사이에는 주고 받을 데이터들이 들어간다.

자세한 emit 메서드들은 다음 공식문서에 잘 나와있다.

📌Emit cheatsheet

여기서 우리가 사용한 메서드들만 추려서 설명해보겠다.

1. io.on("connection", (socket) => {/* ... */})

  • 이 이벤트는 새 연결 시 발생한다.
  • 첫 번째 인수는 소켓 인스턴스 이다.
  • 서버에서 작성하는 이벤트들을 이 이벤트의 콜백함수로 감싸주면 된다.

2. socket.on(/* ... */), socket.emit(/* ... */)

  • 기본적인 이벤트를 주고 받는 방식이다.

3. socket.join(roomId)

  • 룸은 소켓이 조인 및 탈퇴할 수 있는 임의의 채널이다. 클라이언트의 하위 집합에 이벤트를 브로드캐스트하는 데 사용할 수 있다.
  • join을 사용하여 소켓을 지정된 채널에 가입시킬 수 있다.

4. io.to(roomId).emit('/* ... */)

  • 그 해당 방에 브로드캐스팅하여 이벤트를 보낼 때 사용한다.
  • 다양한 방에 한 번에 이벤트를 보낼 수도 있다.
    io.to("room1").to("room2").to("room3").emit("some event")

5. io.emit('onlineStatus', connectionData)

  • 연결된 모든 클라이언트에게 이벤트를 보낼 때 사용한다.

socket 공식문서를 잘 보면서 적절한 상황에 적절한 API를 사용하는 게 중요하다 생각이 들었다.

초반에 부조건 socket.emit, socket.on만 사용하여 백엔드와 이벤트를 주고받으려고 했는데, socket과 io의 차이점을 모르고 사용해서 메세지가 돌아오지 않는 경우가 빈번하게 발생했다.

또한 ~.to.emit과 ~.emit의 차이점도, room의 개념과 사용법을 제대로 알지 못한 상태에서 개발하여 다른 방에 연결된 사용자가 다른 방에서 보낸 메세지를 받게 되는 상황도 발생했었다.

다음 글은 백엔드 분과 함께 기획한 이벤트에 대해 소개하고 그에 따라 구현한 코드를 설명하겠다.

profile
성공 = 무한도전 + 무한실패

0개의 댓글