본문 바로가기
  • 살짝 구운 김 유나
Project/zum:go

[React] 실시간 채팅 구현하기 - STOMP

by yunae 2023. 2. 3.

소켓 프록시 수동 설정하기

https://create-react-app.dev/docs/proxying-api-requests-in-development/#configuring-the-proxy-manually

 

Proxying API Requests in Development | Create React App

Note: this feature is available with react-scripts@0.2.3 and higher.

create-react-app.dev

http-proxy-middleware 설치하기

$ npm install http-proxy-middleware --save
$ # or
$ yarn add http-proxy-middleware
// ws 프로토콜을 사용해야하니 설정해주는 것!
const { createProxyMiddleware } = require("http-proxy-middleware");

module.exports = (app) => {
  app.use(
    "ws",
    createProxyMiddleware({
      target: "http://localhost:8080",
      ws: true,  // 웹소켓을 사용하겠다!
    })
  );
};

 

 

STOMP

(SImple Text Orientated Messaging Protocol)

브로커(중개 서버)를 통해서 클라이언트간에 비동기적으로 메시지를 전송하기 위한 프로토콜

Websocket 위에서 동작하며 클라이언트와 서버가 전송할 메시지의 유형, 형식 내용들을 정의한다.

STOMP는 메시지의 헤더를 작성할 수 있어 통신 시에 인증 처리를 구현하는 것이 가능하다!

=> 우리 프로젝트에서 STOMP를 사용하는 가장 큰 이유이기도 함,,!

 

stomp 설치

$ npm install @stomp/stompjs --save

import 

import * as StompJs from "@stomp/stompjs";

 

 

stomp의 흐름
1. 서버와 연결할 클라이언트를  Connect
2. Subscriber(경로 생성) 지정
3. Publisher(메시지 전송) 지정


 

1. 서버와 연결할 클라이언트  Connect

const clientdata = new StompJs.Client({
        brokerURL: "ws://localhost:8080/chat",
        connectHeaders: {
          login: "",
          passcode: "password",
        },
        debug: function (str) {
          console.log(str);
        },
        reconnectDelay: 5000, // 자동 재 연결
        heartbeatIncoming: 4000,
        heartbeatOutgoing: 4000,
      });

- 서버의 endpoint '/chat' 을 brokerURL 로 설정해준다.

- STOMP의 가장 큰 장점?? 인 connectHeaders를 통해서 서버에 데이터를 전송할 수 있다. 현재는 의미 없는 정보가 보내지고 있는 상태. 로컬스토리지에 있는 토큰을 받아 헤더에 실어서 보내면 사용자 인증 구현도 가능하다고 한다.

- reconnectDelay : 5초마다 자동 재연결을 시도한다.

 

 

 

2. Subscribe (구독하기)

// 구독
  clientdata.onConnect = function () {
    clientdata.subscribe("/sub/channels/" + chatroomId, callback);
  };
      
      
 const callback = function (message) {
    if (message.body) {
      let msg = JSON.parse(message.body);
      setChatList((chats) => [...chats, msg]);
    }
  };

- 채팅방 번호가 담긴 주소로 구독요청. 구독과 동시에 실행할 콜백함수를 인자로 넘긴다.

- 우리는 채팅 배열에 새로 받은 메시지를 추가하는 동작을 하게 했다.

 

 

 

3. Publish (전송하기)

const sendChat = () => {
    if (chat === "") {
      return;
    }

    client.publish({
      destination: "/pub/chat/" + chatroomId,
      body: JSON.stringify({
        type: "",
        sender: userId,
        channelId: "1",
        data: chat,
      }),
    });

    setChat("");
  };

- destination과 body를 publish를 사용해 서버단에 보내준다.

 

 

 

이렇게만 쓰니까,, 굉장히 간편해 보이지만 삽질 좀 많이 했다..

내가 기억하려고 올리는,, 전체코드,,

import React, { useCallback, useRef, useState, useEffect } from "react";
import styles from "./styles/ChatRoom.module.css";
import { useSelector } from "react-redux";
import testImg from "../assets/images/testImg.jpg";
import { useNavigate, useParams } from "react-router-dom";

import * as StompJs from "@stomp/stompjs";

// heroicons
import {
  CameraIcon,
  ChevronLeftIcon,
  MegaphoneIcon,
} from "@heroicons/react/24/outline";
import { ArrowUpCircleIcon } from "@heroicons/react/24/solid";

