본문 바로가기
넓고 얕은 웹 지식

직접 만들어 보면서 이해하는 웹소켓 (3) - STOMP 실시간 채팅방 구현(프론트엔드 영역)

by 팡펑퐁 2024. 5. 23.
728x90

지난 글에 이어 STOMP에 대해 간단히 알아보고, 이제는 소켓 통신에 대한 실습을 넘어 간단하게 실시간 채팅 웹사이트를 구현해 보겠다.

 

📌 WebSocket의 한계와 STOMP의 등장 배경

  • WebSocket은 실시간 통신을 위한 효율적인 기술이지만, 사용상 몇 가지 한계가 있다

🥲 프로토콜 수준의 메시지 패턴 부재

  • 저수준의 프로토콜로 단순한 메시지 전송 기능만 제공한다.
  • 따라서 복잡한 메시징 패턴이나 메시지 형식을 개발자가 직접 설계하고 구현해야 한다.

🥲 메시지 라우팅의 어려움

  • 클라이언트에서 서버로 메시지를 전송할 때 해당 메시지를 어떻게 처리할 것인지, 어느 클라이언트에게 전달할 것인지에 대한 라우팅 로직 역시 개발자가 직접 구현해야 한다.

🥲 표준화된 메시징 기능의 부재

  • 메시지 교환에 대한 고급 기능(예: 메시지 브로커나 트랜잭션 메시징)을 제공하지 않는다.
  • 이 역시 개발자가 직접 기능을 구현해야 하는 부담을 가진다.

 

📌 STOMP(Simple Text Oriented Messaging Protocol)의 등장

  • STOMP는 WebSocket의 이러한 한계를 극복하기 위해 등장한 메시징 프로토콜이다.
  • STOMP는 텍스트 기반의 간단한 프로토콜로 표준화된 메시지 전송 패턴과 명령 세트를 제공한다.


🤗 STOMP의 장점

표준화된 메시지 패턴

  • STOMP는 `CONNECT`, `SEND`, `SUBSCRIBE`, `UNSUBSCRIBE`, `BEGIN`, `COMMIT`, `ABORT`, `ACK`, `NACK` 등의 명령어를 통해 표준화된 메시징 패턴을 제공한다.
    • 이를 통해 개발자는 복잡한 메시징 로직을 쉽게 구현할 수 있다.

메시지 라우팅과 구독 관리

  • STOMP를 사용하면 서버 측에서 클라이언트의 구독을 관리하고, 특정 주제에 대한 메시지를 구독하는 클라이언트에게 자동으로 메시지를 전달할 수 있다. 이는 메시지 라우팅을 간단하게 만들어 준다.

다양한 메시지 브로커와의 호환성

  • STOMP는 RabbitMQ, ActiveMQ와 같은 다양한 메시지 브로커와 호환된다.
    • 이를 통해 개발자는 기존의 메시지 브로커를 활용하여 더욱 강력하고 유연한 메시징 솔루션을 구축할 수 있다.

 

⚒️ 실시간 채팅 만들어보기

💡 지난 글에서는 크롬 확장 프로그램을 이용했기 때문에 서버로부터 소켓 통신을 주고받는 웹페이지를 따로 구현할 필요가 없었다. 그러나, 이번에는 완전한 실시간 채팅방 구현하는 것을 목표로 하기 때문에 간단한 웹페이지를 함께 만들어야 한다.

 

📃 프론트 엔드 영역

채팅 페이지

  • 현재 상태 표시를 위해 유저가 접속 중인 채널명과 닉네임 표시를 화면 왼쪽에 배치했다.
  • 오른쪽에는 현재 서버에 접속 중인 유저 목록이 아이디로 나오고, 유저들이 만든 채널 리스트가 그 옆에 위치한다.
  • 코드는 STOMP 코드 위주로만 설명한다.

 

$( "#connect" ).click(function() { connect(); });
$( "#disconnect" ).click(function() { disconnect(); });
  • 연결 버튼을 누르면 소켓 통신이 시작되고, 해제를 누르면 연결을 종료한다.

 

connect() 메서드

