sns 프로젝트

메세지 채팅 기능 (웹소켓, redis, 실시간 알람 기능)

우주속공간 2024. 1. 24. 18:00

미리보기


📁 채팅 기능 미리보기

 

message페이지
현재 로그인하고 있는 유저에게 새로운 메세지가 왔을때 알림창 뜨기

 

 

실시간 소통을 위해서 웹소켓 통신 방법을 사용하였고, 채팅 데이터를 저장하기 위해서 redis를 사용했다.

redis 를 사용하게 된 이유는 기존에 사용하던 mysql 데이터베이스는 상대적으로 실시간 데이터를 가져오기에는 느릴것이라고 판단했고

 

redis는 

  • 빠른 읽기/쓰기 성능 제공, 실시간으로 데이터 접근하고 업데이트, 빠른 응답 시간 유지
  • Pub/Sub 기능 제공 (특정 집단에 메세지 전달 유용할것이라고 판단)

같은 장점을 가지고 있기때문에 사용하게 되었다.

 

 

이번 게시물에서는 채팅 기능 중 프론트 쪽 코드에 대해서 적어보고자 한다. 

 

 

 

기능 설명


프론트

 

Auth 페이지

  • 로그인하면 login통신으로 로그인한 유저 email과 socketId를 보내줌.

 

Message 페이지

  • useEffect 내부 소켓 이벤트 핸들러
    • "RECEIVE_MESSAGE" : 새로운 메세지 도착시 알람
    • "REQUEST_DATA" : 처음 페이지를 로딩할때 현재 유저 id를 백엔드로 보내서 유저 채팅 데이터 받기
    • "RESPOND_DATA" : 유저가 속한 모든 채팅 데이터,  서버가 보낸 데이터 받아서 정리
    • "BEFORE_DATA" : 유저가 선택한 특정 채팅방의 데이터 받기
  • peopleClick : 유저 검색을 통해서 새로운 채팅방을 시작할려고 했을때 사용 
  • handleSendMessage : 사용자가 채팅 메시지를 보낼 때 사용
  • handleChatRoomClick : 사용자가 특정 채팅방을 선택할 때 사용

 

 

실제 구현한 코드


Auth 페이지 

 로그인하면 login통신으로 로그인한 유저 email과 socketId를 보내줌

const onLogin = (data: any) => {
    customAxios
      .post("/login", {
        email: data.email,
        password: data.password,
      })
      .then(async (response) => {
        alert("로그인 성공");
        dispatch(changeState(true));
        
        // 로그인하면 login통신으로 로그인한 유저 email과 socketId를 보내줌.
        socket.emit("login", { email: data.email, socketID: socket.id });
      })

      .catch((error) => {
        // ... 에러 처리
      });
  };

 

 

 

message 페이지

 

"RECEIVE_MESSAGE" 이벤트 핸들러

  • 목적: 새로운 메시지가 도착했을 때 사용자에게 알림을 제공
  • 기능:
    • 새 메시지가 도착하면 경고창(alert)으로 사용자에게 알립니다.

"REQUEST_DATA" 이벤트 발송

  • 목적: 서버에 현재 사용자의 채팅방 데이터를 요청
  • 기능:
    • 현재 사용자의 ID를 서버에 전송하여, 해당 사용자의 채팅방 데이터를 요청.

"RESPOND_DATA" 이벤트 핸들러

  • 목적: 서버로부터 받은 채팅방 데이터를 처리.
  • 기능:
    • 받은 데이터를 각 채팅방별로 분류하고, 메시지를 날짜 순으로 정렬.
    • 정렬된 데이터를 상태에 저장하여, 채팅방 목록을 업데이트.

"BEFORE_DATA" 이벤트 핸들러

  • 목적: 특정 채팅방의 이전 메시지 데이터를 처리.
  • 기능:
    • 받은 채팅방 ID에 해당하는 이전 메시지 데이터를 상태에 저장합니다.
    • 선택된 채팅방 ID를 저장하여 후에 해당 채팅방에서 메세지를 보낼 때 사용.
 useEffect(() => {
    socket.on("RECEIVE_MESSAGE", (data: ChatMessage[]) => {
      alert("새로운 메세지 도착");

    });
    socket.emit("REQUEST_DATA", {
      id: id,
    });

    socket.on("RESPOND_DATA", (data: ChatMessage[]) => {
      let sortedChatRooms: ChatRooms = {};

      data.forEach((message) => {
        if (!sortedChatRooms[message.roomId]) {
          sortedChatRooms[message.roomId] = [];
        }
        sortedChatRooms[message.roomId].push(message);
      });

      // 각 채팅방에서 메시지를 날짜 순으로 정렬
      Object.keys(sortedChatRooms).forEach((roomId) => {
        sortedChatRooms[roomId].sort((a, b) => {
          let dateA = new Date(a.date);
          let dateB = new Date(b.date);
          return dateA.getTime() - dateB.getTime();
        });
      });

      setChatRoomData(sortedChatRooms);
    });

    socket.on("BEFORE_DATA", (data: string) => {
      console.log(data);
      setSelectedMessages(chatRoomData[data]);
      setSelectedRoomId(data);
    });

    return () => {
      socket.off("RECEIVE_MESSAGE", (data: any) => {});
    };
  }, [socket]);

 

 

 peopleClick 함수

  • 목적: 사용자가 다른 유저를 검색하여 새로운 채팅방을 시작하려고 할 때 사용.
  • 기능: 유저가 검색을 통해서 원하는 유저를 선택하면
    • 새로운 채팅방 ID를 생성하여 selectRoomId에 저장
    • 선택된 사용자의 ID를 상태에 저장하고, 소켓을 통해 서버에 새 채팅방 시작을 알림(START_CHAT).
  const peopleClick = (user_id: number, data: any) => {
    setModalIsOpen(false);

    const roomId = `chat-${[id, user_id].sort().join("-")}`; // 채팅방 ID 생성
    setSelectedRoomId(roomId); // 선택된 채팅방 ID 업데이트
    setSelectUser(user_id);
    socket.emit("START_CHAT", {
      users: [id, user_id],
      roomId: roomId,
      date: new Date().toISOString(),
    });
  };

 

 

