프로젝트(개인)

[Spring , react] 실시간 채팅 구현하기

그zi운아이 2024. 3. 19. 18:04

웹 기반 실시간 채팅의 핵심 기술 소개

WebSocket: 실시간 양방향 통신의 기반

  • 정의: 웹 애플리케이션과 서버 간에 실시간, 양방향, 풀 듀플렉스 통신을 가능하게 하는 고급 통신 프로토콜입니다.
  • 장점: 지속적인 연결을 통해 실시간 데이터 교환이 가능, HTTP 폴링에 비해 훨씬 효율적인 네트워크 자원 사용.
  • 사용 예: 실시간 게임, 채팅 애플리케이션, 금융 시장 데이터 스트리밍.

STOMP (Simple Text Oriented Messaging Protocol): 메시지 교환 프로토콜

  • 정의: 간단한 텍스트 기반의 메시징 프로토콜로, 웹소켓 위에서 더 고급 메시징 기능을 제공합니다.
  • 특징: 헤더와 바디를 포함하는 단순한 텍스트 메시지 형식을 사용. 구독/발행(pub-sub) 모델을 통해 특정 주제에 대한 메시지를 구독하고, 서버로부터 메시지를 받거나 전송할 수 있습니다.
  • 적용: 복잡한 메시징 요구사항을 가진 애플리케이션, 예를 들어, 대규모 채팅 시스템, 브로커를 통한 메시지 분배가 필요한 경우.

SockJS: 광범위한 호환성을 위한 라이브러리

  • 정의: 웹소켓을 지원하지 않는 브라우저에서도 웹소켓과 유사한 기능을 에뮬레이트하여, 개발자가 일관된 프로그래밍 모델을 사용할 수 있게 해주는 JavaScript 라이브러리입니다.
  • 장점: 폴백 옵션을 제공하여, 웹소켓이 불가능한 환경에서도 연결을 유지할 수 있습니다. 이를 통해 거의 모든 브라우저에서 실시간 통신 기능을 사용할 수 있게 됩니다.
  • 적용 사례: 다양한 브라우저 환경에서 안정적인 실시간 애플리케이션을 구현하고자 할 때.

구현 단계 및 방법

백엔드 구현: 실시간 메시지 처리

의존성 주입

implementation 'org.springframework.boot:spring-boot-starter-security'

 

웹소켓 설정 (WebSocketConfig 클래스)

package com.example.fitconnect.config.webSocket;

import lombok.extern.slf4j.Slf4j;
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
@Slf4j
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws/{chatRoomId}").setAllowedOrigins("도메인주소")
                .withSockJS()
                .setInterceptors(new CustomHandshakeInterceptor());;
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        log.info("Configuring message broker {}", registry);
        registry.enableSimpleBroker("/topic");
        registry.setApplicationDestinationPrefixes("/app");
    }
}

웹소켓 설정 (WebSocketConfig 클래스)

  • 엔드포인트 등록: 클라이언트가 서버와 웹소켓 연결을 시작할 수 있는 엔드포인트(/ws/{chatRoomId})를 정의합니다. 
  • CORS(Cross-Origin Resource Sharing) 설정: setAllowedOrigins("")를 사용하여 특정 출처에서의 웹소켓 연결을 허용합니다. 이는 보안을 강화하며, 지정된 출처에서만 리소스 접근이 가능하게 합니다.
  • SockJS 사용: 웹소켓을 지원하지 않는 브라우저에 대한 호환성을 제공합니다.
  • 메시지 브로커 구성: 클라이언트와 서버 간 메시지 교환을 위한 설정을 포함합니다. /app로 시작하는 목적지 주소를 가진 메시지는 애플리케이션으로 라우팅되고, /topic으로 시작하는 주소를 구독하는 클라이언트에게 메시지가 발행됩니다.

도메인 모델 (ChatMessage 및 ChatRoom 클래스)

@Entity
@Getter
@Table(name = "chat_messages")
public class ChatMessage extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 1000)
    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    private ChatRoom chatRoom;

    @ManyToOne(fetch = FetchType.LAZY)
    private User sender;

    public ChatMessage() {
    }

    public ChatMessage(String content,ChatRoom chatRoom,User user) {
        this.content = content;
        this.chatRoom = chatRoom;
        this.sender = user;
    }