function connect() {

    var socket = new SockJS('/stomp');
    stompClient = Stomp.over(socket);

    // 유저 닉네임, 세션 아이디 생성 후 서버 등록
    currentNickname = generateRandomString();
    $("#user_name").html("닉네임: " + currentNickname);

    var headers = {
        "nickname" : currentNickname
    }

    stompClient.connect(headers, function (frame) {
        setConnected(true);
        console.log('Connected: ' + frame);

        stompClient.subscribe('/sub/user-list', function (message) {
            const response = JSON.parse(message.body)
            if (response.message && response.message === "DUPLICATED") {
                $("#name").val('');
                if (response.sessionId === userSessionId) {
                    $("#user_name").html("닉네임: " + exNickname);
                    alert("이미 등록된 닉네임입니다.");
                }
            } else {
                $("#current_user_list").empty();
                currentUsers = [];
                Object.keys(response).forEach((key) => {
                    $("#current_user_list").append("<li>" + key +"</li>");
                    currentUsers.push(key);
                    if (key === currentNickname) {
                        userSessionId = response[key];
                    }
                });
            }
        });

        stompClient.subscribe('/sub/user-count', function (message) {
            let count = JSON.parse(message.body);
            $("#user-count").empty();
            $("#user-count").append("접속 중인 유저 수 : " + count);
        });

        get_channel();
    });
}
  • 연결 버튼을 누르면 connect() 함수의 코드가 순서대로 진행된다.

 

var socket = new SockJS('/stomp');
  • SockJS 클라이언트를 생성한다.
    • SockJS는 웹소켓을 지원하지 않는 브라우저에서도 작동할 수 있도록 폴백 옵션을 제공한다.
    • 즉, 웹소켓이 가능한 환경에서는 웹소켓을 사용하고, 그렇지 않은 경우에는 다른 방법(예: HTTP 롱 폴링)을 사용하여 비슷한 기능을 제공한다.
  • /stomp은 서버의 특정 엔드포인트로, 이곳과 연결을 시도한다.
  • 이 엔드포인트는 서버 측에서 SockJS 서버를 설정할 때 정의된다.
    • 서버 코드는 백엔드 영역에서 자세히 다루겠다.

 

stompClient = Stomp.over(socket);
  • SockJS 연결을 통해 STOMP 클라이언트를 생성한다.
  • Stomp.over 함수는 매개변수로 SockJS 객체를 받아 STOMP 세션을 초기화한다.
  • 이렇게 함으로써 SockJS 연결을 통해 STOMP 메시징 프로토콜을 사용할 준비가 끝난다.
  • 위 두 줄의 코드를 사용하여 클라이언트 측 JavaScript에서 서버와의 통신을 위한 STOMP 연결을 설정할 수 있는 것이다.
  • 이후 stompClient.connect() 함수를 사용하여 서버에 연결하고, subscribe() 메서드로 특정 주제를 구독하거나 send() 메서드로 메시지를 전송할 수 있다. 

 

stompClient.connect(headers, function (frame) {
	...
    
    stompClient.subscribe('/sub/user-list', function (message) {
    	...
    });
    
    stompClient.subscribe('/sub/user-count', function (message) {
        ...
    });
    
    get_channel();
});

// 채널 접속에 대한 메서드 별도 분리
function get_channel() {

    stompClient.subscribe("/sub/channel-list", function (channel_list) {
    	...
    });
    
    ...
}

connect()

  • stompClient를 통해 서버와 통신하여 소켓 연결을 수립하는데 이때 첫 번째 인자로 header를 보낸다.
  • header에는 최초로 접속 시 랜덤으로 만들어지는 문자열 값이 들어있으며 나는 이 값을 유저의 nickname으로 사용했다.
  • 이를 서버에 보내 서버에서 해당 유저의 nickname을 유저 리스트에 관리하게 했다.

subscribe()

  • 이후 서버에 접속 중인 유저 목록과 수, 생성된 채널 목록을 출력하기 위한 메시지 구독을 시작한다.

 

연결 후

>>> CONNECT

  • 브라우저에서 서버에게 보낸 STOMP 메시지가 나온다. 브라우저에서 생성한 nickname을 header에 포함시켜 전달하는 것을 확인할 수 있다.
  • 연결이 완료되면 위 코드 대로  총 세 개의 주제(user-list, user-count, cahnnel-list)를 구독하게 된다.
  • 서버에서는 소켓 연결을 이벤트로 설정하여 연결과 동시에 유저 목록과 수, 채널 목록이 바로 표시되도록 설정했다.

<<< MESSAGE

  • 따라서 최초 연결 시 서버로부터 {"본인의 nickname(클라이언트에서 생성)" : "sessionid(서버에서 생성)"}를 받는다.
    • 현재 접속자수는 1명(나 혼자), 생성된 채널은 없기 때문에 빈 배열을 받는다.

 

닉네임 변경, 채널 만들기

  • 닉네임 변경하기와 채널 이동하기를 눌러보자.

 

변경하기 버튼 눌렀을 때

