개발/웹

[캡스톤디자인] Socket.IO와 React로 실시간 채팅 구현

xeaxonx 2024. 11. 26. 04:32

우리 졸업 프로젝트는 간단히 말해 p2p 환전 매칭 플랫폼이다. 외화를 구매하는 사람들에게 판매자를 추천해주고, 구매자가 자신이 마음에 드는 판매자에게 채팅을 보내는 구조를 포함한다. 

 

채팅 기능은 웹소켓을 사용하여 구현되며, 이를 통해 실시간 메시지 송수신이 가능하다. 이번 글에서는 프론트엔드에서 채팅 메시지를 주고받는 코드를 구현한 과정을 설명해보려 한다.

 

 

우선, 웹소켓이란 무엇인가?

웹소켓은 서버와 클라이언트 간의 양방향 통신을 가능하게 하는 프로토콜이다. HTTP 통신은 클라이언트의 요청에 서버가 응답하는 방식이라, 실시간으로 데이터를 주고받기 어렵다. 반면, 웹소켓은 서버와 클라이언트 사이의 지속적인 연결을 유지해 주기 때문에, 실시간 데이터 전송에 적합하다.

 

나는 실시간 채팅 기능을 구현하기 위해 Socket.io를 사용했다. Socket.io는 웹소켓을 쉽게 다룰 수 있도록 도와주는 라이브러리이다. 이제 내가 구현한 과정을 설명해보려 한다.

 

 

1. Socket.io 설치 및 초기화

 

우선 프로젝트에 Socket.io를 설치해야 한다.

npm install socket.io-client​

 


 

이후, io 객체를 통해 소켓 연결을 초기화하고, 지정한 url의 서버와 연결한다. 나의 경우 localhost:3000을 사용하였다.

import io from "socket.io-client";

const socket = io("http://localhost:3000");

 

 

 

2. 메시지 수신

 

이제 소켓 이벤트로 메시지를 수신할 것이다. 

useEffect(() => {
  socket.on("receive_message", (message) => {
    setMessages((prevMessages) => [...prevMessages, message]);
  });

  return () => {
    socket.off("receive_message"); // 이벤트 해제
  };
}, []);

 

 

 

(1) socket.on("receive_message")

 

이 코드는 소켓 이벤트 리스너를 등록한다. "receive_message"라는 이벤트가 발생하면, 서버에서 전달된 message 데이터를 받아온다. 

 

(2) 상태 업데이트

 

이제 setMessages를 호출하여 기존 메시지 목록(prevMessages)에 새 메시지를 추가한다.

이는 화면에 실시간으로 메시지가 반영되도록 합니다.

 

(3) 정리

 

return 부분에서 socket.off("receive_message")를 호출하여 컴포넌트가 언마운트될 때 이벤트 리스너를 제거한다. 이렇게 하면 메모리 누수를 방지할 수 있다. 

 

 

 

3. 메시지 전송

 

이제는 메시지를 전송할 차례이다.

const sendMessage = () => {
  if (inputMessage.trim()) {
    const message = {
      sender: "me", // 보낸 사람
      text: inputMessage, // 입력된 메시지
      timestamp: new Date(), // 현재 시간
    };

    socket.emit("send_message", message); // 서버로 메시지 전송
    setMessages((prevMessages) => [...prevMessages, message]); // 로컬 상태 업데이트
    setInputMessage(""); // 입력 필드 초기화
  }
};

 

 

(1) 전송 조건 확인

 

if (inputMessage.trim())는 사용자가 공백 문자만 입력하지 않았는지 확인한다. 공백 문자만 있을 경우 메시지를 보낼 수 없다.

 

(2) 메시지 객체 생성

 

sender: 메시지의 발신자를 표시한다. 내 코드에서는 우선 "me"로 설정되어 있다.

text: 사용자가 입력한 메시지 내용이다.

timestamp: 메시지가 생성된 시간을 저장합니다.

 

(3) socket.emit 호출

 

socket.emit("send_message", message)는 "send_message"라는 이벤트를 서버에 전달한다. 그러면 메시지 객체(message)가 서버로 전송된다.

 

(4) 로컬 상태 업데이트

 

전송한 메시지를 setMessages를 통해 UI에 즉시 반영한다. 이렇게 하면 서버 응답을 기다리지 않고도 사용자 인터페이스가 즉각 업데이트될 수 있다.

 

(5) 입력 초기화

 

메시지를 전송한 후 setInputMessage("")를 호출하여 입력 필드를 비우면, 다음 메시지를 전송할 준비가 되는 것이다.

 

 

 

요약하면?

1. 서버로 메시지 전송: 클라이언트에서 socket.emit을 호출하여 서버에 메시지를 전송한다.

