[백로그] Message를 실시간으로 바꿔보자 : WebSocket (3)

Jade·2023년 2월 15일
1

프로젝트

목록 보기
18/28
post-thumbnail

실시간 채팅 구현 90% 성공!

어떻게 저렇게 구현은 했는데요?
저는 아직 궁금한 게 많거든요?


구현

useLiveChat이라는 Hook을 만들어서 웹소켓을 제어하기로 했다.

지금은 connect 하는 과정 안에 subscribe가 들어가있는데, 메시지 페이지 안에 들어간 뒤에 방을 바꿀 때마다 connect를 했다 끊었다 하는 게 비효율적일 것이기 때문에 메시지룸을 옮길 땐 subscribe, unsubscribe를 하도록 connect와 subscribe를 분리하는 작업이 남아있다.

//useLiveChat.js  
//몇몇 정보 제외한 코드라 불완전할 수 있음

const useLiveChat = () => {
  const [text, setText] = useState('');
  const [messageRoom, setMessageRoom] = useState([]);
  const CurrentRoomId = useRecoilValue(CurrentRoomIdState);
  const { profileId } = useRecoilValue(Profile);
  const [receiverId, setReceiverId] = useState(0);
  const client = useRef({});

  const URL ='여기에 서버 URL+서버에서 지정한 웹소켓 연결 endpoint';

  useEffect(() => {
    initialChatSetting();
    connect();
    return () => disconnect();
  }, [CurrentRoomId]);

  const initialChatSetting = async () => {
    if (CurrentRoomId !== 0 && CurrentRoomId !== undefined)
      await axios
        .get(`/messages/rooms/${profileId}/${CurrentRoomId}`)
        .then(({ data: { data } }) => {
          if (profileId === data.tuteeId) {
            setReceiverId(data.tutorId);
          } else setReceiverId(data.tuteeId);
          setMessageRoom(data);
        })
        .catch((err) => console.log(err));
  };

  const connect = () => {
    client.current = new StompJs.Client({
      webSocketFactory: () => new SockJS(URL),
      connectHeaders: {
        Authorization: TOKEN,
      },
      debug: (str) => {
        console.log(str);
      },
      reconnectDelay: 3000,
      heartbeatIncoming: 2000,
      heartbeatOutgoing: 2000,
      onConnect: () => {
        subscribe();
      },
      onStompError: (frame) => {
        console.error(frame);
      },
    });

    client.current.activate();
  };

  const disconnect = () => {
    client.current.deactivate();
  };

  const subscribe = () => {
    client.current.subscribe(`/sub/room/${CurrentRoomId}`, (res) => {
      setMessageRoom((prev) => ({
        ...prev,
        messages: [...prev.messages, JSON.parse(res.body)],
      }));
    });
  };

  const publish = (type = '') => {
    if (!client.current.connected) {
      return;
    }

    let sendText = '';
    switch (type) {
      case '':
        sendText = text;
        break;
      case 'matchingConfirm':
        sendText = 'MAT_CHING_CON_FIRM';
        break;
      case 'matchingCancel':
        sendText = 'MAT_CHING_CAN_CEL';
        break;
    }

    client.current.publish({
      destination: `/pub/chats/${CurrentRoomId}`,
      body: JSON.stringify({
        senderId: profileId,
        receiverId: receiverId,
        messageContent: sendText,
        messageRoomId: CurrentRoomId,
      }),
    });
    setText('');
  };

  return {
    disconnect,
    publish,
    messageRoom,
    setMessageRoom,
    text,
    setText,
  };
};

export default useLiveChat;

Sock Js를 다시 살펴보자

실시간 채팅 구현을 시작할 때 Sockjs라는 걸 단순히 웹소켓과 비슷한 기능을 하는 브라우저 js 라이브러리라고만 알았는데, 오늘 백엔드 팀원분께서 보내주신 블로그를 보면서 Sockjs가 하는 일이 정확히는 '웹소켓을 사용하도록 한다 => 하지만 사용할 수 없는 환경에서는 웹소켓 이외의 대안으로 대체'하는 것임을 알게 되었다.

클라이언트 웹소켓간의 통신시에

  • 모든 클라이언트 브라우저에서 웹소켓을 지원한다는 보장이 없음
  • 클라와 서버 중간에 위치한 프록시가 upgrade 헤더를 해석하지 못하고 서버에 전달하지 못함
  • 클라와 서버 중간에 위치한 프록시가 유휴 상태에서 도중에 커넥션을 종료시켜버릴 수도 있음
    이런 예외 상황들이 존재할 수 있다고 한다.

이를 Websocket Emulation이라는 방법을 통해 해결할 수 있는데,
웹소켓을 첫번째로 시도 => 연결 실패 => HTTP-Streaming, HTTP Long Polling 과 같은 HTTP 기반의 다른 기술로 전환해 다시 연결 시도하는 것이다.