// 닉네임 설정
$("#nickname").click(function() {

    if (currentUsers.includes($("#name").val())) {
        alert("이미 등록된 닉네임입니다. 다시 입력해주세요.")
        return;
    }

    exNickname = currentNickname;
    currentNickname = $("#name").val();
    console.log("currentNickname : " + currentNickname);
    if (currentNickname === null || currentNickname === undefined || currentNickname === "") {
        alert("닉네임이 제대로 입력되지 않았습니다.(\"\", \" \" 등 빈 값은 입력될 수 없습니다.)")
    } else {
        $("#user_name").html("닉네임: " + currentNickname);

        var enroll = {
            exNickname: exNickname,
            changeNickname: currentNickname,
            sessionId: userSessionId
        }

        stompClient.send("/pub/enroll", {}, JSON.stringify(enroll));

        $("#name").val('');
        alert("닉네임을 '" + currentNickname + "'(으)로 변경합니다.");
    }
});
  • 닉네임 변경하기를 누르면 위 로직이 실행된다.

 

stompClient.send("/pub/enroll", {}, JSON.stringify(enroll));
  • enroll이란 객체를 만들어 데이터를 싣고 서버의 특정 엔트포인트에 메시지를 발행한다.
  • 이렇게 되면 서버에서는 exNickname(변경 전 닉네임)과 changeNickname(변경 후 닉네임), sessionId를 통해 유저 목록에서 해당 닉네임을 찾아 새로운 닉네임으로 변경하게 될 것이다. 이는 그대로 모든 접속 중인 클라이언트에게 전송되어 다른 브라우저에서도 해당 유저의 이름 변경을 유저 목록에서 실시간으로 확인할 수 있게 된다.

 

이동하기 버튼 눌렀을 때

// 채널 변경
$("#enter_room").click(function() {
    changeChannel();
});

function enter_channel(channel) {
    if (channel && channel.trim() !== "") {
        subscriptionChannel = channel;
        stompClient.subscribe('/sub/channel/' + subscriptionChannel, function (message) {
            showMessage(JSON.parse(message.body));
        });
        $("#channel_name").html("채널명: " + subscriptionChannel);
        alert("채팅방 '" + subscriptionChannel + "'에 입장했습니다.");
        console.log("구독중인 채널: " + subscriptionChannel);

        stompClient.send("/pub/channel-list", {}, channel);
        $("#room_name").val('');
    } else {
        console.log("유효하지 않은 채널로 구독 취소");
    }
}
  • 해당 채널을 들어가게 되면 실행되는 코드이다.

 

stompClient.subscribe('/sub/channel/' + subscriptionChannel, function (message) {
	...
}
  • 채팅방 기능의 핵심인 채팅 보기이다.
  • 채널 접속 시 해당 채널의 채팅을 볼 수 있는 메시지를 구독하게 된다.

 

stompClient.send("/pub/channel-list", {}, channel);
  • channel-list 엔드포인트에 해당 채널 이름으로 메시지 발행하여 모든 클라이언트가 채널 이름을 보게 한다.
    • 채널 리스트의 채널을 클릭하면 해당 채널로 이동하게 된다.

 

메시지 입력 후 보내기

// 메시지 보내기
$( "#send" ).click(function() { sendMessage(); });

function sendMessage() {
    if (subscriptionChannel === null) {
        alert("구독할 채널을 먼저 설정해야 합니다."); // 구독 채널이 설정되지 않았을 경우 알림을 표시합니다.
        return;
    }

    if (currentNickname === null || currentNickname === undefined || currentNickname === "") {
        alert("닉네임을 입력해주세요.");
        return;
    }

    var messageContent = $("#message").val();
    if (messageContent && stompClient) {
        var chatMessage = {
            channelName: subscriptionChannel,
            sender: currentNickname,
            sessionId: userSessionId,
            content: messageContent
        };
        stompClient.send("/pub/chat", {}, JSON.stringify(chatMessage));
        $("#message").val('');
    }
}
stompClient.send("/pub/chat", {}, JSON.stringify(chatMessage));
  • chatMessage라는 객체에 구독 채널과 메시지 전송자, 내용 등을 싣고 메시지 발행을 한다.
  • 그럼 위의 해당 채널을 구독 중인 모든 클라이언트가 채팅 메시지를 보게 된다.

 

전체 로직 정리

두 명이 새로 접속했다.

 

  • 닉네임을 변경함과 동시에 유저 목록에 반영됨을 확인할 수 있다.

 

  • 채널을 클릭하면 해당 채널로 이동할 수 있다.

 

  • 메시지를 입력하면 내가 입력할 경우 "나"라고 나오고, 다른 유저일 경우 해당 유저의 닉네임과 메시지가 출력된다.

 

다음 글에서는 백엔드 서버 코드를 살펴보겠다.

 

 

참고

뤼튼

https://growth-coder.tistory.com/157

728x90