채팅이나 실시간 통신에 필수로 쓰이는 프로토콜인
WebSocket
, 프론트엔드를 연동 중Stomp
를 연결해달라는 말에 혼란이 왔다.Stomp
는 프로토콜 아닌가? 왜Stomp
를 연결해달라는 거지? 라는 의문이 생겼다. 이 참에 WebSocket과 STOMP 내친김에 SockJS까지 알아보자.
웹소켓은 양방향 통신을 지원하는 프로토콜로, 실시간, 양방향 통신을 가능하게 하는 기술이다. 클라이언트와 서버 간에 한 번 연결이 이루어지면, 그 후로는 지속적으로 연결을 유지하며 데이터를 주고 받을 수 있다. 이를 통해 서버는 클라이언트에게 실시간으로 데이터를 푸시할 수 있다. 따라서 채팅 앱, 게임, 실시간 거래 정보 제공 등에 활용되고 있다.
특징 | WebSocket | HTTP | Polling (Long Polling) |
---|---|---|---|
통신 방식 | 양방향 통신 (클라이언트 ↔ 서버) | 단방향 통신 (클라이언트 → 서버) | 주기적인 요청 (클라이언트 → 서버) |
연결 유지 | 연결 유지 (한 번 연결 후 지속적) | 요청 후 연결 종료 | 연결 유지 (주기적인 요청/응답) |
실시간 데이터 처리 | 실시간 데이터 푸시 가능 | 실시간 처리 불편 | 실시간 처리 비효율적 |
서버 푸시 기능 | 서버가 클라이언트에게 데이터를 푸시 가능 | 불가능 (클라이언트 요청 시 응답) | 클라이언트가 요청을 보낸 후 서버에서 응답 |
효율성 | 매우 효율적 (지속적 연결, 실시간) | 비효율적 (매 요청마다 연결) | 비효율적 (주기적 요청, 많은 트래픽 발생) |
예시 | 채팅, 실시간 게임, 주식 거래 등 | 웹 페이지 요청, API 호출 등 | 실시간 데이터 업데이트가 필요한 경우 (Long Polling 사용) |
STOMP는 Simple Text Oriented Messaging Protocol 의 약자로, 웹소켓 위에서 동작하는 서브 프로토콜이다.
이는 클라이언트와 서버 간 메시지 교환에 있어 메시지의 형식, 목적지, 명령 유형 등을 표준화하는 역할을 한다.
WebSocket 자체는 텍스트 또는 바이너리 형식의 메시지 전송만 정의할 뿐, 그 내용이나 구조에 대해서는 전혀 규약이 없다.
따라서 WebSocket 만 사용하는 경우, 서버는 수신한 메시지가 채팅 메시지인지, 알림 메시지인지, 시스템 메시지인지 직접 구분해야 하며, 어떤 사용자에게 전달되는 메시지인지, 어떤 포맷(JSON, 텍스트 등)인지도 사용자가 명시적으로 정의하고 파싱해야 한다.
예를 들어, 순수 웹소켓으로 구현할 경우 다음과 같은 처리를 개발자가 직접 해야 한다 :
- 메시지 타입 구분
- 수신 대상 파악
- 데이터 파싱 및 처리
하지만 STOMP를 사용하면, 메시지의 목적지(destination), 명령 유형(CONNECT, SEND, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT), 헤더, 본문 등이 표준 형식으로 정의된다.
COMMAND
header1:value1
header2:value2
Body^@
전체적으로 위 틀을 이 공통 포맷을 따른다.
WebSocket 위에서 STOMP 프로토콜로 연결 요청을 보낸다.
CONNECT
accept-version:1.2
host:yourserver.com
^@
서버가 연결을 승인하면 특정 채널이나 목적지를 구독한다.
SUBSCRIBE
id:sub-0
destination:/topic/chatroom.1234
^@
채널로 메세지를 보낸다.
SEND
destination:/app/chat.sendMessage
{ "content": "Hello", "sender": "me" }
^@
특정 subscribe id의 구독을 해제할 때 사용한다.
UNSUBSCRIBE
id:sub-0
^@
서버와의 STOMP 연결을 종료할 때 사용한다.
DISCONNECT
receipt:77
^@
자 이제 WebSocket과 STOMP 개념은 구분이 잘 갔을 것이다. 그렇다면 클라이언트 입장에서는 STOMP를 어떻게 사용할까? 더불어 WebSocket 연결을 한다는 말과 STOMP 연결을 한다는 말이 무엇을 뜻할까?
- WebSocket 연결한다는 것
클라이언트가 서버에 WebSocket 연결을 설정하여, 이후 실시간으로 메시지를 주고받을 수 있게 됨을 의미한다.
- STOMP를 연결한다는 것
클라이언트가 WebSocket을 통해 STOMP 프로토콜로 메시지를 주고받겠다고 설정하는 것을 의미한다.
그렇다면 frontend 개발자 입장에서는 어떻게 연결을 하면 될까?
var socket = new WebSocket('ws://localhost:8080/chat');
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/chat', function(messageOutput) {
console.log('Received: ' + messageOutput.body);
});
stompClient.send("/app/chat.sendMessage", {}, JSON.stringify({'content': 'Hello, STOMP!'}));
});
위의 방식도 있지만 stomp.js 라이브러리 덕에 우리는 websocket 연결 따로 stomp 연결 따로 하지 않아도 된다.
아래는 @stomp/stompjs
를 이용한 WebSocket, STOMP 연결 예시이다.
export function connectWebSocketWithStomp(token, roomId, onMessageCallback) {
if (!token) {
console.error("🚫 토큰이 없습니다.");
return;
}
// STOMP Client 생성
stompClient = new Client({
brokerURL: `ws://211.47.114.99:10/v1/ws-chat/websocket?token=${token}`,
debug: (str) => console.log("STOMP Debug:", str),
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
// STOMP로 WebSocket 연결
onConnect: (frame) => {
console.log("✅ STOMP 연결 성공", frame);
// STOMP 채팅 채널 구독
stompClient.subscribe(`/topic/chat/${roomId}`, (message) => {
console.log("📩 받은 메시지:", message.body);
if (onMessageCallback) {
onMessageCallback(JSON.parse(message.body));
}
});
},
onStompError: (frame) => {
console.error("❌ STOMP 에러 발생", frame.headers["message"]);
console.error("🔍 상세 내용:", frame.body);
},
});
stompClient.activate();
}
위에서 주의할 점은, WebSocket 자체는 HTTP처럼 커스텀 헤더를 지원하지 않는다.
만약에 백엔드 개발자가 accessToken
을 헤더로 보내달라고 요청했다면 어떻게 할까?
→ 그냥 쿼리 파라미터로 토큰 넘기면 된다 ㅇㅇ..
brokerURL: `ws://211.47.114.99:10/v1/ws-chat/websocket?token=${token}`,
그렇기 때문에 위처럼 쿼리 파라미터로 토큰을 넘겨주니 잘 연결됐다.
돌이켜보니, 예전에 졸업프로젝트를 진행할 때 실시간 알림을 구현하기 위해서
WebSocekt
을 채택하여 구현했던 기억이 있다. 이 때sockJS
를 적용하니 안됐던 기억이 있다. 당시에는 Chat GPT도 없던 때라 하나하나 메서드를 지워가며 오류를 찾았던 기억이 있는데.withSockJS()
메서드가 있을 때 없을 때 차이가 났다.
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(),"/user")
.addHandler(socketTextHandler(),"/text")
.setAllowedOrigins("*")
.withSockJS();
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
@Bean
public WebSocketHandler socketTextHandler() {
return new SocketTextHandler();
}
}
위에서 WebSocket Handler를 등록하는 메서드에서,
.withSockJS();
라는 코드가 문제가 됐었었다 그럼 SockJS가 뭔지부터 살펴보자
SockJS는 웹소켓을 지원하지 않는 환경에서 WebSocket 기능을 대체하는 WebSocket 에뮬레이션 라이브러리이다.
WebSocket Emulation 이란?
1. Websocket 연결시도
2. 실패 -> 지원하지 않음
3. HTTP Streaming, Long Polling 등으로 양방향 통신 시도
의 방식으로 WebSocket을 대체한다.
웹소켓의 경우 모든 환경에서 지원하지 않는다. 이 때 SockJS를 이용하면 웹소켓을 지원하지 않는 환경에서도 가능하게 해준다.
어플리케이션이 WebSocket API를 사용하도록 허용하지만, 만약 내가 사용하는 클라이언트 브라우저가 WebSocket을 받아들이지 못하는 상태라면 어플리케이션 코드 변경 없이 런타임 대안을 실행하기 위한 것이다.
이런 상황에서 사용하는 것이 WebSocket Emulation이다.
나는 당시 안드로이드 환경과 Chrome 환경을 사용했었는데 안드일때는 OkHttp라는 라이브러리가 WebSocket을 지원해줬고, 웹개발할때는 위처럼 거의 대부분의 Chrome 이 WebSocket을 지원하고 있었다. ( https://caniuse.com/ 참고 )
그런데 왜 .withSockJS() 메서드 하나만으로 WebSocket 통신이 막혀버리는 것일까. 개념대로라면 SockJS는 WebSocket 연결실패 대책 아닌가?
→ 일부 서버는 SockJS에서 사용하는 xhr-streaming, eventsource 등을 CORS 또는 보안 정책 때문에 막아놓을 수 있다고 한다.
이 경우 withSockJS()를 사용하면 연결은 되지만 실제 통신이 막힌다.
웹 개발할 때 기억을 가다듬어 보니, CORS Error가 떴던 것 같다...
→ 안드로이드에서는 왜 안되는지 잘 모르겠다 ^^;; 사실 테스트 상에서만 안되고 Android에서는 작동했을 수도 있다.. 기억 미흡..
암튼 webSocket에서 CORS에러를 해결할 수도 있지만.. 웹개발을 하는 중이라면 요새 브라우저들은 webSocket을 대부분 지원하니 정신건강에는 그냥 sockJS를 빼는게 좋을 것이다..
여러분들은 이 점 참고하시어 즐개발하시길 🍀
참고
- https://youtu.be/rvss-_t6gzg?si=-acOlw4kT5LeVNE7
- https://parkmuhyeun.github.io/project/2022-11-29-chat/
- https://velog.io/@yyong3519/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%9B%B9%EC%86%8C%EC%BC%932
- https://velog.io/@horang12/%EC%9B%B9%EC%86%8C%EC%BC%93-%EC%B1%84%ED%8C%85-withSocketJs%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EC%97%90%EB%9F%AC
- https://seongil-shin.github.io/posts/web-STOMP/
- https://an-jjin.tistory.com/34