개인 메시징 프로젝트 → 실제 React 채팅 클라이언트

Realtime Chat

연결 성공이 아니라, 메시지가 저장되고 끊긴 뒤 다시 이어지는 순간까지 채팅 제품으로 만들었습니다.

Realtime Chat React 클라이언트에서 Alice와 Bob이 저장 완료 상태를 확인하며 대화하는 화면

구독 인가와 Kafka ordering을 검증한 메시징 실험에서 출발했지만 실제 사용자는 연결보다 메시지가 사라지지 않는지를 경험합니다.

Setting
개인 메시징 프로젝트 → 실제 React 채팅 클라이언트
Audience
두 브라우저에서 전송·수신·재연결을 확인하려는 포트폴리오 방문자
Role
메시지 영속화 경계 재설계, WebSocket 클라이언트, 재연결 E2E
  • 메시지를 SENDING·ACCEPTED·PERSISTED·FAILED 상태로 구분해 낙관적 UI와 서버 결과를 합쳤습니다.
  • DB commit 전에 상대에게 보일 수 있던 독립 broadcast 경로를 제거했습니다.
  • 오프라인 동안 빠진 메시지를 마지막 DB ID 이후부터 다시 가져오고 중복 없이 합쳤습니다.
  1. 01

    닉네임으로 상대를 찾아 1:1 대화방을 엽니다.

  2. 02

    메시지는 SENDING에서 ACCEPTED, DB 저장이 끝나면 PERSISTED로 바뀝니다.

  3. 03

    상대가 오프라인이었다면 재연결 뒤 마지막 persisted ID 이후 메시지를 보충합니다.

저장된 메시지만 상대에게 전달하는 구조실시간 fan-out과 reconnect sync가 같은 persisted message ID를 기준으로 만납니다.SENDroom keyconsumecommitbroadcastreconnect syncSender clientoptimistic stateSTOMP gatewayauth + membershipKafkaroom partitionPersistencecommit then publishDatabasemessage IDReceiver clientlive + sync
  1. STOMP SEND를 roomId key로 Kafka에 기록합니다.
  2. Persistence transaction이 DB message ID를 만든 뒤 commit합니다.
  3. commit된 payload만 상대와 발신자에게 broadcast합니다.
  4. 재연결 client는 마지막 DB ID 이후를 sync합니다.
실시간 fan-out과 reconnect sync가 같은 persisted message ID를 기준으로 만납니다.
저장된 메시지만 상대에게 전달하는 구조
저장된 메시지만 상대에게 전달하는 구조실시간 fan-out과 reconnect sync가 같은 persisted message ID를 기준으로 만납니다.SENDroom keyconsumecommitbroadcastreconnect syncSender clientoptimistic stateSTOMP gatewayauth + membershipKafkaroom partitionPersistencecommit then publishDatabasemessage IDReceiver clientlive + sync
실제 화면에서 상대가 본 메시지가 DB에 없을 수 있다는 사실이 드러나면서 전달 순서를 다시 설계했습니다.
Lab

구독 권한과 room ordering을 나누어 검증하기

handshake 인증만 믿지 않고 SUBSCRIBE 시점 membership과 room 단위 partition을 확인했습니다.

Product client

사용자가 보는 메시지 상태 만들기

발신자의 임시 메시지를 서버가 저장한 DB message로 치환하고 상대 수신을 분리했습니다.

Repair

저장보다 먼저 보이는 phantom delivery 없애기

DB commit 뒤에만 broadcast하고, 재연결 cursor가 실시간 전달 공백을 메우게 했습니다.

React 채팅 클라이언트와 두 사용자 브라우저 E2E를 포함하며 공개 서버는 아직 운영하지 않습니다.

  • 발신자는 accepted와 persisted를 구분해 확인합니다.
  • 비멤버는 REST와 STOMP 양쪽에서 방에 접근할 수 없습니다.
  • 연결이 끊겨도 마지막 persisted ID 이후를 동기화해 대화를 이어갑니다.
  • 저장 뒤 전달로 재구성한 제품 흐름 merge

    공개 저장소에서 확인
    근거 ↗
  • 비멤버 STOMP 구독 거부 테스트

    공개 저장소에서 확인
    근거 ↗
  • room 단위 Kafka ordering 테스트

    공개 저장소에서 확인
    근거 ↗
  • persisted ID 이후 동기화 API 테스트

    공개 저장소에서 확인
    근거 ↗
  • optimistic row 치환·DB ID 중복 제거 테스트

    공개 저장소에서 확인
    근거 ↗
  • DB identity·중복 부수 효과 경계 테스트

    공개 저장소에서 확인
    근거 ↗
  • Redis publish 실패 뒤 Kafka redelivery 테스트

    공개 저장소에서 확인
    근거 ↗
  • 발신자 PERSISTED 상태 알림 통합 테스트

    공개 저장소에서 확인
    근거 ↗
  • 두 노드 전달·오프라인·실패 복구 브라우저 E2E

    공개 저장소에서 확인
    근거 ↗
  • 상대방의 읽음 여부를 실시간으로 전달하는 receipt UI는 제공하지 않습니다.
  • 공개 테스트는 로컬 Kafka·Redis·DB 환경의 정합성 검증이며 운영 사용자 수를 뜻하지 않습니다.
JavaSpring BootWebSocketKafkaRedis