handleSendMessage 함수

  • 목적: 사용자가 채팅 메시지를 보낼 때 사용
  • 기능:
    • 메시지가 유효한 경우에만 처리
    • 새 메시지 객체를 생성하고, 소켓을 통해 이를 서버에 전송합니다.
    • 전송된 메시지를 로컬 채팅방 데이터에 추가합니다.
  const nowDate = new Date();
  const time = new Date(nowDate.getTime())
    .toISOString()
    .replace("T", " ")
    .slice(0, -5);

  const score = parseInt(
    time.replace(" ", "").replace(/-/g, "").replace(/:/g, ""),
    10
  );

 
 
 const handleSendMessage = (receiveUser: number) => {
    if (!selectedRoomId || message.trim() === "") return;
    
    const newMessage = {
      send: id,
      receive: receiveUser,
      message: message,
      date: time,
      score: score,
      roomId: selectedRoomId,
    };

    socket.emit("SEND_MESSAGE", newMessage);
    setMessage("");
    // 선택된 채팅방의 메시지 목록 업데이트
    const updatedMessages: ChatMessage[] = [...selectedMessages, newMessage];
    setSelectedMessages(updatedMessages);
    console.log(newMessage);

    // 전역 채팅방 데이터 업데이트
    const updatedChatRoomData: { [key: string]: ChatMessage[] } = {
      ...chatRoomData,
      [selectedRoomId]: updatedMessages,
    };
    setChatRoomData(updatedChatRoomData);
  };

 

 

handleChatRoomClick 함수

  • 목적: 사용자가 특정 채팅방을 선택할 때 사용
  • 기능:
    • roomId를 받아와서 선택된 채팅방의 메시지를 표시.
    • 채팅방 ID를 분석하여, 현재 사용자가 아닌 다른 사용자의 ID를 추출하고 상태에 저장.
 const handleChatRoomClick = (roomId: string) => {
    setSelectedMessages(chatRoomData[roomId]);
    setSelectedRoomId(roomId);
    const parts = roomId.split("-");

    // 'chat' 문자열과 현재 사용자 ID를 제외하고 나머지 부분을 추출
    const otherUserId = parts.find(
      (part) => part !== "chat" && part !== id.toString()
    );
    if (otherUserId == undefined) {
      setSelectUser(id);
    } else {
      console.log(otherUserId);
      setSelectUser(parseInt(otherUserId));
    }
  };

 

 

 

 

모든 데이터를 가져온 뒤에 각 채팅방마다 가장 마지막으로 보낸 메세지를 가지고 와서 보여주기

                  {Object.keys(chatRoomData).map((roomKey) => {
                    const lastMessage =
                      chatRoomData[roomKey][chatRoomData[roomKey].length - 1];

                    return (
                      <div
                        className="peopleBox hover:bg-slate-200"
                        key={roomKey}
                        onClick={() => handleChatRoomClick(roomKey)}
                      >
                        <div>
                          <div className="font-bold pl-3">{roomKey}</div>
                          <div className="pl-3">{lastMessage.message}</div>
                        </div>
                      </div>
                    );
                  })}

 

오른쪽 부분 채팅 화면 : 선택된 채팅방의 데이터를 가져와서 현재 로그인한 유저와 메세지를 보낸 사람의 id가 같으면 파란색으로 표시. 아니면 회색으로 표시 

  <div className=" messageBox">
                  {selectedMessages.map((message, i) => (
                    <div
                      className={
                        id == message.send
                          ? "flex justify-end"
                          : "flex justify-start"
                      }
                      key={i}
                    >
                      <div
                        className={id == message.send ? "myChatBox" : "chatBox"}
                        key={i}
                      >
                        {message.message}
                      </div>
                    </div>
                  ))}
                </div>
                <div className=" flex p-2">
                  <input
                    className="input"
                    type="text"
                    placeholder="전송하려는 메세지를 입력하세요."
                    value={message}
                    onChange={(e: any) => setMessage(e.target.value)}
                  />
                  <button
                    className="sendButton w-10"
                    onClick={() => {
                      handleSendMessage(selectUser);
                    }}
                  >
                    전송
                  </button>
                </div>
              </div>