개인 메시징 프로젝트 → 실제 React 채팅 클라이언트
Realtime Chat
연결 성공이 아니라, 메시지가 저장되고 끊긴 뒤 다시 이어지는 순간까지 채팅 제품으로 만들었습니다.

왜 시작됐는가
구독 인가와 Kafka ordering을 검증한 메시징 실험에서 출발했지만 실제 사용자는 연결보다 메시지가 사라지지 않는지를 경험합니다.
누구와 어떤 상황에서 만들었는가
- Setting
- 개인 메시징 프로젝트 → 실제 React 채팅 클라이언트
- Audience
- 두 브라우저에서 전송·수신·재연결을 확인하려는 포트폴리오 방문자
- Role
- 메시지 영속화 경계 재설계, WebSocket 클라이언트, 재연결 E2E
내가 맡은 범위
- 메시지를 SENDING·ACCEPTED·PERSISTED·FAILED 상태로 구분해 낙관적 UI와 서버 결과를 합쳤습니다.
- DB commit 전에 상대에게 보일 수 있던 독립 broadcast 경로를 제거했습니다.
- 오프라인 동안 빠진 메시지를 마지막 DB ID 이후부터 다시 가져오고 중복 없이 합쳤습니다.
실제 사용자 흐름
- 01
닉네임으로 상대를 찾아 1:1 대화방을 엽니다.
- 02
메시지는 SENDING에서 ACCEPTED, DB 저장이 끝나면 PERSISTED로 바뀝니다.
- 03
상대가 오프라인이었다면 재연결 뒤 마지막 persisted ID 이후 메시지를 보충합니다.
- STOMP SEND를 roomId key로 Kafka에 기록합니다.
- Persistence transaction이 DB message ID를 만든 뒤 commit합니다.
- commit된 payload만 상대와 발신자에게 broadcast합니다.
- 재연결 client는 마지막 DB ID 이후를 sync합니다.
전환점과 발견한 문제
실제 화면에서 상대가 본 메시지가 DB에 없을 수 있다는 사실이 드러나면서 전달 순서를 다시 설계했습니다.
구독 권한과 room ordering을 나누어 검증하기
handshake 인증만 믿지 않고 SUBSCRIBE 시점 membership과 room 단위 partition을 확인했습니다.
사용자가 보는 메시지 상태 만들기
발신자의 임시 메시지를 서버가 저장한 DB message로 치환하고 상대 수신을 분리했습니다.
저장보다 먼저 보이는 phantom delivery 없애기
DB commit 뒤에만 broadcast하고, 재연결 cursor가 실시간 전달 공백을 메우게 했습니다.
현재 결과
React 채팅 클라이언트와 두 사용자 브라우저 E2E를 포함하며 공개 서버는 아직 운영하지 않습니다.
- 발신자는 accepted와 persisted를 구분해 확인합니다.
- 비멤버는 REST와 STOMP 양쪽에서 방에 접근할 수 없습니다.
- 연결이 끊겨도 마지막 persisted ID 이후를 동기화해 대화를 이어갑니다.
Engineering cases
한 번에 한 경계만 깊게
이 프로젝트의 흐름을 단계별로 재생할 수 있습니다.
Flow 재생하기공개 근거
- 근거 ↗
저장 뒤 전달로 재구성한 제품 흐름 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 환경의 정합성 검증이며 운영 사용자 수를 뜻하지 않습니다.