@Entity
@Getter
@Table(name = "chat_rooms")
public class ChatRoom extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    @Size(min = 1 ,max = 100)
    private String title;
    @ManyToOne(fetch = FetchType.LAZY)
    private ExerciseEvent exerciseEvent;

    @ManyToOne(fetch = FetchType.LAZY)
    private User creator;

    @ManyToOne(fetch = FetchType.LAZY)
    private User participant;

    public ChatRoom(String title,ExerciseEvent exerciseEvent,User creator,User participant) {
        this.title = title;
        this.exerciseEvent = exerciseEvent;
        this.creator = creator;
        this.participant = participant;
    }
  • ChatMessage: 채팅 메시지의 내용, 송신자, 속한 채팅방 등을 정의합니다. 메시지 수정 및 삭제 권한 검증을 포함합니다.
  • ChatRoom: 채팅방의 제목, 참여자, 생성자 등을 정의합니다. 채팅방 수정 권한 검증을 포함합니다.

ChatController

@Controller
@RequiredArgsConstructor
@Slf4j
public class ChatController {

    private final ChatMessageCreationService chatMessageCreationService;

    @MessageMapping("/chat/{chatRoomId}/sendMessage")
    @SendTo("/topic/{chatRoomId}")
    public ResponseEntity<ChatMessageResponseDto> sendMessage(
            SimpMessageHeaderAccessor headerAccessor,
            ChatMessageRegistrationDto dto, @DestinationVariable Long chatRoomId) {

        Long userId = (Long) headerAccessor.getSessionAttributes().get("userId");
        ChatMessageResponseDto chatMessageResponseDto = chatMessageCreationService.createChatMessage(
                dto.getContent(), chatRoomId, userId);

        return ResponseEntity.ok().body(chatMessageResponseDto);
    }
}
  • ChatController에서 /chat/{chatRoomId}/sendMessage 엔드포인트를 통해 메시지 송신 요청을 처리할 때, ChatMessageCreationService를 사용하여 실제 메시지 처리 로직을 수행합니다. 이 과정에서 메시지는 생성, 저장되며 추후에 대화 내용을 확인할 수 있습니다.

메시지 저장 로직 (ChatMessageCreationService 클래스)

 

@Service
@RequiredArgsConstructor
public class ChatMessageCreationService {

    private final ChatMessageRepository chatMessageRepository;

    private final ChatRoomFindService chatRoomFindService;

    private final UserFindService userFindService;

    @Transactional
    public ChatMessageResponseDto createChatMessage(String message, Long chatRoomId, Long userId) {
        ChatRoom chatRoom = findChatRoom(message, chatRoomId);
        User sender = findUser(userId);

        ChatMessage chatMessage = new ChatMessage(message, chatRoom, sender);
        ChatMessage saveChatMessage = chatMessageRepository.save(chatMessage);
        return ChatMessageResponseDto.toDto(saveChatMessage);

    }

 

  • 메시지 생성 및 저장: 사용자가 보낸 메시지(message), 채팅방 ID(chatRoomId), 사용자 ID(userId)를 기반으로 새로운 ChatMessage 인스턴스를 생성하고, 이를 데이터베이스에 저장합니다.

프론트 구현: 실시간 메시지 처리

의존성 주입

    "sockjs-client": "^1.6.1",
    "stompjs": "^2.3.3",

 

웹소켓 연결과 메시지 구독

const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket);
stompClient.connect({}, () => {
  setClient(stompClient);
  stompClient.subscribe(`/topic/${chatRoomId}`, (msg) => {
    const newMessage = JSON.parse(msg.body);
    setMessages(prevMessages => [...prevMessages, newMessage.body]);
  });
});
  • SockJS와 Stomp를 사용하여 웹소켓 연결을 설정하고, 특정 채팅방의 메시지를 구독

메시지 전송 로직

if (message.trim() && client) {
  await client.send(`/app/chat/${chatRoomId}/sendMessage`, {}, JSON.stringify({ content: message.trim() }));
  setMessage('');
}
  • 사용자가 메시지를 전송하는 이벤트 핸들러