현재 우리 프로젝트가 어떻게 웹소캣을 사용하고 있고 핵심 로직이 어떻게 구현되어 있는지 파트별로 분석해보겠습니다!
사용자가 입력을 하고 어떻게 실시간 소통이 되는 지 흐름대로 가보겠습니다.
클라이언트 : 구독 후 메시지 입력
↓
Vue Store : sendMessage() 호출
↓
STOMP : /app/chat.sendMessage/{gameId}로 전송
↓
백엔드 : ChatWebSocketController.sendMessage() 처리
↓
서비스 : ChatMessageService.createMessage() → DB 저장
↓
응답 : ChatMessageResponseDto 반환
↓
STOMP : @SendTo("/topic/game/{gameId}")로 브로드캐스트 (
↓
모든 구독자 : 메시지 수신 및 화면 업데이트
// 사용자가 게임 상세 페이지로 이동
router.push(`/games/123`) // 123번 게임 채팅방 입장
// GameDetail.vue에서
onMounted(async () => {
// 1. 게임 정보 조회
await gameStore.fetchGameDetail(gameId.value)
// 2. 채팅 연결 시작! ← 여기서 구독 과정 시작
await chatStore.connectToGame(gameId.value, game.value)
})
// chatStore.connectToGame()에서
async connectToGame(gameId, gameData) {
this.currentGameId = gameId
this.currentGame = gameData
try {
// A) 기존 채팅 기록 먼저 로드
await this.loadChatHistory(this.currentRoomId)
// B) 웹소켓 연결 + 구독! ← 핵심!
await this.connectStomp()
} catch (error) {
console.error('❌ 게임 연결 실패:', error)
}
}
// chatStore.connectStomp()에서
async connectStomp() {
// 1) SockJS 웹소켓 연결
const socket = new SockJS('http://localhost:8080/chat-socket')
this.stompClient = new Client({ webSocketFactory: () => socket })
// 2) 연결 성공하면 구독 시작!
this.stompClient.onConnect = (frame) => {
console.log('✅ STOMP 연결 성공!')
this.connected = true
// 🔔 구독 신청! ← 이게 핵심!
this.stompClient.subscribe(`/topic/game/${this.currentGameId}`, (message) => {
console.log('📨 새 메시지 수신:', message.body)
const newMessage = JSON.parse(message.body)
this.addMessage(newMessage) // 받은 메시지를 화면에 추가
})
console.log(`📡 구독 완료: /topic/game/${this.currentGameId}`)
}
// 3) 연결 시작
this.stompClient.activate()
}
웹소켓에서는 구독이라는 용어가 있다
/topic/game/123 123번 게임 채팅을 듣겠다고 신청 (구독)
누군가 123번 게임에 채팅하면 -> 구독한 모든 사람에게 전달
// 웹소켓 구독과 별개로 HTTP API로 기존 메시지 가져오기
async loadChatHistory(roomId) {
const response = await http.get(`/api/chats/rooms/${roomId}`)
const messages = response.data || []
// 기존 메시지들을 화면에 표시
messages.forEach(apiMessage => {
const message = this.formatMessage(apiMessage)
if (message.team === 'home') {
this.homeMessages.push(message)
} else {
this.awayMessages.push(message)
}
})
}
// 사용자가 메시지 입력하고 전송 버튼 클릭
async sendMessage(content, team) {
// 구독이 완료된 상태에서만 전송 가능
if (!this.stompClient || !this.connected) {
console.error('아직 연결되지 않았습니다!')
return
}
// 메시지 전송
this.stompClient.publish({
destination: `/app/chat.sendMessage/${this.currentGameId}`,
body: JSON.stringify({
teamId: team === 'home' ? 1 : 2,
content: content.trim(),
type: 'TEXT'
})
})
}
@MessageMapping("/chat.sendMessage/{gameId}")
@SendTo("/topic/game/{gameId}") // ← 구독자들에게 전송!
public ChatMessageResponseDto sendMessage(
@DestinationVariable Long gameId,
@Payload ChatMessageRequestDto messageRequest
) {
// DB에 저장
Long roomId = chatRoomService.getRoomIdByGameId(gameId);
ChatMessageResponseDto response = chatMessageService.createMessage(roomId, messageRequest);
// 이 리턴값이 /topic/game/{gameId}를 구독한 모든 클라이언트에게 전달됨!
return response;
}
// 위에서 구독한 콜백 함수가 실행됨
this.stompClient.subscribe(`/topic/game/${this.currentGameId}`, (message) => {
console.log('📨 새 메시지 수신:', message.body)
const newMessage = JSON.parse(message.body)
this.addMessage(newMessage) // 화면에 새 메시지 추가!
})