고래씌
[SpringBoot] 2-4. React를 스프링부트에 연동(메세지 추가) 본문
1. 메세지 추가
① 웹소켓 작업을 위해 새터미널을 열고 다음과 같이 설치 진행
② StompConfig.java 클래스 파일 생성
※ Stomp
- MessageBroker방식 처리
- publih 발행 / subscribe 구독 패턴
- 특정 url을 '구독'하는 사용자들에게 메세지를 '발행'해줌
shift+alt+ S 단축키를 이용해 Override/Implements Method...클릭후 아래 메소드 추가
▶ StompConfig.java
package com.kh.api.model.websocket.config;
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;
/*
* Stomp
* - MessageBroker방식 처리
* - publih 발행 / subscribe 구독 패턴
* - 특정 url을 '구독'하는 사용자들에게 메세지를 '발행'해줌
*
*/
@Configuration
@EnableWebSocketMessageBroker // 메시지를 대신 전달해주는 중개인
public class StompConfig implements WebSocketMessageBrokerConfigurer {
/*
* 클라이언트에서 웹소켓 서버로 연결요청을 보낼 endPoint설정
*
* http://localhost:8084/stompServer로 요청시 웹소켓 객체 생성
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/stompServer")
.setAllowedOrigins("http://localhost:3000") // 프록시 설정을 위해서 설정
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// /chat으로 시작하는 url이 발행가능하도록 설정
registry.enableSimpleBroker("/chat");
// 접두어 자동 추가 기능
registry.setApplicationDestinationPrefixes("/chat"); // 구독 url에 /chat을 자동으로 붙임
}
}
③ SompContrller.java 클래스 파일 생성
▶ StompController.java
package com.kh.api.model.websocket.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import com.kh.api.model.service.ChatService;
import com.kh.api.model.vo.ChatMessage;
@Controller
public class StompController {
@Autowired
private ChatService service;
private final Logger logger = LoggerFactory.getLogger(StompController.class);
// 채팅방에 메세지 추가기능
// http:~~/api/chat/sendMessage/chatRoomNo/{chatRoomNo}
@MessageMapping("/sendMessage/chatRoomNo/{chatRoomNo}") // 코드를 간결하게 쓸수있음
@SendTo("/chat/chatRoomNo/{chatRoomNo}/message")
public ChatMessage insertChatMessage(ChatMessage chatMessage) {
// 1) db에 등록후
logger.info("{chatMessage ?? {}", chatMessage);
service.insertChatMessage(chatMessage);
// 2) 같은방 사용자에게 다시 전달
return chatMessage;
}
}
▶ ChattingRoom.js
- 웹소켓 연결을 위해 useState 생성
- useEffect(() => {} 함수에 웹소켓을 연결할 수 있는 함수와 웹소켓 객체 생성
- 객체와 연결
=> stomp 같은 경우 웹서버와 통신을 할 때 frame단위로 동작을 함. frame를 메세지를 교환하는 틀이라고 생각할 것
=> 구독 url 설정
=> 내가 구독한 url로 데이터가 전달이 되면 실시간으로 감지하고 콜백함수를 실행함
useEffect(() => {
// 웹소켓 연결할 수 있는 함수
// const createWebsocket = () => new SockJs("http://localhost:3000/api/stompServer");
const createWebsocket = () => new SockJs("http://localhost:8084/api/stompServer"); // 프록시 설정
// 웹소켓 객체 생성은 stomp가 주관할 예정
const stompClient = Stomp.over(createWebsocket);
stompClient.connect({}, (frame) => {
console.log(frame);
// stomp 같은 경우 웹서버와 통신을 할 때 frame단위로 동작을 함. frame를 메세지를 교환하는 틀이라고 생각할 것
// 구독 url 설정
// 내가 구독한 url로 데이터가 전달이 되면 실시간으로 감지하고 콜백함수를 실행함
stompClient.subscribe(`/chat/chatRoomNo/${chatRoomNo}/message`, async(frame) => {
console.log(frame.body); // JSON형태의 데이터. 가공처리는 직접해줘야함
let jsonMessage = frame.body;
let parsedMessage = await JSON.parse(jsonMessage);
// 의존성배열이 비어있는 useEffect내부에서 state값은 항상 "초기 랜더링시의 값"만을 가지고있다.
setChatMessages((preState) => [...preState, parsedMessage]) // 깊은복사
})
}); // 2번째 매개변수 : 연결이 완료되었을때 실행시킬 콜백함수 지정
setWebSocket(stompClient); // 웹소켓은 stompClient로 작업할 수 있도록 지정
// 1) CHAT_ROOM_JOIN 테이블에 참여자정보 추가
let chatRoomJoin = {
userNo : user.userNo,
chatRoomNo,
userStatus : 1
}
axios
.post("/api/joinChatRoom", chatRoomJoin)
.then((res) => {
console.log('참여완료');
})
// 2) 채팅방 메세지 목록 가져오기
axios
.get("/api/chatMessage/chatRoomNo/" + chatRoomNo)
.then((list) => { // 만약 정상적으로 가져왔다면 배열형태의 메시지 가져옴
setChatMessages(list.data);
}).catch(err => console.log(err))
// 3) 채팅방 참여자 정보 조회
return () => {
// 컴포넌트 소멸시 스톰프 클라이언트 연결해제
stompClient.disconnect();
}
},[])
- 엔터를 치면은 <li></li>태그가 있는 곳까지 스크롤이 자동으로 내려가도록 설정
// 스크롤 자동으로 아래로 내리기
const bottomRef = useRef(null);
// 메시지 전송할 때마다 개행처리되는 것 방지
const textareaRef = useRef(null);
|
- textarea에 setMessages 함수 추가
- 전송버튼에 onClick 이벤트핸들러 추가
- enter키 눌렀을 때 이벤트 핸들러 발생되도록 onKeyDown 이벤트 핸들러 추가
- 스크롤이 자동으로 <li></li>있는 태그로 이동되도록 bottomRef 추가
- 개행처리 방지하도록 ref={textareaRef}추가
<div className="chatting-area">
<div className="chat-header">
<button className="btn btn-outline-danger">나가기</button>
</div>
<ul className="display-chatting">
<Message chatMessages={chatMessages} />
<li ref={bottomRef}></li> {/* li태그 주소값을 계속 가져오기 위한 변수 */}
</ul>
<div className="input-area">
<textarea rows="3" name="message" ref={textareaRef}
onKeyDown={submitMessage}
onChange={(e) => {
setMessages(e.target.value)
}}
value={message}></textarea>
<button onClick={sendMessage}>전송</button>
</div>
</div>
- scrollToBottom 함수
const scrollToBottom = () => {
if(bottomRef.current){
bottomRef.current.scrollIntoView({behavior: 'smooth'});
}
}
=> useEffect 안에도 스크롤이 자동으로 내려가도록 추가
- sendMessage 함수
const sendMessage = () => {
const chatMessage = {
message,
chatRoomNo,
userNo : user.userNo
}
if(!message){ // 메세지가 아무것도 입력하지 않았을 때
alert("뭐든 입력하세요");
return;
}
if(!user){
alert("로그인 좀 하세요");
return;
}
if(!webSocket){
alert("웹소켓 연결중입니다");
return;
}
// 웹소켓 서버로 데이터 전송
webSocket.send("/chat/sendMessage/chatRoomNo/" + chatRoomNo, {}, JSON.stringify(chatMessage));
// 다른 서버와 주고받을 때 반드시 JSON 문자열형태로 전달해야한다!
}
- subitMessage 함수 (enter키 눌렀을 때 메시지 전송가능하도록)
// enter키 누를때
const submitMessage = (e) => {
if(e.key=== 'Enter' && !e.shiftKey){
sendMessage();
// 비동기 함수와 동일하게 작동시키기 위해 지연시간을 추가
setTimeout(() => {
textareaRef.current.value="";
})
setTimeout(() => {
// 스크롤 내림
scrollToBottom();
}, 100);
}
}
☞ ChattingRoom.js 전체코드
import { useEffect } from "react";
import axios from 'axios';
import { useSelector } from "react-redux";
import { useParams } from "react-router-dom";
import Message from "../components/Messages";
import { useState } from "react";
import SockJs from 'sockjs-client';
import {Stomp} from '@stomp/stompjs';
import { useRef } from "react";
export default function ChattingRoom (){
// userNo를 가져와야하는데 redux에서 꺼내오는 방법이 있음. useSelector를 이용하여 꺼내옴
let user = useSelector((state) => state.user) // store에서 키값만 제시해서 꺼내오면 됨
// 채팅방번호는 파라미터 값을 이용하여 가져올 예정
const {chatRoomNo} = useParams(); // chatRoomNo로 넘어온 데이터를 변수로 꺼내어 사용
let [chatMessages, setChatMessages] = useState([]);
const [webSocket, setWebSocket] = useState(null);
const [message, setMessages] = useState('');
// 스크롤 자동으로 아래로 내리기
const bottomRef = useRef(null);
// 메시지 전송할 때마다 개행처리되는 것 방지
const textareaRef = useRef(null);
useEffect(() => {
// 웹소켓 연결할 수 있는 함수
// const createWebsocket = () => new SockJs("http://localhost:3000/api/stompServer");
const createWebsocket = () => new SockJs("http://localhost:8084/api/stompServer"); // 프록시 설정
// 웹소켓 객체 생성은 stomp가 주관할 예정
const stompClient = Stomp.over(createWebsocket);
stompClient.connect({}, (frame) => {
console.log(frame);
// stomp 같은 경우 웹서버와 통신을 할 때 frame단위로 동작을 함. frame를 메세지를 교환하는 틀이라고 생각할 것
// 구독 url 설정
// 내가 구독한 url로 데이터가 전달이 되면 실시간으로 감지하고 콜백함수를 실행함
stompClient.subscribe(`/chat/chatRoomNo/${chatRoomNo}/message`, async(frame) => {
console.log(frame.body); // JSON형태의 데이터. 가공처리는 직접해줘야함
let jsonMessage = frame.body;
let parsedMessage = await JSON.parse(jsonMessage);
// 의존성배열이 비어있는 useEffect내부에서 state값은 항상 "초기 랜더링시의 값"만을 가지고있다.
setChatMessages((preState) => [...preState, parsedMessage]) // 깊은복사
})
}); // 2번째 매개변수 : 연결이 완료되었을때 실행시킬 콜백함수 지정
setWebSocket(stompClient); // 웹소켓은 stompClient로 작업할 수 있도록 지정
// 1) CHAT_ROOM_JOIN 테이블에 참여자정보 추가
let chatRoomJoin = {
userNo : user.userNo,
chatRoomNo,
userStatus : 1
}
axios
.post("/api/joinChatRoom", chatRoomJoin)
.then((res) => {
console.log('참여완료');
})
// 2) 채팅방 메세지 목록 가져오기
axios
.get("/api/chatMessage/chatRoomNo/" + chatRoomNo)
.then((list) => { // 만약 정상적으로 가져왔다면 배열형태의 메시지 가져옴
setChatMessages(list.data);
}).catch(err => console.log(err))
setTimeout(() => {
scrollToBottom();
}, 100)
// 3) 채팅방 참여자 정보 조회
return () => {
// 컴포넌트 소멸시 스톰프 클라이언트 연결해제
stompClient.disconnect();
}
},[])
const sendMessage = () => {
const chatMessage = {
message,
chatRoomNo,
userNo : user.userNo
}
if(!message){ // 메세지가 아무것도 입력하지 않았을 때
alert("뭐든 입력하세요");
return;
}
if(!user){
alert("로그인 좀 하세요");
return;
}
if(!webSocket){
alert("웹소켓 연결중입니다");
return;
}
// 웹소켓 서버로 데이터 전송
webSocket.send("/chat/sendMessage/chatRoomNo/" + chatRoomNo, {}, JSON.stringify(chatMessage));
// 다른 서버와 주고받을 때 반드시 JSON 문자열형태로 전달해야한다!
}
const scrollToBottom = () => {
if(bottomRef.current){
bottomRef.current.scrollIntoView({behavior: 'smooth'});
}
}
// enter키 누를때
const submitMessage = (e) => {
if(e.key=== 'Enter' && !e.shiftKey){
sendMessage();
// 비동기 함수와 동일하게 작동시키기 위해 지연시간을 추가
setTimeout(() => {
textareaRef.current.value="";
})
setTimeout(() => {
// 스크롤 내림
scrollToBottom();
}, 100);
}
}
return (
<>
{/* 채팅방 참여자 목록을 보여주는 부분 */}
<div className="chat-room-Members">
<h4>참여자 목록</h4>
<ul className="chat-room-members-ul">
<li>
{/* 사용자의 접속상태를 표시. 웹소켓과 연동해서 실시간으로 바뀌게 할 예정 */}
<span className="user-status online"></span>루피1
</li>
<li>
<span className="user-status offline"></span>루피2
</li>
</ul>
</div>
<div className="chatting-area">
<div className="chat-header">
<button className="btn btn-outline-danger">나가기</button>
</div>
<ul className="display-chatting">
<Message chatMessages={chatMessages} />
<li ref={bottomRef}></li> {/* li태그 주소값을 계속 가져오기 위한 변수 */}
</ul>
<div className="input-area">
<textarea rows="3" name="message" ref={textareaRef}
onKeyDown={submitMessage}
onChange={(e) => {
setMessages(e.target.value)
}}
value={message}></textarea>
<button onClick={sendMessage}>전송</button>
</div>
</div>
</>
)
}
▶ ChatService.java
// 메시지 추가
public void insertChatMessage(ChatMessage chatMessage) {
dao.insertChatMessage(chatMessage);
}
▶ ChatDao.java
// 메세지 추가
public void insertChatMessage(ChatMessage chatMessage) {
session.insert("chatMapper.insertChatMessage", chatMessage);
}
▶ chatting-mapper.xml
<!-- 채팅 메세지 추가 -->
<insert id="insertChatMessage">
INSERT INTO CHAT_MESSAGE
VALUES (
SEQ_CM_NO.NEXTVAL,
#{message},
SYSDATE,
#{chatRoomNo},
#{userNo}
)
</insert>
▶ 결과
=> 메세지가 계속 추가되는 것 확인
'Server > SpringBoot' 카테고리의 다른 글
[SpringBoot] 2-3. React 스프링부트에 연동(채팅방 상세보기, 메시지) (0) | 2024.02.06 |
---|---|
[SpringBoot] 2-2. React에 스프링부트 연동(채팅방 생성) (0) | 2024.02.05 |
[SpringBoot] 2-1. React에 스프링부트 연동(게시판, 채팅방 목록) (0) | 2024.02.05 |
[SpringBoot] 1-5. 메뉴 삭제 - DELETE (0) | 2024.02.02 |
[SpringBoot] 1-4. 메뉴 검색, 메뉴 수정 - PUT 방식 (0) | 2024.02.02 |