- Published on
웹소켓과 STOMP를 이용한 채팅 구현 중 메시지 동기화 문제 해결
- Authors

- Name
- Hyo814
1. 문제 상황
웹소켓(WebSocket)과 STOMP로 채팅 기능을 구현하다가 다음 문제가 생겼습니다.
- 처음 메시지를 전송할 때는 동기화가 잘 됨
- 두 번째 메시지 전송 이후에는 동기화가 이루어지지 않음
- 새로고침을 하면 전송된 메시지는 잘 가져옴 (HTTP GET 요청으로 확인 가능)
메시지가 정상적으로 전송되고 데이터베이스에도 저장되지만, 자동 동기화가 실패하는 상황이었습니다.
2. 문제 원인 분석
2.1 메시지 ID 존재 여부
메시지 ID는 각 메시지를 고유하게 식별하는 값으로, MongoDB에서 메시지가 저장될 때 자동으로 생성됩니다. 문제는 이 메시지 ID가 프론트엔드에서 제대로 활용되지 못해 동기화 문제로 이어졌다는 점입니다.
채팅 실행 순서
- HTTP GET 요청으로 기존 대화 목록 조회
- WebSocket STOMP 연결 설정
- 메시지 전송
- 백엔드로 메시지 전달
- 메시지가 MongoDB에 저장됨 (이때 MongoDB에서
messageId가 자동으로 생성됨) - 저장된 메시지를 프론트엔드로 전달
- 메시지 동기화
2.2 프론트엔드에서 messageId의 역할
프론트엔드에서는 React의 key 속성에 messageId를 넣어 각 메시지를 고유하게 식별합니다. 예시는 아래와 같습니다.
<div key={msg.messageId} className={styles.userMessages}>
{showDate && (
<div className={styles.dateSeparator}>
<hr className={styles.dateHr} />
<span className={styles.dateText}>{formatDate(msg.timestamp)}</span>
<hr className={styles.dateHr} />
</div>
)}
{msg.nickname !== userNickname && (
<div className={styles.userInfo}>
<Image
src={msg.profileUrl}
alt={`${msg.nickname} 프로필`}
width={40}
height={40}
className={styles.profileImage}
/>
<span className={styles.nickname}>{msg.nickname}</span>
</div>
)}
<div className={msg.nickname === userNickname ? styles.receiveMessage : styles.sentMessage}>
<span>{msg.message}</span>
<span className={styles.timestamp}>{formatTimestamp(msg.timestamp)}</span>
</div>
</div>
React는 key로 컴포넌트를 추적해 업데이트 시 변경된 항목만 다시 렌더링합니다. 하지만 messageId 값이 없으면 React가 메시지를 고유하게 식별하지 못해 동기화도 제대로 되지 않습니다.
2.3 초기 난수 설정 및 정렬 부족
초기에는 messageId가 없는 상태에서 프론트엔드가 난수를 임의로 생성해 채워넣었지만, 이를 기준으로 정렬하지 않다 보니 메시지가 뒤섞이고 동기화도 실패했습니다. 정렬 로직은 다음과 같습니다.
setMessages((prev) => {
const combinedMessages = [...prev, newMessage]
return combinedMessages.sort(
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
)
})
2.4 백엔드에서 messageId를 누락한 응답 데이터
백엔드에서는 메시지 저장 후 응답 데이터에 messageId를 담지 않았습니다.
public ChatResponse sendChatMessage(ChatMessageRequest chatMessageRequest) {
validateSchedule(chatMessageRequest.getScheduleId());
Member member = findChatMemberByNickname(chatMessageRequest.getNickname());
TravelAttendee attendee = findTravelAttendee(chatMessageRequest.getScheduleId(), member.getUserId());
if (!attendee.getPermission().isEnableChat()) {
throw new ForbiddenChatException(ErrorCode.FORBIDDEN_CHAT_ATTENDEE);
}
ChatMessage message = ChatMessage.of(member, chatMessageRequest);
chatMessageRepository.save(message);
return ChatResponse.from(member, message); // 여기서 messageId가 누락됨
}
chatMessageRepository.save(message)로 MongoDB에 저장은 끝나지만, 저장 후 생성된 messageId 값을 응답에 담지 않아 프론트엔드가 고유 식별자를 쓸 수 없었습니다.
3. 해결 방법
백엔드에서 messageId 값을 반환하도록 수정
백엔드 로직을 아래와 같이 고쳐 저장된 메시지의 messageId 값을 응답에 담았습니다.
public ChatResponse sendChatMessage(ChatMessageRequest chatMessageRequest) {
validateSchedule(chatMessageRequest.getScheduleId());
Member member = findChatMemberByNickname(chatMessageRequest.getNickname());
TravelAttendee attendee = findTravelAttendee(chatMessageRequest.getScheduleId(), member.getUserId());
if (!attendee.getPermission().isEnableChat()) {
throw new ForbiddenChatException(ErrorCode.FORBIDDEN_CHAT_ATTENDEE);
}
ChatMessage message = chatMessageRepository.save(ChatMessage.of(member, chatMessageRequest));
return ChatResponse.from(member, message); // 저장된 messageId 포함
}
chatMessageRepository.save()가 반환하는 message 객체에는 MongoDB에서 생성한 messageId가 들어있고, 이를 ChatResponse에 담아 프론트엔드로 보냅니다.
프론트엔드에서 messageId 활용
수정된 백엔드 응답 데이터로 메시지를 고유하게 식별하고 정렬해 동기화 문제를 풀었습니다.
setMessages((prev) => {
const combinedMessages = [...prev, newMessage]
return combinedMessages.sort(
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
)
})
4. 결과
messageId로 React에서 각 메시지를 고유하게 식별- 백엔드 응답 데이터에
messageId를 담아 동기화 문제 해결 - 메시지 동기화가 정상적으로 이루어져 새로고침 없이도 실시간으로 메시지가 보임
이렇게 메시지 동기화 문제를 해결했습니다.