Websocket을 도전해보자

robin Han·2025년 7월 13일
1

웹소켓

WebSocket은 브라우저와 서버 간의 양방향 실시간 통신을 가능하게 하는 프로토콜(protocol)이다.

기존에 호출했던 API들은 HTTP 기반으로 요청할때마다 연결하여 단방항으로 요청하면 응답해주는 방식을 사용하고 있었다 HTTP의 문제는 단방향 즉 요청할때 까지 응답을 해주지 못한다.

하지만 Websocket은 한번 연결하면 하나의 다리를 만들어줘서 양방향 즉 서버도 실시간으로 응답을 할수있게 해준다.
클라이언트 <-> 서버 <-> 클라이언트

여기서 의문점은 그럼 서버에서 통신할때 Websocket으로만 가능한가?
NOPE!
SSE : Server-Sent Events 라고 서버에서 클라이어트로 데이터를 지속적으로 보낼수있다. 하지만 이또한 단방향 즉 서버 -> 클라이언트

이제 코드를 보면서 확인해보자

서버측 코드

// 서버측 websocket
import WebSocket, { WebSocketServer } from "ws";
import { userSockets } from "./socket";

export const setupWebSocket = (server: any) => {
  const wss = new WebSocketServer({ server });

  wss.on("connection", (ws) => {
    let userId: string | null = null;

    ws.on("messageㄴ", (msg: string) => {
      try {
        const data = JSON.parse(msg);

        if (data.type === "init" && data.userId) {
          userId = data.userId;
          userSockets.set(userId as string, ws);
          console.log(`🔗 유저 ${userId} 연결됨`);
          return;
        }

        if (data.type === "chat") {
          const { fromUserId, toUserId, message } = data;
          const receiver = userSockets.get(toUserId);
          console.log("🧪 메시지 받을 유저:", toUserId);
          console.log("🧪 현재 등록된 소켓들:", [...userSockets.keys()]);
          const payload = {
            type: "chat",
            fromUserId,
            message,
            timestamp: new Date().toISOString(),
          };

          if (receiver && receiver.readyState === WebSocket.OPEN) {
            console.log(`📤 ${fromUserId}${toUserId}: ${message}`);
            receiver.send(JSON.stringify(payload));
          } else {
            console.log(`⚠️ ${toUserId} 유저는 오프라인입니다.`);
          }
        }
      } catch (err) {
        console.error("메시지 처리 중 에러:", err);
      }
    });

    ws.on("close", () => {
      if (userId) {
        userSockets.delete(userId);
        console.log(`❌ 유저 ${userId} 연결 해제`);
      }
    });
  });
};

///userSocket 
const userSockets = new Map<string, WebSocket>();

우선 핸드 쉐이크를 통해서 연결을 해줘야 하는데 이건 HTTP 요청이다
클라이언트: 다리 연결해주셈 -> 서버 : ㅇㅋ userId 등록.하고 다리 연결 해줌
1. new WebSocketServer({ server }) server 라는 HTTP 를 받아서 Websocket 서버를 HTTP 서버에통합
2. ws.on('connection',.... ) 새 클라이언트가 websocket에 연결되었을때 실행
3. ws.message() 메세지 수신 처리
안에 들어가는 type에 따른 다른 동작을 수행할수 있다

  1. type === 'init' 유저 등록 -> userSocket인 map 함수에 유저 추가
  2. type === 'message' 메세지 전송 userSokcets.get()으로 유저 찾고 연결되어있는 상태라면 메세지 전송
  3. ws.on('close',...) 브라우저가 탭을 닫거나 끊기면 실행 유저를 제거 한다

    여기서 주의할점 express 앱과 Websokcet을 사용할때 WebSocket 서버를 그 HTTP 서버에 연결하고 하나의 서버에서 HTTP 와 Webscoket처리가 가능하다

// 1. 기존의 Express 서버 생성
const server = http.createServer(app);
// 2. WebSocket 서버를 Express 기반 HTTP 서버에 붙임
setupWebSocket(server);
server.listen(PORT, () => {
  console.log(`🚀 서버 실행됨: http://localhost:${PORT}`);
});