그리고 이런 역할을 해주는 게 SockJs!

SockJs를 이용해 웹소켓 연결을 하게 되면 아래와 같이 개발자도구의 네트워크창에서 'info?t=숫자'와 같은 GET 요청을 보내는 것을 확인할 수 있다. 처음에 테스트 코드를 작성할 때 백엔드와 프론트에서 모두 이런 엔드포인트를 설정한 적이 없는데?? 하면서 어리둥절 했던 기억이 있는데 이게 SockJs가 서버로부터 기본 정보를 획득하기 위해 보내는 요청이었다.

이 요청으로 서버가 웹소켓을 지원하는지, 전송 과정에서 Cookies 지원이 필요한지 여부, CORS를 위한 Origin 정보 등을 응답으로 전달 받게 된다고 한다.

This url is called before the client starts the session. It's used to check server capabilities (websocket support, cookies requiremet) and to get the value of "origin" setting (currently not used).

참고


개발자 도구 탐방

팀원들과 오늘치 작업 시간을 가지고 나서 혼자 개발자도구를 탐방하던 도중 발견한 부분인데, 개발자 도구 => 네트워크 => 웹소켓이 연결된 곳(이미지에서는 websocket)을 클릭하고 메시지 부분을 클릭하면 이렇게 웹소켓이 연결된 것부터 구독, 메시지가 보내진 것까지 확인이 가능하다. (근데 연결이 끊어지고 난 뒤에야 기록이 남는듯 하다?)

stompjs의 Client에 디버그 설정을 해두면 비슷하게 콘솔에서 진행사항을 확인 가능하긴 하다. (아래 이미지 참고)

아무튼 개발자 도구에서 메시지 부분을 살펴보는데

빨간색으로 표시한 부분에서 h라는 메시지를 받고 있는 것을 확인할 수 있었다.

이게 뭐임? 🤔

아까 보던 블로그를 쭉 살펴보다 보니 웹소켓의 연결 유지 여부를 확인하는 Heartbeat Frame이라고 한다.

SockJs에서 Message Frame 크기를 최소화하기 위해서
open frame의 경우에는 'o'
Message Frame은 a["message1","message2"]
Heartbeat Frame은 'h'
커넥션 종료시에는 Close Frame c["message"]
이라고 전송한다고 한다.


CORS : ㅎㅇ? 또 옴 ㅋ

오늘 프로젝트 작업 시간에 제일 애를 먹였던 에러 중 하나가 Access-Control-Allow-Credentials관련된 에러였다. 구글밋에서 채팅으로 냄겨뒀더니 에러 전체 코드가 남아있지는 않은데.. 아무튼 해당 설정이 필요한데 왜 안 해!!! 와 같은 느낌의 에러였다.

//App.jsx 파일에 axios default 설정 추가 

axios.defaults.withCredentials = true;

서버쪽에서 관련된 설정을 해주고, App.jsx 파일에 위의 default 설정을 추가해서 어찌어찌 해결은 했는데 정확한 이유에 대해서 알고 싶어서 백엔드 팀원께 여쭤보다 (또) 이 블로그를 참고하게 되는데... 이 블로그의 CORS 관련 설명에 따르자면 SockJs에서는 CORS 헤더에 Access-Control-Allow-Credentials 값이 항상 true로 설정이 된다고 한다.

우리 팀원이 chatGPT에게 질문하고 답변 받은 내용을 추가해보자면...

Sock.js에서 withCredentials를 항상 true로 설정하는 이유는, Sock.js가 사용되는 대부분의 상황에서는 인증 정보를 사용해야 하기 때문입니다. 예를 들어, 사용자가 로그인을 한 상태에서 채팅 애플리케이션을 사용하고 있다면, Sock.js를 통해 채팅 서버와 통신할 때에도 인증 정보를 함께 전송해야 합니다.
따라서, Sock.js에서 withCredentials를 항상 true로 설정하면, 인증 정보가 포함된 요청을 보낼 수 있으며, 보안적인 이슈를 예방할 수 있습니다. 단, 이 경우에는 서버도 클라이언트의 요청에 대해 인증 정보를 확인하는 로직을 구현해야 합니다.

똑똑허네...chatGPT...


a와 A는 다르다 😈

이건 그냥... 하기 쉬운 실수인 거 같아서 덧붙여 놓는다.
connect 할 때 connectHeaders 값으로 토큰을 넣어주었었는데,
{Authorization: TOKEN}이라고 해줘야 할 것을 {authorization: TOKEN}으로 설정해주는 바람에 연결 시도가 제대로 되지 않았다 ^^... 잘 확인해서 작성하자...

profile
키보드로 그려내는 일

0개의 댓글