반응형
해당 글에서는 WebSocket 서버와의 연결을 통해서 STOMP Client를 이용하여 실시간 웹 소켓 통신을 구현하는 방법에 대해 알아봅니다
💡 [참고] STOMP을 이용한 웹 소켓 및 Socket.io를 이용한 소켓 연결에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
분류 | 상세 분류 | 주제 | 링크 |
STOMP | Server | Spring Boot WebSocket + Stomp 이해하고 구성하기 -1: 초기 구성 및 간단 소켓 연결 | https://adjh54.tistory.com/573 |
STOMP | Server | Github : Spring Boot WebSocket + STOMP 소스코드 | https://github.com/adjh54ir/blog-codes/tree/main/spring-boot-websocket |
STOMP | Client | STOMP 기반 소켓 서버 활용하기 + Typescript : @stomp/stompjs, sockjs-client | https://adjh54.tistory.com/574 |
STOMP | Client | STOMP 기반 소켓 서버 활용하기 소스코드 | https://github.com/adjh54ir/blog-codes/blob/main/react-chatting/src/components/stomp/StompComponent.tsx |
Socket.io | Server & Client | Socket.io 기반 소켓 서버 구축 방법 -1 : 구성 요소 및 흐름 + React 기반 채팅 화면 구성 | https://adjh54.tistory.com/548 |
Socket.io | Server & Client | Socket.io 기반 소켓 서버 구축 방법 -2 : Room, Namespace 별 소켓 통신 방법 | https://adjh54.tistory.com/549 |
Socket.io | Server & Client | Socket.io 기반 소켓 서버 구축 방법 -3: PM2 클러스터링 + Redis 기반 분산 처리 | https://adjh54.tistory.com/567 |
Socket.io | Client | PM2(Process Manager 2) 이해하고 주요 특징 알아보기 | https://adjh54.tistory.com/551 |
Socket.io | Server | Github : Socket.io 기반 웹 소켓 소스코드 | https://github.com/adjh54ir/blog-codes/tree/main/node-wss-server |
Socket.io | Client | Github : Socket.io를 이용하는 웹 클라이언트 소스코드(채팅) | https://github.com/adjh54ir/blog-codes/tree/main/react-chatting |
1) 소켓 통신(Socket Communication)
💡 소켓 통신(Socket Communication)
- 네트워크에서 두 컴퓨터 간의 ‘실시간 양방향 통신’을 제공하는 기술을 의미합니다. 양방향 통신은 송신자 측이 데이터를 수신할 수도 있으며, 수신자 측이 데이터를 송신할 수 있는 구조를 가집니다.
- 두 컴퓨터 간에는 '특정 IP 주소와 포트번호'의 조합으로 네트워크 간의 연결을 수행하며, 수신자(클라이언트)는 데이터를 요청하면 송신자(서버)에서는 요청에 대한 응답을 제공해 주는 '클라이언트-서버' 모델을 사용하여 데이터를 송수신할 수 있습니다.
- 소켓 통신은 'TCP'와 'UDP'라는 두 가지 주요 프로토콜을 사용할 수 있습니다.
- 이러한 프로토콜을 통해 웹 서버, 이메일 서버, 데이터베이스 서버 등 다양한 응용 프로그램에서 사용되며, 실시간 통신, 스트리밍, 온라인 게임 등에서도 사용됩니다.
용어 | 설명 |
패킷(Packet) | 네트워크를 통해 전송되는 데이터의 기본단위를 의미합니다. 하나의 정보는 여러개의 패킷으로 나누어져 전송되며, 출발지와 목적지 정보(메타데이터)와 실제 전송되는 데이터(페이로드)로 구성이 되어 있습니다. |
소켓(Socket) | 네트워크를 통해 데이터(패킷)을 전송하기 위한 일종의 ‘통로’를 의미합니다. |
채널(Channel) | 서버와 클라이언트 간에 데이터를 주고 받는 ‘전송 경로’를 의미합니다. 이 채널을 통해 데이터는 송신자에서 수신자로 전달되며, 데이터의 전송방향(일방향 또는 양방향)과 전송 모드(단일 대화 또는 다중 대화)에 따라 구분될 수 있습니다. |
클라이언트(Client) | 데이터를 요청하는 측으로 서버와 연결을 하여 서버로부터 데이터를 받는 역할을 수행합니다. |
서버(Server) | 데이터를 요청을 받아 응답하여 클라이언트에게 데이터를 전송하는 역할을 수행합니다. |
TCP(Transmission Control Protocol) | 연결 지향적인 프로토콜로, 데이터를 전송하기 전에 송신자와 수신자 간의 연결을 설정합니다. 그 후 패킷으로 나누어 전송하고 패킷이 수신자에게 전송되었는지 확인합니다. |
UDP(User Datagram Protocol) | 비연결 지향적인 프로토콜로, 패킷을 수신자에게 보내고 패킷이 정확하게 도착했는지 확인하지 않습니다. TCP보다는 빠르지만 패킷 순서나 손실에 대한 보장이 없어 신뢰성이 낮습니다 |
HandShake | 각각 네트워크에서 연결을 설정하는 단계를 의미합니다. 이는 통신이 시작되기 전에 두 장치 사이에 통신 세션을 설정하고 서로 데이터 전송을 준비하는 과정을 의미합니다. |
💡[참고] Socket 통신에 대해 상세히 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
2) STOMP(Simple or Streaming Text Oriented Messaging Protocol)
💡 STOMP(Simple or Streaming Text Oriented Messaging Protocol)
- 텍스트 기반의 메시징 프로토콜을 의미합니다. 클라이언트와 메시지 브로커 간의 통신을 간단하고 효율적으로 수행할 수 있도록 설계되었습니다. 이는 WebScoket에서 쉽게 메시지를 주고받을 때 사용이 됩니다.
- 해당 프로토콜은 Websocket을 사용하여 클라이언트와 서버 간의 메시지 교환을 구조화하고 표준화하는 데 사용됩니다.
명령어 | 설명 |
CONNECT | 클라이언트가 서버에 연결을 요청할 때 사용합니다. |
SEND | 클라이언트 또는 서버가 특정 목적지로 메시지를 보낼 때 사용합니다. |
SUBSCRIBE | 클라이언트가 특정 목적지의 메시지를 구독할 때 사용합니다. |
UNSUBSCRIBE | 클라이언트가 특정 목적지의 메시지 구독을 취소할 때 사용합니다. |
DISCONNECT | 클라이언트가 서버와의 연결을 종료할 때 사용합니다. |
1. STOMP 기반 연결 및 통신 화면
💡STOMP 기반 연결 및 통신 화면
- Google Chrome Extension을 통해서 구성한 웹 소켓 연결을 수행하면 아래와 같이 연결이 되고 데이터를 주고받는 것을 확인할 수 있습니다.
3) STOMP Client 화면 구성
1. 구성환경
💡구성 환경
- 해당 글에서는 아래의 환경에서 STOMP Client가 구성이 되어 있습니다.
구성 | 환경 버전 |
Node | 21.7.1 |
React | 18.3.1 |
Typescript | 4.9.5 |
@stomp/stompjs | 7.0.0 |
stompjs | 2.3.3 |
2. 초기 개발 환경 구축
💡 초기 개발환경 구축
- 아래와 같이 CRA + Typescript 기반으로 React 프로젝트와 Router를 구성하였습니다.
3. 라이브러리 추가
💡라이브러리 추가
- sockjs-client : WebSocket의 대체 라이브러리로, 브라우저와 웹 서버 간의 로우 레벨 크로스-브라우저 JavaScript 객체를 생성합니다.
- @stomp/stompjs : STOMP(Simple Text Oriented Messaging Protocol)는 메시징 프로토콜로, @stomp/stompjs는 이를 JavaScript에서 구현한 클라이언트 라이브러리입니다.
$ yarn add net sockjs-client @stomp/stompjs
$ yarn add --dev @types/@stomp/stompjs
4. 컴포넌트 구성 : StompComponent.tsx
💡컴포넌트 구성 : StompComponent.tsx
- 구성한 Spring Boot WebSocket에 최초 연결을 하고 Endpoint를 통해 Sub(구독), Pub(게시)를 통해 실시간 데이터 통신을 수행합니다.
1. stompHandler.connect()
- STOMP의 소켓에 연결을 시도합니다.
- onConnect로 웹 소켓 연결이 완료되면 채팅방을 입장시켜 줍니다.(setIsEnterChat)
- client.subscribe를 통해서 SUB_ENDPOINT(/sub/message)로 특정 메시지에 대한 수신을 구독(Sub)합니다. - client.activate()를 통해서 구성한 client를 활성화합니다.
2. stompHandler.sendMessage()
- 채팅방에서 입력한 값을 웹 소켓으로 전송(Pub)합니다.
- 생성된 Client가 존재하고 연결이 되어 있으며, 전송하려는 메시지가 있을 때 동작을 수행합니다.
- wsClient.publish를 통해서 PUB_ENDPOINT(/pub/messages)로 특정 메시지를 전송(Pub)합니다.
3. stompHandler.disconnect()
- STOMP의 소켓의 연결을 종료합니다.
- 채팅방 입장을 종료합니다(setIsEnterChat)
- wsClient.deactivate()를 통해서 구성한 client를 비 활성화 합니다.
import React, { useEffect, useState } from 'react';
import { Client, IFrame, IMessage } from '@stomp/stompjs';
import SockJS from 'sockjs-client';
interface MessagesType {
sender: string; // 보내는 주체
content: string; // 보내는 메시지
}
const StompComponent = () => {
const SERVER_URL = '<http://localhost:8081/ws-stomp>'; // STOMP 연결 엔드포인트
const PUB_ENDPOINT = '/pub/messages'; // 메시지를 전송하기 위한 엔드포인트
const SUB_ENDPOINT = '/sub/message'; // 메시지를 수신하기 위한 엔드포인트
// STOMP가 연결되고 생성한 Client를 관리하는 상태 관리
const [wsClient, setWsClient] = useState<Client>();
// 채팅방 입장 여부
const [isEnterChat, setIsEnterChat] = useState<boolean>(false);
// 채팅에서 누적되는 데이터를 관리합니다.
const [messages, setMessages] = useState<MessagesType[]>([]);
// 사용자의 구분을 짓기 위해 임시로 발급한 사용자 아이디
const [userId, setUserId] = useState(Math.random().toString(36).substr(2, 9));
// 채팅에서 보내지는 데이터를 관리합니다.
const [messageObj, setMessageObj] = useState<MessagesType>({
content: '',
sender: userId,
});
/**
* STOMP를 연결하고 라이프사이클을 관리하는 Handler
*/
const stompHandler = (() => {
return {
/**
* STOMP 연결을 시도합니다.
* @returns
*/
connect: () => {
// [STEP1] 연결 시 Client 객체를 생성합니다.
const client = new Client({
webSocketFactory: () => new SockJS(SERVER_URL),
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
// [STEP2] 웹 소켓 연결
onConnect: (conn: IFrame) => {
console.log('[+] WebSocket 연결이 되었습니다.', conn);
setIsEnterChat(true); // 채팅방 입장여부 상태를 변경
// [WebSocket - Subscribe] 특정 엔드포인트로 메시지를 수신합니다.
client.subscribe(SUB_ENDPOINT, (message: IMessage) => {
const receiveData = JSON.parse(message.body);
setMessages((prevMessages) => [
...prevMessages,
{
content: receiveData.content,
sender: receiveData.sender,
},
]);
});
},
// 웹 소켓 연결 종료
onWebSocketClose: (close) => {
console.log('[-] WebSocket 연결이 종료 되었습니다.', close);
},
// 웹 소켓 연결 에러
onWebSocketError: (error) => {
console.error('[-] 웹 소켓 연결 오류가 발생하였습니다.', error);
},
// STOMP 프로토콜 에러
onStompError: (frame) => {
console.error('Broker reported error: ' + frame.headers['message']);
console.error('Additional details: ' + frame.body);
},
});
setWsClient(client); // 구성한 Client 객체를 상태 관리에 추가합니다.
client.activate(); // Client를 활성화 합니다.
return () => {
stompHandler.disconnect(); // Socket 연결을 종료합니다.
};
},
/**
* 웹 소켓 메시지를 전송합니다.
*/
sendMessage: () => {
if (wsClient && wsClient.connected && messageObj.content.trim() !== '') {
// [WebSocket - Publish] 특정 엔드포인트로 메시지를 전송합니다.
wsClient.publish({
destination: PUB_ENDPOINT,
body: JSON.stringify({ ...messageObj, sender: userId }),
});
// 입력한 값을 초기화합니다.
setMessageObj({ content: '', sender: userId });
}
},
/**
* 웹 소켓 연결을 종료합니다.
*/
disconnect: () => {
console.log('[-] 웹 소켓 연결을 종료합니다.');
if (wsClient) {
wsClient.deactivate();
setWsClient(undefined);
setIsEnterChat(false);
}
},
};
})();
return (
<div style={{}}>
{!isEnterChat ? (
<div
style={{
flexDirection: 'column',
justifyContent: 'center',
alignContent: 'center',
alignItems: 'center',
textAlign: 'center',
}}>
<h2>Spring Boot WebSocket 채팅방에 오신것을 환영합니다.</h2>
<button onClick={stompHandler.connect}>채팅방 입장</button>
</div>
) : (
<div
style={{
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignContent: 'center',
alignItems: 'center',
textAlign: 'center',
}}>
<h1>STOMP을 이용한 채팅방입니다. </h1>
<div style={{ flexDirection: 'row' }}>
<input
type='text'
value={messageObj.content}
onChange={(e) => setMessageObj({ ...messageObj, content: e.target.value })}
/>
<button onClick={stompHandler.sendMessage}>전송</button>
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
height: 300,
backgroundColor: '#f5d682',
border: '1px solid red',
margin: 20,
}}>
{messages.map((item, index) => (
<h1
style={{
fontSize: 13,
textAlign: 'left',
}}
key={`messages-${index}`}>
{item.sender === userId ? `[ME] ${item.content}` : `[OTHER] ${item.content}`}
</h1>
))}
</div>
<div style={{ marginTop: 10 }}>
<button onClick={stompHandler.disconnect}> 채팅방 나가기 </button>
</div>
</div>
)}
</div>
);
};
export default StompComponent;
4) 결과 확인
1. 채팅방 입장 결과 화면
💡채팅방 입장 결과 화면
- 아래와 같이 다른 브라우저 내에서 접속을 하였습니다.
💡 연결이 완료되어 아래와 같이 연결에 대한 결과값을 반환받았습니다.
2. 채팅 결과 화면
💡 채팅 결과 화면
- 아래와 같이 서로 간의 데이터를 주고받음을 확인하였습니다.
- ME의 경우는 입력한 브라우저이고, OTHER의 경우는 다른 브라우저를 의미합니다.
분류 | 엔드포인트 | 설명 |
소켓 서버 연결 | http://localhost:8081/ws-stomp | 소켓 서버에 연결을 하는데 사용이 되는 엔드포인트 입니다. |
메시지 전송 | /pub/messages | 메시지는 전송하기 위해 사용되는 엔드포인트입니다/ |
메시지 수신 | /sub/message | 메시지를 수신하기 위해 사용되는 엔드포인트입니다. |
3. 채팅방 나가기 결과화면
💡 채팅방 나가기 결과화면
- 채팅방을 나가는 경우 아래와 같은 정상적인 소켓 종료를 확인하였습니다.
💡 [참고] 위에 소스코드는 아래의 Repository 내에서 확인이 가능합니다.
오늘도 감사합니다. 😀
반응형