본문으로 건너뛰기

실시간 통신

Connect Base 실시간 통신 심화 가이드입니다.

WebSocket 아키텍처

카테고리/룸 패턴

WebSocket Server
├── 카테고리: chat
│   ├── 룸: room-1
│   ├── 룸: room-2
│   └── 룸: room-3
├── 카테고리: notifications
│   └── 룸: user-123
└── 카테고리: presence
    └── 룸: online-users

연결 관리

typescript
// Public Key 만으로 연결 (게스트)
await cb.realtime.connect()

// 또는 앱 멤버 토큰으로 연결
await cb.realtime.connect({
  accessToken: 'user_access_token',  // 앱 멤버 JWT
  userId: 'user_123',                 // 표시용 식별자 (선택)
  maxRetries: 10,                     // 자동 재연결 횟수
  retryInterval: 1000                 // 재연결 간격 (ms)
})

// 연결 종료
cb.realtime.disconnect()

// 상태 확인
console.log(cb.realtime.connectionId)  // 현재 연결 ID
console.log(cb.realtime.appId)          // 앱 ID

Pub/Sub 패턴

카테고리 구독

📌 카테고리는 콘솔의 실시간 → 카테고리 에서 미리 만들어두어야 합니다. "룸"이라는 별도 개념은 없으며, 룸별로 분리하려면 chat-room-1, chat-room-2 처럼 카테고리 이름으로 구분합니다.

typescript
// 카테고리 구독 (반환된 Subscription 객체로 send/onMessage 사용)
const chat = await cb.realtime.subscribe('chat-room-123')

// 메시지 수신 핸들러 등록 — 반환된 함수로 해제 가능
const off = chat.onMessage<{ text: string; userId: string }>((msg) => {
  console.log('새 메시지:', msg.data.text, 'sentAt:', msg.sentAt)
})

// 핸들러 제거
off()

메시지 발행

typescript
// 발신자도 echo 받음 (기본값)
await chat.send({
  text: '안녕하세요!',
  userId: 'user_123'
})

// 발신자 자신은 echo 안 받음
await chat.send({ text: '...' }, { includeSelf: false })

히스토리 조회

typescript
// persist=true 로 설정된 카테고리만 메시지가 저장됩니다
if (chat.info.persist) {
  const history = await chat.getHistory<{ text: string }>(50)
  history.messages.forEach((m) => {
    console.log(m.from, m.data.text, new Date(m.sentAt).toISOString())
  })
}

구독 해제

typescript
await chat.unsubscribe()

Presence / Typing

cb.realtime 가 presence/typing 의 단일 SoT(Single Source of Truth) 입니다. cb.database.setPresence / subscribePresence 는 v2.0.0 에서 제거되었습니다.

typescript
// 본인 온라인 상태 publish
await cb.realtime.setPresence('online', { device: 'web', metadata: { nickname: '홍길동' } })

// 다른 사용자 상태 구독
const unsub = await cb.realtime.subscribePresence('user_123', (info) => {
  console.log(info.userId, info.status, info.eventType) // join | leave | update
})

// 룸 단위 typing indicator
await cb.realtime.startTyping('room-123')
await cb.realtime.onTypingChange('room-123', (typing) => {
  console.log(typing.users)
})

WebRTC (화상/음성)

cb.webrtc.connect(...) 한 메서드로 1:1 / 그룹 통화 모두 지원합니다.

typescript
// 1) 로컬 스트림 가져오기
const localStream = await navigator.mediaDevices.getUserMedia({
  video: true,
  audio: true
})

// 2) WebRTC 시그널링 서버에 연결
await cb.webrtc.connect({
  roomId: 'live:room-123',
  userId: 'user-456',
  isBroadcaster: true,   // 송출자 / false 면 시청자
  localStream
})

// 3) 이벤트 핸들러
cb.webrtc.onRemoteStream((peerId, stream) => {
  // 새 비디오 요소에 stream 부착
  console.log('peer joined:', peerId)
})

cb.webrtc.onPeerJoined((peerId, info) => {
  console.log('피어 입장:', peerId, info)
})

cb.webrtc.onPeerLeft((peerId) => {
  console.log('피어 퇴장:', peerId)
})

cb.webrtc.onStateChange((state) => {
  console.log('연결 상태:', state)
})

// 4) 종료
cb.webrtc.disconnect()

룸 ID 명명 규칙:

  • live:xxx — 라이브 스트리밍 (broadcaster + viewers)
  • call:xxx — 1:1 또는 그룹 통화

실시간 패턴

채팅

typescript
const chat = await cb.realtime.subscribe('chat-room-123')

// 메시지 전송
async function sendMessage(text: string) {
  await chat.send({
    id: crypto.randomUUID(),
    text,
    userId: currentUser.id,
    userName: currentUser.name
  })
}

// 메시지 수신
chat.onMessage<{ text: string; userName: string }>((msg) => {
  addMessageToUI(msg.data)
})

실시간 협업 (커서 공유)

typescript
const collab = await cb.realtime.subscribe('collab-doc-123')

// 내 커서 위치 발행 (echo 없음으로 전송량 절반 절약)
document.addEventListener('mousemove', (e) => {
  void collab.send(
    { userId: currentUser.id, x: e.clientX, y: e.clientY },
    { includeSelf: false }
  )
})

// 다른 사용자 커서 수신
collab.onMessage<{ userId: string; x: number; y: number }>((msg) => {
  updateCursorPosition(msg.data.userId, msg.data.x, msg.data.y)
})

실시간 대시보드

typescript
const metrics = await cb.realtime.subscribe('metrics')

// 서버 → 클라이언트 메트릭 푸시는 보통 백엔드에서 NATS publish
metrics.onMessage<{ cpu: number; memory: number; requests: number }>((msg) => {
  updateDashboard(msg.data)
})