Tech/Sofeware Development

Web Socket 으로 간단한 채팅앱 만들기 (Node.js, React)

행복한 시지프 2024. 11. 10. 19:10

Web Socket Protocol 이란?

두 프로그램간 양방향으로 메시지를 교환하기 위한 통신규약을 말한다. IETF의 RFC6455 에서 표준 프로토콜로 정의하고, W3C에 의해서 웹 기술 표준으로 정의하고 있다.

웹 소켓의 특징

1. 양방향 통신

클라이언트와 서버가, 서로 통신을 주고받을 수 있다. HTTP 프로토콜은 단방향 통신이 특징인 것과 차별점을 가진다.

 

2. 지속적인 연결

HTTP 통신은 비연결성을 가진다. 한번 요청, 응답을 주고받으면 종료되는 것이 기본 특징이다. Web Socket은 한번 연결이 되면, close 할 때까지 지속적으로 서로 메시지를 주고받을 수 있다. 그러므로, 불필요한 handshake 과정이 없고, 불필요하게 큰 header 정보를 계속 주고받지 않아도 된다. 그래서 실시간 채팅앱, 주식 차트, IoT 기기 센싱 등 다양한 곳에서 사용된다.

웹 소켓의 동작

  1. HTTP 프로토콜로 handshake 하여, 양쪽 서버 연결 시도
  2. ws 프로토콜로 메시지 양방향으로 주고받기
  3. close 호출 시, socket 연결 종료

실제로 연결하고, 브라우저 네트워크탭의 WS 로 필터링하면 세부 정보를 볼 수 있다. HTTP GET 으로 통신하며, 101 Switching Protocols 상태를 받는다.

Node.js 구현 (with ws)

Node.js 라이브러리인 ws 를 활용하여 구현한다. 구현 자체는 매우 간단하다.

  1. WebSocket 서버를 생성한다.
  2. 클라이언트에서 메시지 이벤트를 받았을 때, 모든 클라이언트로 데이터를 브로드캐스트해준다.
import { WebSocketServer, WebSocket } from "ws";
import { createServer } from "http";

// HTTP 서버 생성 (WebSocket 연결을 위해 HTTP 서버가 필요)
const server = createServer();
const wss = new WebSocketServer({ server });

// 클라이언트 연결 시 실행될 이벤트 리스너 설정
wss.on("connection", (ws: WebSocket) => {
  console.log("New client connected");

  // 클라이언트에서 메시지 수신 시 실행되는 이벤트
  ws.on("message", (message: string) => {
    const parsedMessage = JSON.parse(message);

    // 모든 클라이언트에게 브로드캐스트
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify(parsedMessage));
      }
    });
  });
});

const PORT = 8080;
server.listen(PORT, () => {
  console.log(`Server is running on <http://localhost>:${PORT}`);
});

서버 테스트

Client 를 만들기 어렵다면, terminal 로 웹소켓 서버와 연결할 수 있다.

npm install wscat -g 로 wscat 라이브러리를 설치하고, wscat -c ws://localhost:8080 로 웹소켓 서버와 연결한다.

그러면 prompt 가 가능하고, 메세지를 송수신 해볼 수 있다.

Client 구현 (with React)

React를 활용해서 Web Socket Client 를 구현해보자. 간단한 채팅앱을 구현해볼 것이다.

 

 

브라우저에서 기본적으로 제공하는 WebSocket 객체를 활용할 것이다.

  1. WebSocket Server 와 연결
  2. 서버의 메세지 이벤트를 받아서, 상태 업데이트
const [socket, setSocket] = useState<WebSocket | null>(null);
const [messages, setMessages] = useState<
  { clientId: string; text: string }[]
>([]);

useEffect(() => {
  // WebSocket 서버 연결
  const ws = new WebSocket("ws://localhost:8080");
  setSocket(ws);

  // 서버로부터 메시지 수신
  ws.onmessage = (event) => {
    const messageData = JSON.parse(event.data);
    setMessages((prevMessages) => [...prevMessages, messageData]);
  };

  return () => {
    // 컴포넌트 언마운트 시 WebSocket 연결 종료
    ws.close();
  };
}, []);

 

 

3. 클라이언트에서 메시지를 전송한다.

const sendMessage = () => {
  if (socket && input) {
    const message = { text: input, clientId };
    socket.send(JSON.stringify(message));
    setInput("");
  }
};

상세한 구현은 아래 repository 에 남긴다.

https://github.com/euijinkk/web-socket-full-practice

 

구현을 완료하고, 탭을 여러개 띄워서, 2개 이상의 Client 에 접속하여, 하나의 Web Socket server에 여러개의 Client 와 연결한다. 그리고 각 탭에서 채팅을 보내 실시간으로 정보를 주고받을 수 있는지 확인한다.

웹소켓 사용시 주의할 점과 대안

웹소켓은 클라이언트-서버 간의 지속적인 연결을 유지한다. 그러므로, 잘못 사용하면 불필요하게 메모리, CPU 리소스를 소모할 수 있다.

그러므로 꼭 필요할 때만 사용하고, 그렇지 않다면 다른 통신 방법을 사용하는 것이 바람직하다. 꼭 필요한 상황은, 양방향 + 실시간 통신이 요구될 때이다.

 

대안으로는 Polling, SSE(Server Sent Event) 등이 있다.

 

Polling 은 HTTP 요청을 주기적으로 보내는 방식을 말한다. 특정 시간 안에, 서버에서 데이터가 오는 것을 확신할 수 있고, 완벽한 실시간성을 요구하지 않는다면, Polling 을 사용하는 것으로 충분하다. 긴 시간 속에서 언제 서버가 update 되는지 알 수 없다면, 불필요한 HTTP 요청이 많아질 것이다. 또한 완벽한 실시간성을 요구한다면, Polling interval 을 줄여야 하고, 그러면 서버 부하가 더 커질뿐더러, HTTP 통신 자체가 느려서 어느정도 통신 지연을 가질 수밖에 없다.

 

SSE는 Web Socket과 마찬가지로 한번 HTTP 통신으로 서버와 클라이언트를 연결한 후, 서버가 클라이언트로 단방향으로 지속적으로 메시지를 보낼 수 있다. 양방향 통신은 필요없지만, 실시간 통신을 요구하는 경우 이 기능을 사용할 수 있다.

마치며

간단한 형태의 채팅앱을 만들어보며, Server, Client 에서 어떻게 Web Socket 통신하는지 알아보았다. 구현하면서 프론트엔드, 서버 구조 모두에서 다양한 의문이 들었다. 클라이언트에서 Socket 이 끊어질 때를 대응하고, Socket 통신이 필요없을 때는 적절한 시점에 끊어서 메모리 누수를 방지한다거나 하는 고민을 해볼 수 있다. 서버에서는 로드밸런싱은 어떻게 할지, 실시간성을 지키기 위해서 어떻게 Socket 메시지를 처리해야할지, 어떤 DB를 사용해야 할지 등을 생각해볼 수 있다. 생각할 포인트가 너무 많았고, 웹소켓 통신으로 좋은 경험을 전달하는 것이 기술적으로 엄청나게 어려운 문제임을 깨달았다.