[FE-06] STOMP 실시간 소켓 훅 (useChatSocket)

작업 내용 (설계 의도)

변경 사항

근거: 20260704-채팅시스템고도화-design-fe-app.md “방안 비교 — 실시간 상태”·“재연결/백필”·BE TDD “STOMP 계약”.

WebSocket/STOMP 실시간을 훅으로 캡슐화한다(컴포넌트는 소켓을 직접 다루지 않음). 신규 파일 lib/useChatSocket.ts + @stomp/stompjs 의존성 추가(package.json). RN 전역 WebSocketwebSocketFactory로 사용, brokerURL/ws, CONNECT 헤더에 Authorization: Bearer <accessToken>(useAuthStore).

  • 구독: SUBSCRIBE /topic/rooms/{roomId}·/typing·/read. 수신 BroadcastMessageChatMessage로 정규화(messageId→id,userId→senderId,createdAt→sentAt)해 queryClient.setQueryData(messagesQueryKey(roomId))에 append(dedup by id). 서버 상태 스토어 복사 금지.
  • 발화: send(roomId, content)SEND /app/rooms/{roomId}/send. 미연결 시 REST POST /rooms/{id}/messages 폴백.
  • 타이핑/읽음: sendTyping, sendRead. 수신 타이핑/읽음은 콜백/상태로 노출.
  • 재연결: 지수 백오프(reconnectDelay 증가), 3회 연속 실패 시 pollingFallback=true 노출(방 화면이 REST refetchInterval 활성). @react-native-community/netinfo로 오프라인 즉시 감지.
  • 재연결 성공 시 마지막 수신 id 이후를 backfillMessages(FE-05)로 채우고 dedup. backfill은 FR-10(2단계) 기능chat.realtime.enabled 게이트 하위에서만 동작하며, 실시간(1단계 핵심 송수신)과 backfill(2단계 보정)의 경계를 코드 주석으로 명시한다.
  • 기능 플래그 chat.realtime.enabled OFF면 연결하지 않음(REST 폴백만). 실시간 송수신·타이핑·읽음(FR-6/7/8, 1단계)과 재연결 backfill(FR-10, 2단계)이 모두 이 플래그 하위지만, backfill은 2단계 배포 전까지 비활성 가능하도록 별도 조건 분기.

롤백: 플래그 EXPO_PUBLIC_CHAT_REALTIME_ENABLED=false로 즉시 실시간 비활성(REST 폴링 유지).

의존

  • FE-02 (chat-types)

다이어그램

처리 흐름

sequenceDiagram
    participant Screen
    participant Hook as useChatSocket
    participant Client as stompjs
    participant Cache as queryClient
    Screen->>Hook: subscribe(roomId)
    Hook->>Client: SUBSCRIBE /topic/rooms/{id}
    Client-->>Hook: BroadcastMessage
    Hook->>Hook: normalize → ChatMessage
    Hook->>Cache: setQueryData(append, dedup)
    Screen->>Hook: send(content)
    Hook->>Client: SEND /app/rooms/{id}/send

테스트 케이스

  • 수신 BroadcastMessageChatMessage로 정규화해 메시지 캐시에 append한다
  • 동일 messageId 중복 수신 시 캐시에 한 번만 존재한다(멱등 dedup)
  • 미연결 상태에서 send 호출 시 REST 폴백으로 전송한다
  • 재연결 3회 연속 실패 시 pollingFallback이 true가 된다
  • 재연결 성공 시 마지막 수신 id 이후 구간을 backfill로 채운다
  • CONNECT 시 Authorization 헤더에 accessToken이 포함된다
  • 플래그 OFF면 소켓 연결을 시도하지 않는다