export default function ChatRoom() {
  let navigate = useNavigate();

  const param = useParams(); // 채널을 구분하는 식별자
  const chatroomId = param.chatroomId;
  const token = JSON.stringify(window.localStorage.getItem("token")); // 현재 로그인 된 사용자의 토큰

  let [client, changeClient] = useState(null);
  const [chat, setChat] = useState(""); // 입력된 chat을 받을 변수
  const [chatList, setChatList] = useState([]); // 채팅 기록

  // userSlice.js에 저장된 로그인된 유저의 코드를 받음
  const userId = useSelector((state) => {
    return state.user.userCode;
  });

  //컴포넌트가 변경될 때 객체가 유지되어야하므로 'ref'로 저장

  // 내가 보낸 메시지, 받은 메시지에 각각의 스타일을 지정해 주기 위함
  const msgBox = chatList.map((item, idx) => {
    if (Number(item.sender)!== userId) {
      return (
        <div key={idx} className={styles.otherchat}>
          <div className={styles.otherimg}>
            <img src={testImg} alt="" />
          </div>
          <div className={styles.othermsg}>
            <span>{item.data}</span>
          </div>
          <span className={styles.otherdate}>{item.date}</span>
        </div>
      );
    } else {
      return (
        <div key={idx} className={styles.mychat}>
          <div className={styles.mymsg}>
            <span>{item.data}</span>
          </div>
          <span className={styles.mydate}>{item.date}</span>
        </div>
      );
    }
  });

  const connect = () => {
    // 소켓 연결
    try {
      const clientdata = new StompJs.Client({
        brokerURL: "ws://localhost:8080/chat",
        connectHeaders: {
          login: "",
          passcode: "password",
        },
        debug: function (str) {
          console.log(str);
        },
        reconnectDelay: 5000, // 자동 재 연결
        heartbeatIncoming: 4000,
        heartbeatOutgoing: 4000,
      });

      // 구독
      clientdata.onConnect = function () {
        clientdata.subscribe("/sub/channels/" + chatroomId, callback);
      };

      clientdata.activate(); // 클라이언트 활성화
      changeClient(clientdata); // 클라이언트 갱신
    } catch (err) {
      console.log(err);
    }
  };

  const disConnect = () => {
    // 연결 끊기
    if (client === null) {
      return;
    }
    client.deactivate();
  };

  // 콜백함수 => ChatList 저장하기
  const callback = function (message) {
    if (message.body) {
      let msg = JSON.parse(message.body);
      setChatList((chats) => [...chats, msg]);
    }
  };

  const sendChat = () => {
    if (chat === "") {
      return;
    }

    client.publish({
      destination: "/pub/chat/" + chatroomId,
      body: JSON.stringify({
        type: "",
        sender: userId,
        channelId: "1",
        data: chat,
      }),
    });

    setChat("");
  };

  useEffect(() => {
    // 최초 렌더링 시 , 웹소켓에 연결
    // 우리는 사용자가 방에 입장하자마자 연결 시켜주어야 하기 때문에,,
    connect();

    return () => disConnect();
  }, []);

  const onChangeChat = (e) => {
    setChat(e.target.value);
  };

  const handleSubmit = (event) => {
    event.preventDefault();
  };

  return (
    <>
      {/* {JSON.stringify(user)} */}
      {/* <GlobalStyle/> */}
      <div className={styles.container}>
        {/* 상단 네비게이션 */}
        <div className={styles.topbar}>
          <ChevronLeftIcon
            onClick={() => {
              navigate("/chatlist ");
            }}
          />
          <span>상대방 이름</span>
          <MegaphoneIcon onClick={() => navigate(`/report/1`)} />
        </div>

        {/* 채팅 리스트 */}
        <div className={styles.chatbox}>{msgBox}</div>

        {/* 하단 입력폼 */}
        <form className={styles.sendzone} onSubmit={handleSubmit}>
          {/* <input type="file" accept='image/*'/>  */}
          <CameraIcon className={styles.cameraicon} />
          <div className={styles.inputbar}>
            <div>
              <input
                type="text"
                id="msg"
                value={chat}
                placeholder="메시지 보내기"
                className={styles.input}
                onChange={onChangeChat}
                onKeyDown={(ev) => {
                  if (ev.keyCode === 13) {
                    sendChat();
                  }
                }}
              />
            </div>
            <ArrowUpCircleIcon
              value="전송"
              className={styles.sendbtn}
              onClick={sendChat}
            />
          </div>
        </form>
      </div>
    </>
  );
}

실행 결과 괜히 뿌듯해서 올려보기,,

 

 

 

 

 

챗 기능 구현할 때 김유나,, 최근검색어로 웃겨버림..ㅎ

꽤 뿌듯하자나,, 히히 😊

댓글