목적
- 로스트 아크 게임 유저들을 위한 깐부 찾기 랜덤채팅 사이트
구성
- 프론트 : React, Shadcn, Javascript, Socket, STOMP
- 백 : SpringBoot, Java, MySQL, Redis, WebSocket, STOMP
목차
웹소켓 + STOMP 설정
웹소켓과 STOMP에 대해서는 이전 포스팅에서 설명하고 넘어갔으니 스프링에서 어떻게 설정하는지에 대해서 자세히 알아보자. 첫 포스팅에도 간단히 첨부는 하고 넘어갔던 데이터들이다.
build.gradle
// websocket - 실시간 통신
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.webjars:sockjs-client:1.1.2'
// stomp - 구독
implementation 'org.webjars:stomp-websocket:2.3.3-1'
WebSocketConfig
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 메세지 구독 경로
config.enableSimpleBroker("/sub");
// 메시지 발행 경로 설정
config.setApplicationDestinationPrefixes("/pub");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//Client에서 websocket 연결할 때 사용할 API 경로를 설정 - 채팅용
registry.addEndpoint("/chat")
.setAllowedOriginPatterns("*")
.withSockJS();
//Client에서 websocket 연결할 때 사용할 API 경로를 설정 - 매칭용
registry.addEndpoint("/match")
.setAllowedOriginPatterns("*")
.withSockJS();
}
}
1.configureMessageBroker
웹소켓 설정을 찾아봤다면 해당 함수 내에서 enableSimpleBroker, setApplicationDestinationPrefixes
를 설정해주는 것을 많이 보았을 것이다.
우선 Spring에서 안내하는 기본 설정은 아래와 같다.
package com.example.messagingstompwebsocket;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/gs-guide-websocket");
}
}
어차피 configureMessageBroker 안에 선언하는 경로는 커스텀 설정이기 때문에 똑같이 할 필요는 없으며 여기서 사용되는 경로는 차후에 '클라이언트 -> 서버 / 서버-> 클라이언트'가 서로 메세지를 보낼 때 구독인지 발행인지 판단하는데 사용되는 경로라고 생각하면 되겠다.
프론트에서 아래와 같은 형태로 발행(/pub)과 구독(/sub)을 판단하며 이하 세부 주소는 MessageController에서 매핑하여 연결될 수 있게끔 작업한다.
// 발행
stomp.send('/pub/chat/exit', headers, data);
// 구독
stomp.subscribe('/sub/chat/room/'+roomInfo.roomKey, (message) => {
const newMessage = JSON.parse(message.body);
// 구독해서 받아온 메세지 구현부
});
2. registerStompEndpoints
다음으로 registerStompEndpoints는 소켓 통신을 할 때 사용되는 endpoint를 설정하는 곳이다.
소켓 연결을 할 때, 어느 주소로 소켓 연결을 시작할 것 인지를 선언하는 곳인데 api 주소와 유사하다고 생각하면 된다.
이는 프론트에서 아래와 같은 형태로 sockJS연결할 때 사용된다.
const socket = new SockJS("http://localhost:8080/chat");
MessageController
1. 백엔드 STOMP구현
private final SimpMessageSendingOperations template;
@MessageMapping("/chat/enter")
public void enterUser(ChatMessages chat, SimpMessageHeaderAccessor headerAccessor) {
chat.setMessage("채팅방에 참여하였습니다.");
template.convertAndSend("/sub/chat/room/" + chat.getRoomKey(), chat);
// 입장 관련 Service 구현부
}
@MessageMapping("/chat/message")
public void sendMsg(ChatMessages message) {
template.convertAndSend("/sub/chat/room/"+ message.getRoomKey(), message);
// 메세지 관련 Service 구현부
}
@MessageMapping("/chat/exit")
public void exitUser(ChatMessages chat) {
chat.setMessage("채팅방에서 퇴장하였습니다.");
template.convertAndSend("/sub/chat/room/" + chat.getRoomKey() , chat);
// 퇴장 관련 Service 구현부
}
앞서 간단하게 언급했는데
프론트에서 연결 후 , /pub/chat/~ 로 발행하게 된다면 내가 만든 컨트롤러에서 @MessageMapping("/chat/~") 어노테이션 경로로 해당 요청을 받을 수 있게 된다.
그리고 발행 내용을 받았으니 서버에서는 해당 내용을 구독자들에게 뿌리는데 그 부분이 아래 코드다.
template.convertAndSend("/sub/chat/room/" + chat.getRoomKey(), chat);
2. 백엔드와 프론트엔드 간 STOMP
그럼 쉬운 이해를 위해 프론트 <-> 백 사이의 통신이 어떻게 이루어 지는지 보자.
const [stompClient, setStompClient] = useState(null);
// socket오픈
useEffect(() => {
const socket = new SockJS("http://localhost:8080/chat");
const stomp = Stomp.over(socket);
setStompClient(stomp);
}, []);
// 채팅방 입장
useEffect(() => {
if (stompClient) {
let headers = {Authorization: getCookie('accessToken')};
// stomp연결 및 /pub/chat/enter경로로 메세지DTO발행
stompClient.connect(headers, () => {
if(props.enter == true && stompClient){
stompClient.send('/pub/chat/enter', {},
JSON.stringify({
type: 'ENTER',
roomKey: roomInfo.roomKey,
sender: uuId,
message: "",
sendTime: moment().format('YYYY-MM-DDTHH:mm:sszz')}
));
}
}
}, [stompClient, roomInfo]);
@MessageMapping("/chat/enter")
public void enterUser(ChatMessages chat) {
chat.setMessage("채팅방에 참여하였습니다.");
template.convertAndSend("/sub/chat/room/" + chat.getRoomKey(), chat);
chatRepository.saveMessages(chat, ChatMessages.MessageType.ENTER);
chatRoomRepository.updateLastChat(chat.getRoomKey(), chat.getSendTime(), chat.getMessage());
}
// 구독
useEffect(() => {
if (stompClient) {
stompClient.subscribe('/sub/chat/room/'+roomInfo.roomKey, (message) => {
const newMessage = JSON.parse(message.body);
setChatHistory((prevHistory) => [...prevHistory, newMessage]);
});
}
}, [stompClient]);
프론트에서는 sockJS를 연 순간부터 해당 엔드포인트로 양방향 통신이 이루어 지고 있다.
const socket = new SockJS("http://localhost:8080/chat");
발행을 프론트에서 행한다면
stompClient.send('/pub/chat/enter', {},
백에서는 MessageMapping을 통해 발행 내용을 받고
@MessageMapping("/chat/enter")
메세지 가공이 필요하다면 가공을 한 후 해당 발행내용 타겟 구독자들에게 다시 발행해준다.
여기서 chat.getRoomKey()의 경우 해당 방 구독자들만 받아볼 수 있는 키 값이라고 보면 되겠다.
chat.setMessage("채팅방에 참여하였습니다.");
template.convertAndSend("/sub/chat/room/" + chat.getRoomKey(), chat);
프론트에서는 앞서 sockJS연결한 정보에 관해 stompClient에 갖고 있고, 해당 객체에 작업이 들어온다면 그걸 받는 useEffect처리를 해둔 상황이다.
useEffect(() => { ), [stompClient]);
그리고 내 구독함에 메세지가 들어온다면 해당 내용을 화면에다 뿌려준다.
stompClient.subscribe('/sub/chat/room/'+roomInfo.roomKey, (message) => {
const newMessage = JSON.parse(message.body);
setChatHistory((prevHistory) => [...prevHistory, newMessage]);
});
다 적용하고 직접 겪어보면 쉬운데 처음 찾아 볼 때는 다들 쓰고 있는 엔드포인트도 다르고
프론트에서 어떻게 쓰이고 -> 백에서 어떻게 받고 -> 그 반대는 다시 어떻게 하는지에 대한 설명이 없어서 상당히 헤맸었다.
이 정도만 알면 소켓 통신에 대해서는 어떻게 쓰는지 쉽게 이해할 수 있으리라 생각된다.
다음은 Redis 사용에 관해 포스팅해보겠다. 마찬가지로 처음쓰면서 삽질을 많이 했던 파트다.
'IT이야기 > PROJECT' 카테고리의 다른 글
[AWS] S3 + CloundFront를 이용한 React 배포 (3) | 2024.10.21 |
---|---|
[React + SpringBoot] 1대1 랜덤채팅 프로젝트 - Redis를 이용한 매칭 로직 (0) | 2024.03.11 |
[React + SpringBoot] 1대1 랜덤채팅 프로젝트 - Redis를 이용한 매칭 / Redis설정 및 사용방법 (0) | 2024.03.10 |
[React + SpringBoot] 1대1 랜덤채팅 프로젝트 - 화면 및 기능 설계 (실시간 통신, 매칭) (1) | 2024.03.08 |
[React + SpringBoot] 1대1 랜덤채팅 프로젝트 - 환경설정 및 구성 (0) | 2024.03.08 |