문서 / 실시간 WebSocket v1

실시간 WebSocket v1

기존 공개 room 연결과 메시지·이력 형식을 안내합니다.

공개 데모와 회원 전용 기능을 구분하세요

v1 경로는 /_joripspace/realtime입니다. room 단위 발행·구독을 제공하지만 v2의 서버 서명 접속권과 room별 사용자 권한 검증 계약을 제공하는 것은 아닙니다. 임의 room 이름을 숨기는 것만으로 권한 검사가 완성되지 않습니다.

아래 main room은 누구나 참여해도 되는 공개 데모입니다. 주문·개인 알림·회원 전용 대화처럼 권한 검사가 필요한 신규 개발에는 v2 예제를 사용하세요. 두 경로는 room과 데이터가 분리되어 있습니다.

두 브라우저 화면에서 송수신

브라우저 realtime-v1.js
// 공개 데모 전용입니다. 회원 전용 메시지는 v2를 사용하세요.
// 페이지에 <button id="send" disabled>보내기</button><ul id="messages"></ul> 추가
const room = 'main';
const url = new URL('/_joripspace/realtime', location.origin);
url.protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
url.searchParams.set('room', room);
url.searchParams.set('resource_type', 'chat');
const socket = new WebSocket(url);
const button = document.querySelector('#send');
const seen = new Set();
function render(message) {
  if (message.type !== 'chat.message' || !message.id || seen.has(message.id)) return;
  seen.add(message.id);
  const item = document.createElement('li');
  item.textContent = String(message.data?.body || '');
  document.querySelector('#messages').append(item);
}
socket.addEventListener('message', async (event) => {
  const message = JSON.parse(event.data);
  if (message.type === 'system.connected') {
    button.disabled = false;
    // 수신 리스너를 먼저 연결한 뒤 이력과 실시간 이벤트를 ID로 병합합니다.
    const historyUrl = new URL('/_joripspace/realtime/rooms/' + room + '/messages', location.origin);
    historyUrl.searchParams.set('resource_type', 'chat');
    historyUrl.searchParams.set('limit', '50');
    const response = await fetch(historyUrl);
    if (!response.ok) return console.warn('이력을 불러오지 못했습니다.');
    const page = await response.json();
    page.messages.forEach(render);
    // 이전 이력은 next_cursor를 보관해 before 쿼리로 추가 조회합니다.
  } else if (message.type === 'error') console.warn(message.code);
  else render(message);
});
button.addEventListener('click', () => {
  if (socket.readyState !== WebSocket.OPEN) return;
  const message = { id: crypto.randomUUID(), type: 'chat.message', data: { body: '안녕하세요!' } };
  socket.send(JSON.stringify(message)); // 재시도할 때는 같은 message 객체를 재사용
});
socket.addEventListener('close', () => { button.disabled = true; });
window.addEventListener('pagehide', () => socket.close());

서로 다른 두 창에서 같은 프로젝트·room·resource_type으로 연결하세요. 시스템 연결 메시지를 받은 뒤 보내기를 활성화합니다. DB에 직접 행을 넣거나 Worker 전역 Map에 소켓을 모으는 것은 플랫폼 room 발행이 아닙니다.

이력 조회와 누락 복구

검색·페이지네이션
const url = new URL('/_joripspace/realtime/rooms/main/messages', location.origin);
url.searchParams.set('resource_type', 'chat');
url.searchParams.set('limit', '50');
// 이전 응답의 next_cursor가 있으면 다음 요청에 넣습니다.
if (nextCursor) url.searchParams.set('before', nextCursor);
// 검색할 때는 같은 URL에 search를 추가합니다.
url.searchParams.set('search', '안녕하세요');
const response = await fetch(url);
if (!response.ok) throw new Error('이력 조회 실패');
const page = await response.json();

기본 예제는 한 번 연결하고 최근 50건만 가져옵니다. 운영형 v1 클라이언트에는 1초부터 최대 30초까지 지수 백오프·무작위 지연, 연결 종료 시 재연결, 여러 페이지 이력 복구, 메시지 ID 기준 병합과 시간·ID 정렬을 추가해야 합니다. 장시간 사용 시 표시 목록과 중복 확인 집합도 정리하세요. 자동 복구 SDK가 필요하면 v2를 사용합니다.

목록·편집·삭제 구현

이력은 최신순입니다. 오래된 메시지부터 표시하려면 가져온 페이지를 복사해 뒤집고, 여러 페이지·실시간 이벤트는 ID로 중복을 제거한 뒤 created_at·id로 정렬하세요. 검색어를 바꾸면 이전 커서를 초기화합니다. 오래된 검색 응답이 새 결과를 덮어쓰지 않도록 요청 취소나 요청 번호 검사를 넣으세요.

v1에는 기존 본문을 덮어쓰는 수정 API가 없습니다. 같은 ID를 다시 보내도 수정되지 않습니다. 앱 DB에 원본과 버전을 저장하고 새로운 ID의 변경 이벤트를 발행하는 방법은 편집 설계를 참고하세요. v1 발행에는 기존 WebSocket send 형식을 사용합니다.

공개 메시지 삭제
// 공개 v1 room 전용. room과 messageId는 현재 목록의 값입니다.
const url = new URL('/_joripspace/realtime/rooms/' + encodeURIComponent(room)
  + '/messages/' + encodeURIComponent(messageId), location.origin);
url.searchParams.set('resource_type', 'chat');
const response = await fetch(url, { method: 'DELETE' });
if (!response.ok && response.status !== 404) throw new Error('삭제 실패');
// 성공 또는 이미 없는 메시지라면 현재 화면의 해당 행을 제거합니다.
// v1은 다른 화면에 삭제 알림을 자동 전송하지 않습니다.
// 다른 화면은 이력을 다시 조회해 현재 조회 범위의 목록을 교체해야 합니다.

v1에는 v2의 사용자별 삭제 권한 검사와 onDelete 알림 계약이 없습니다. 앱 서버에 버튼 권한 검사만 붙였다고 공개 v1 경로까지 보호되지는 않습니다. 작성자·관리자 전용 편집·삭제에는 v2와 앱 서버 권한 검사를 사용하세요. 삭제 후 재조회는 현재 페이지 범위만 대조해야 하며 다른 페이지에 있다는 이유로 삭제된 것으로 처리하지 않습니다.

실제 수신을 확인하세요

보낸 화면의 입력이 지워지는 것만으로 성공을 판단하지 마세요. 별도 두 창에서 같은 ID 수신, 다른 room 격리, 연결 단절 중 발행한 메시지 이력 조회를 확인합니다. 토큰·이메일·개인정보는 WebSocket URL에 넣지 않습니다.