- 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
가 자동으로 생성됨) - 저장된 메시지를 프론트엔드로 전달
- 메시지 동기화
messageId
의 역할
2.2 프론트엔드에서 프론트엔드에서는 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()
)
})
messageId
를 누락한 응답 데이터
2.4 백엔드에서 백엔드에서는 메시지 저장 후 응답 데이터에 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
를 포함하여 동기화 문제 해결 - 메시지 동기화가 정상적으로 이루어지고 새로고침 없이도 실시간으로 메시지를 확인 가능
이로써 메시지 동기화 문제를 성공적으로 해결했습니다!