사용 기술

프론트엔드는 Next.js / 백엔드는 스프링을 이용하여 채팅 기능을 구현해볼 것이다.

채팅 기능에만 집중해서 만들것이며 기본적인 CORS같은건 굳이 설명하지 않는다.

이 글에는 프론트엔드 사이드 내용만 적고 백엔드 쪽은 이 글로 분리하려고 한다.

sockjs-client는 npm에서 관리가 안된지 오래 돼서 타입스크립트 사용 시 타입도 따로 추가해야한다.

따라서 types/sockjs-client 라이브러리를 추가로 넣어주도록 한다.

  • Next.js 14 / App router
  • TypeScript
  • @types/sockjs-client, sockjs-client, stompjs

채팅 기능 구현

원래는 STOMP Client를 brokerUrl을 설정해서 만들어보려다가 포기하고 SockJS를 구현체로 사용했다.

이건 백엔드 도메인을 http로 받는듯.brokerUrl은 ws://localhost:8080/ws 의 형식이다.대략적인 순서는 아래와 같다.

( + 수정 - 백엔드 쪽에서 withSockJs 설정을 제거하고 프론트 쪽에서 Stomp Client 로 만들면 된다.)

  1. useEffect로 마운트 시 STOMP Client를 만들고 소켓 연결
  2. Client 생성 시 설정해둔 onConnect 콜백이 동작하여 설정해둔 topic 구독
  3. 구독 시 설정해둔 onMessageReceived 콜백이 동작하여 구독중인 topic에서 메시지를 수신
  4. 메시지 종류에 따라 switch 문으로 분기하여 원하는 동작 실행
    본인은 메시지 출력을 위해 messages 배열에 push 하도록 함
  5. 메시지를 전송하고 싶으면 client.publish 함수를 이용하여 백엔드 엔드포인트로 메시지 전송
    백엔드 코드에서 MessageMapping("url")에 해당.
    prefix(본인은 "/app"으로 설정) + url 형식으로 엔드포인트를 설정해주면 됨

 

참고로 한 페이지에 여러 채팅방을 연결해야할 경우 소켓 연결은 부모 컴포넌트에서 한 번만 해도 된다.

자식 컴포넌트에 STOMP Client 를 전달하여 자식 컴포넌트에서 topic을 구독하는 식으로 구현한다.

"use client";

import { Client } from "@stomp/stompjs";
import { useEffect, useState } from "react";
import SockJS from "sockjs-client";

const BASE_URL = "http://localhost:8080";

type Message = { content: string; sender: string; type: string };

type SocketUnitProps = {
  id: number;
};

export default function OldSocketUnit({ id }: SocketUnitProps) {
  const [client, setClient] = useState<Client | undefined>(undefined);
  const [connected, setConnected] = useState(false);
  const [messages, setMessages] = useState<string[]>([]);

  const onMessageReceived = (res: any) => {
    const data = JSON.parse(res.body);

    const { type, content, sender } = data;
    switch (type) {
      case "JOIN":
        messages.push(`${sender}님 입장`);
        break;
      case "CHAT":
        messages.push(content);
        break;
      case "LEAVE":
        messages.push(`${sender} 퇴장`);
        break;
    }
    setMessages([...messages]);
  };

  const sendMessage = () => {
    const chatMEssage = {
      sender: "mi",
      content: "내용",
      type: "CHAT",
    };
    if (client) {
      console.log(client);
      client.publish({
        destination: `/app/chat.sendMessage/${id}`, // 백엔드의 웹소켓 엔드포인트
        headers: {},
        body: JSON.stringify(chatMEssage),
      });
    }
  };

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

  useEffect(() => {
    const client = new Client({
      webSocketFactory: () => {
        return new SockJS("http://localhost:8080/ws");
      },
      onConnect: () => {
        setConnected(true);
        if (client) {
          client.subscribe(`/topic/${id}`, onMessageReceived); // url의 id = chatroomId
          client.publish({
            destination: `/app/chat.addUser/${id}`, // 백엔드의 웹소켓 엔드포인트
            headers: {},
            body: JSON.stringify({ sender: "name", type: "JOIN" }),
          });
        }
      },
      onStompError: () => {
        console.log("에러 발생");
      },
      debug: function (str) {
        console.log(str);
      },
      reconnectDelay: 5000,
      heartbeatIncoming: 4000,
      heartbeatOutgoing: 4000,
    });
    setClient(client);

    try {
      console.log("[웹소켓] 연결 시도");
      client.activate();
    } catch (e) {
      console.log(e);
    }
  }, []);

  return (
    <div>
      <div>connection: {connected ? "true" : "false"}</div>
      {messages.map((message, i) => (
        <div key={i}>{message}</div>
      ))}
      <div style={{ display: "flex", flexDirection: "column" }}>
        <button
          style={{ border: "1px solid green" }}
          onClick={sendMessage}
          type="button"
        >
          메시지 보내기
        </button>
        <button style={{ border: "1px solid red" }} onClick={disconnect}>
          채팅방 입장
        </button>
      </div>
    </div>
  );
}

+ Recent posts