2. 서버에서 메시지 수신 후 브로드캐스트: 서버는 "send_message" 이벤트를 받아 메시지를 처리한 후, 다른 클라이언트에게 "receive_message" 이벤트로 메시지를 전달한다.

3. 클라이언트에서 메시지 수신: socket.on("receive_message")를 통해 다른 클라이언트가 전송한 메시지를 받아 UI에 반영한다. 

 

 

 

아래는 내가 구현한 코드의 프론트 화면이다.

 

 

이렇게 '안녕하세요'를 입력하면,

 

 

상대방에게 '안녕하세요'가 전송된다.

 

이런 식으로 채팅을 이어나갈 수 있다.

 

 

Styled-components를 포함한 전체 코드는 아래와 같다.

import React, { useState, useEffect } from "react";
import styled from "styled-components";
import io from "socket.io-client";

const socket = io("http://localhost:3000"); // 백엔드 서버 주소

function Chat() {
  const [messages, setMessages] = useState([]); // 메시지 목록
  const [inputMessage, setInputMessage] = useState(""); // 입력 메시지
  const [userName] = useState("상대방"); // 상대방 이름 (임시)

  // 메시지 수신
  useEffect(() => {
    socket.on("receive_message", (message) => {
      setMessages((prevMessages) => [...prevMessages, message]);
    });

    return () => {
      socket.off("receive_message"); // 소켓 이벤트 해제
    };
  }, []);

  // 메시지 전송
  const sendMessage = () => {
    if (inputMessage.trim()) {
      const message = {
        sender: "me", // 보낸 사람
        text: inputMessage,
        timestamp: new Date(),
      };

      socket.emit("send_message", message); // 서버로 메시지 전송
      setMessages((prevMessages) => [...prevMessages, message]); // 메시지 UI에 추가
      setInputMessage(""); // 입력 필드 초기화
    }
  };

  return (
    <Container>
      <Header>
        <UserName>{userName}</UserName>
      </Header>

      <MessagesContainer>
        {messages.map((message, index) => (
          <MessageBubble
            key={index}
            isMine={message.sender === "me"}
          >
            {message.text}
          </MessageBubble>
        ))}
      </MessagesContainer>

      <InputContainer>
        <Input
          type="text"
          placeholder="메시지를 입력하세요"
          value={inputMessage}
          onChange={(e) => setInputMessage(e.target.value)}
          onKeyPress={(e) => e.key === "Enter" && sendMessage()}
        />
        <SendButton onClick={sendMessage}>전송</SendButton>
      </InputContainer>
    </Container>
  );
}

export default Chat;

// Styled Components
const Container = styled.div`
  display: flex;
  flex-direction: column;
  height: 100vh;
  width: 100%; 
  margin: 0;
  background-color: #ffffff;
`;

const Header = styled.div`
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 16px;
  background-color: #f5f5f5;
  border-bottom: 1px solid #ddd;
  width: 100%; 
  box-sizing: border-box; 
`;

const UserName = styled.div`
  font-size: 18px;
  font-weight: bold;
`;

const MessagesContainer = styled.div`
  flex: 1;
  padding: 10px;
  display: flex;
  flex-direction: column; 
  gap: 10px; 
  background-color: #ffffff;
`;

const MessageBubble = styled.div`
  display: inline-block;
  padding: 10px;
  border-radius: 20px;
  font-size: 14px;
  color: ${(props) => (props.isMine ? "#ffffff" : "#333333")};
  background-color: ${(props) => (props.isMine ? "#ff5a5f" : "#f1f1f1")};
  align-self: ${(props) => (props.isMine ? "flex-end" : "flex-start")}; 
  max-width: 100%; 
  word-wrap: normal; 
  white-space: nowrap; 
  word-break: normal; 
  margin-right: ${(props) => (props.isMine ? "0" : "auto")};
  margin-left: ${(props) => (props.isMine ? "auto" : "0")};
`;




const InputContainer = styled.div`
  display: flex;
  align-items: center;
  padding: 10px 16px;
  border-top: 1px solid #ddd;
  background-color: #f5f5f5;
  width: 100%; 
`;

const Input = styled.input`
  flex: 1;
  padding: 10px;
  font-size: 14px;
  border: 1px solid #ccc;
  border-radius: 20px;
  margin-right: 10px;
  box-sizing: border-box;
`;

const SendButton = styled.button`
  background: #ff5a5f;
  color: white;
  border: none;
  border-radius: 20px;
  padding: 10px 15px;
  font-size: 14px;
  cursor: pointer;

  &:hover {
    background: #e14e4e;
  }
`;
 

 

현재는 단일 사용자 환경에서 개발을 진행 중이어서, 상대방으로부터 메시지를 수신하는 과정을 테스트하지는 못했다. 이후 로그인 기능을 구현한 뒤, 여러 사용자가 채팅을 주고받는 상황에서 메시지 수신과 동작을 본격적으로 확인해볼 생각이다.