728x90
https://asa9874.tistory.com/697
[SpringBoot] React + SpringBoot + JWT + Stomp으로 WebSocket 채팅 구현하기
1.STOMP(Simple Text Oriented Messaging Protocol)텍스트 기반의 메시징 프로토콜으로 웹소켓과 함께 사용되어 클라이언트와 서버 간의 실시간 통신을 간편하게 구현할 수 있게 해준다.STOMP 없이도 WebSocket으
asa9874.tistory.com
해당 내용의 이어서이다.
저번글에서 채팅방에 들어가면 웹소켓으로 서로 연결 되는것을 확인하였다. 하지만 보통 채팅프로그램에서 채팅방에 들어가지않는다고 해도 채팅방 목록에서 새로운 채팅이 업데이트 되는것이 보여야한다!
1.WebSocketConfig configureMessageBroker 수정
기존 /topic를 라우팅 하도록 설정했었다. 이거는 채팅방에 대한 브로드캐스트 용 웹소켓이고,
이번에는 채팅방에 새로운 메시지가 도착했음을 각각 사용자에게 알리기 위한 /queue 를 생성하였다.
config.setUserDestinationPrefix("/user");를 통해서 특정 사용자에게 메시지를 보내는 목적지 접두사를 지정하였다.
@Override
public void configureMessageBroker(@NonNull MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic","/queue");
config.setApplicationDestinationPrefixes("/app");
config.setUserDestinationPrefix("/user");
}
이설정을 기반으로 서버가 특정 유저에게 메시지를 보낼때 해당 경로로 메시지를 보내게 될것이다.
/user/{userId}/queue/messages
2. PrincipalHandshakeHandler 구현
Spring WebSocket에서 WebSocket 연결 시 사용자 정보를 식별하기 위한 커스텀 HandshakeHandler 구현이다.
우리는 이전 글에서JwtHandshakeInterceptor 클래스에서 beforeHandshake 메서드에서 JWT Token에서 여러 정보를 추출하여 attributes에 넣었었다. 이번에는 그중에 userId를 사용할것이다.
//JwtHandshakeInterceptor 코드 일부
String email = jwtTokenProvider.getEmailFromToken(token);
String role = jwtTokenProvider.getRoleFromToken(token);
Long userId = jwtTokenProvider.getIdFromToken(token);
attributes.put("email", email);
attributes.put("role", role);
attributes.put("userId", userId);
return true;
attributes 맵에서 userId 값을 꺼내서 UsernamePasswordAuthenticationToken객체를 반환해준다.
이 객체는 Spring Security에서 사용자 인증을 표현하는 객체다.
UsernamePasswordAuthenticationToken ⊂ Authentication ⊂ Principal
public class PrincipalHandshakeHandler extends DefaultHandshakeHandler {
@Override
protected Principal determineUser(ServerHttpRequest request,
WebSocketHandler wsHandler,
Map<String, Object> attributes) {
String userId = attributes.get("userId").toString();
return new UsernamePasswordAuthenticationToken(userId, null, Collections.emptyList());
}
}
이제 setHandshakeHandler로 지금 만든 PrincipalHandshakeHandler를 넣어준다.
이 과정을 통해 WebSocket 통신 중 Principal로 유저를 식별할 수 있게된다.
@Override
public void registerStompEndpoints(@NonNull StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins("http://localhost:5173", "http://localhost:8080", "http://127.0.0.1:3000")
.addInterceptors(jwtHandshakeInterceptor)
.setHandshakeHandler(new PrincipalHandshakeHandler())
.withSockJS();
}
3.컨트롤러 수정
이제 메시지를 보내면 그 채팅방안에 브로드 캐스트 할뿐만이 아니라 채팅방의 맴버들에게 1:1 알람또한 보내는것을 추가해야한다.
@MessageMapping("/chat/send")
public void sendMessage(MessageCreateRequestDto requestDto, SimpMessageHeaderAccessor headerAccessor) {
Long senderId = (Long) headerAccessor.getSessionAttributes().get("userId");
MessageResponseDto savedMessage = messageService.sendMessage(requestDto, senderId);
messagingTemplate.convertAndSend("/topic/chat/" + requestDto.getChatRoomId(), savedMessage);
MessageSocketResponseDto responseDto = MessageSocketResponseDto.from(
savedMessage, requestDto.getChatRoomId());
// 1:1 알림 메시지도 전송
List<Member> members = chatRoomService.findMembersByChatRoomId(requestDto.getChatRoomId());
for (Member member : members) {
if (!member.getId().equals(senderId)) {
messagingTemplate.convertAndSendToUser(
member.getId().toString(),
"/queue/messages",
responseDto);
log.error(member.getId().toString()+responseDto.getContent());
}
}
}
특정 사용자에게만 WebSocket 메시지를 전송할 때 사용하는 코드이다. convertAndSendToUser를 통해 1:1 메시지를 보내며
messagingTemplate.convertAndSendToUser(
member.getId().toString(), // 1. 수신자 ID
"/queue/messages", // 2. 목적지 주소 (구독 경로)
responseDto // 3. 전송할 메시지 내용
);
실제로 다음 코드는 다음과같은 실제 전송 주소를 가진다. /user/는 앞에서 설정한 setUserDestinationPrefix에서 설정한 주소기반이다.
/user/{userId}/queue/messages
4.리엑트
4-1. 구독
이번 구독의 경우에는 이전 채팅방 구독할때랑은 약간 다르다.
채팅방의 경우 구독 주소에 채팅방 이름을 넣었지만, 지금은 memberId를 JWT에서 꺼내서 BackEnd에서 알아서 처리해줄것이기 때문에, 다음처럼 그냥 /user/queue/messages를 구독해주면 된다.
export function connectMemberWebSocket(onMessage: (message: Message) => void, memberId: string) {
const jwtToken = localStorage.getItem("token");
if (!jwtToken) {
console.error(" JWT 토큰이 없습니다. ");
return;
}
console.log(" 웹소켓 연결 시도 중...(개인) ", memberId);
stompClient = new Client({
brokerURL: undefined,
webSocketFactory: () => new SockJS(`${SOCKET_URL}?token=${encodeURIComponent(jwtToken)}`),
reconnectDelay: 5000,
onConnect: () => {
console.log(" 웹소켓 연결됨(개인)");
if (!stompClient) {
console.error(" STOMP 클라이언트가 초기화되지 않았습니다.(개인)");
return;
}
stompClient.subscribe('/user/queue/messages', onMessage);
console.log(`/user/queue/messages`)
},
onStompError: (frame) => {
console.error(" STOMP 에러(개인)", frame);
},
});
stompClient.activate();
}
4-2.채팅방목록페이지
이제 채팅방 목록이 있는 페이지에 이 웹소켓을 연결해주는 useEffect를 만들고,
메시지를 받을때마다 해당 채팅방의 정보를 수정하도록 바꿨다.
(아래에서 불필요하게 const로 각 변수들 선언했는데 안그래도 됨)
useEffect(() => {
if (!id) return;
connectMemberWebSocket((messageFrame) => {
const receivedMessage = JSON.parse(messageFrame.body) as MessageSocket;
const changedChatRoomId = receivedMessage.chatRoomId;
const changedLastMessage = receivedMessage.content;
const changedSenderName = receivedMessage.senderName;
const changedDate = receivedMessage.timestamp;
console.log(changedLastMessage)
console.log(changedChatRoomId)
setChatRoomList(prevChatRooms => {
return prevChatRooms.map(chatRoom => {
if (chatRoom.id === Number(changedChatRoomId)) {
return {
...chatRoom,
lastMessage: changedLastMessage,
lastSenderName: changedSenderName,
lastDate: changedDate,
};
}
return chatRoom;
});
});
}, String(id));
}, [id]);
728x90
'BackEnd > SpringBoot' 카테고리의 다른 글
[SpringBoot] websocket 채팅방 이미지 업로드하기 (0) | 2025.05.31 |
---|---|
[SpringBoot] ORM, JPA, 영속성에 대해 (1) | 2025.05.27 |
[SpringBoot] React + SpringBoot + JWT + Stomp으로 WebSocket 채팅 구현하기 (1) | 2025.05.26 |