클라이언트 코드


const socketRef = useRef<WebSocket | null>(null) 
 const socket = new WebSocket("ws://localhost:3005");
  socketRef.current = socket;

  socket.onopen = () => {
    socket.<send(JSON.stringify({ type: "init", userId }));
  };

websocket객체를 컴포넌트 생명주기 동안 유지 주어진 userId websocket 연결 생성 type=== 'init' 을 줘서 연결해줘 요청 보내기
new Websocket은 브라우저ㅇ 내장되어있는 객체를 사용한다
new Websocket에 내장되있는 이벤트 핸들러

  • onopen()은 연결이 성공적일때 실행되는 콜백
  • onmessage()은 서버로부터 메시지를 수신했을 때 실행되는 콜백
  • onclose()은 연결이 종료되었을 때
  socket.onmessage = (event) => {
    try {
      const data = JSON.parse(event.data);
      if (data.type === "notification" && onNotify) {
        onNotify(data.message);
      }
      if (data.type === "chat" && onChat) {
        onChat({ fromUserId: data.fromUserId, message: data.message });
      }
    } catch (err) {
      console.error("메시지 파싱 실패:", err);
    }
  };

type에 따라 알림인지 메세지 인지 확인 하고 모든 메세지 파싱해서 유저 아이디들과 같이 전달

return {
  socketRef,
  sendMessage: (toUserId: string, message: string) => {
    if (
      socketRef.current &&
      socketRef.current.readyState === WebSocket.OPEN
    ) {
      socketRef.current.send(
        JSON.stringify({
          type: "chat",
          fromUserId: userId,
          toUserId,
          message,
        })
      );
    }
  },
};

반환값들 설명
socketRef : useRef로 저장된 인스턴스 반환 , 외부에서 객체 접근할때 사용
ssendMessage : 메시지 전공 함수-> websokcet이 열려있는지 확인하고 json으로 직렬화 후 서버로 메시지 전송

채팅 컴포넌트

 const [messages, setMessages] = useState<Message[]>([]);
 const [input, setInput] = useState("");
 const { sendMessage } = useNotification(userId, {
    onChat: ({ fromUserId, message }) => {
      console.log("📨 받은 메시지:", message);
      setMessages((prev) => [...prev, { fromUserId, message }]);
    },
  });

  const handleSubmit = () => {
    if (!input.trim()) return;
    sendMessage(opponentId, input);
    setMessages((prev) => [...prev, { fromUserId: userId, message: input }]);
    setInput("");
  };

useWebsocket.ts 전체 코드

import { useEffect, useRef } from "react";

interface ChatData {
  fromUserId: string;
  message: string;
}

interface UseNotificationOptions {
  onNotify?: (msg: string) => void;
  onChat?: (data: ChatData) => void;
}

export const useNotification = (
  userId: string,
  { onNotify, onChat }: UseNotificationOptions
) => {
  const socketRef = useRef<WebSocket | null>(null);

  useEffect(() => {
    if (!userId) return;

    const socket = new WebSocket("ws://localhost:3005");
    socketRef.current = socket;

    socket.onopen = () => {
      socket.send(JSON.stringify({ type: "init", userId }));
    };

    socket.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        if (data.type === "notification" && onNotify) {
          onNotify(data.message);
        }
        if (data.type === "chat" && onChat) {
          onChat({ fromUserId: data.fromUserId, message: data.message });
        }
      } catch (err) {
        console.error("메시지 파싱 실패:", err);
      }
    };

    return () => {
      socket.close();
    };
  }, [userId, onNotify, onChat]);

  return {
    socketRef,
    sendMessage: (toUserId: string, message: string) => {
      if (
        socketRef.current &&
        socketRef.current.readyState === WebSocket.OPEN
      ) {
        socketRef.current.send(
          JSON.stringify({
            type: "chat",
            fromUserId: userId,
            toUserId,
            message,
          })
        );
      }
    },
  };
};

1개의 댓글

comment-user-thumbnail
2025년 7월 13일

우왕 굿 채팅 구현 멋져요!

답글 달기