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라는 객체에 구독 채널과 메시지 전송자, 내용 등을 싣고 메시지 발행을 한다.
- 그럼 위의 해당 채널을 구독 중인 모든 클라이언트가 채팅 메시지를 보게 된다.
전체 로직 정리
두 명이 새로 접속했다.
- 닉네임을 변경함과 동시에 유저 목록에 반영됨을 확인할 수 있다.
- 채널을 클릭하면 해당 채널로 이동할 수 있다.
- 메시지를 입력하면 내가 입력할 경우 "나"라고 나오고, 다른 유저일 경우 해당 유저의 닉네임과 메시지가 출력된다.
다음 글에서는 백엔드 서버 코드를 살펴보겠다.
참고
뤼튼
728x90
'넓고 얕은 웹 지식' 카테고리의 다른 글
직접 만들어 보면서 이해하는 웹소켓 (2) - 크롬 확장프로그램을 이용하여 실시간 채팅방 구현하기 (0) | 2024.05.12 |
---|---|
직접 만들어 보면서 이해하는 웹소켓 (1) - 왜 웹소켓(WebSocket)인가? (0) | 2024.05.10 |
로컬에서 도커로 nginx 설치 & 스프링 부트의 프록시 서버 테스트 해보기 (1) | 2023.11.14 |
HTTPS 통신 흐름을 이해하고 NginX에 SSL/TLS를 적용해보자 (1) | 2023.05.17 |
Apache와 NginX 비교 및 차이점 (0) | 2023.05.15 |