전송: WebSocket, HTTP 대체 연결과 재연결
브라우저 위젯과 채팅 서버의 통신: 주소, rc.v1 하위 프로토콜, 프레임 형식, 오류·종료 코드, 제한, seq와 중복 제거 재연결, Server-Sent Events·롱 폴링 HTTP 대체 연결.
이 페이지는 브라우저 위젯과 채팅 서버 사이의 동작을 설명합니다. 문제 해결(개발자 도구의 네트워크 탭), 프록시·방화벽 규칙과 Content Security Policy에 필요합니다.
중요: 통신 프로토콜은 당사 위젯과 당사 서버 사이의 인터페이스입니다. 프로토콜을 직접 사용하는 자체 클라이언트는 공식 지원하지 않으며 버전 1 안에서 필드가 확장될 수 있습니다. 자체 인터페이스에는 JavaScript API, 서버 연결에는 웹훅과 서버 API를 사용합니다.
주소
- WebSocket:
wss://ws.reactive.chat/rc_wss/ - HTTP 대체 연결, 오프라인 양식, 업로드: 같은 주소의
https://, 즉https://ws.reactive.chat/rc_wss/h/…,https://ws.reactive.chat/api/offline/<SITE_KEY>와https://ws.reactive.chat/rc_wss/upload/… - 위젯 파일:
https://cdn.reactive.chat/
위젯이 WebSocket 주소를 찾는 방식:
- 로더 태그의
data-rc-ws-url,window.ReactiveChatSettings.wsUrl,ReactiveChat('boot', { wsUrl })가 설정되어 있으면 해당 값을 사용합니다. - 그 외에는
loader.js위치에서 도출합니다.wss://ws.+ 도메인의 마지막 두 레이블 +/rc_wss/입니다.cdn.reactive.chat에서 도출한 주소:wss://ws.reactive.chat/rc_wss/.
이로 인해 두 가지 주의점이 있습니다.
- 자체 도메인에서
loader.js를 제공하면 운영자의 도메인 아래 주소를 도출합니다. 이 경우data-rc-ws-url을 설정합니다(빠른 시작 참조). .co.uk같은 두 부분 최상위 도메인은 잘못 도출됩니다.data-rc-ws-url을 명시합니다.
WebSocket 경로는 쿼리 매개변수를 평가하지 않습니다.
연결 설정
- 시점: 페이지 로드 중이 아닌 첫 실제 상호작용(마우스 이동, 클릭, 키 입력, 휠, 터치, 포커스)에 연결합니다. 채팅 창이 열려 있거나 열리는 중, 방문자의 대화가 알려져 있거나 미전송 메시지가 있으면 즉시 연결합니다. 모든 요소(보통 로더 태그)에
data-rc-connect="load"를 넣거나window.ReactiveChatSettings.connect = 'load'를 설정하면 이전처럼 로드 시 연결합니다. 페이지 조회 후 일정 시간이 지나 실행되는 규칙은 첫 상호작용부터 집계합니다. - 하위 프로토콜: 위젯이
new WebSocket(url, 'rc.v1')을 열고 서버가rc.v1을 반환합니다. 버전은 주소가 아닌 모든 프레임(v: 1)에 있습니다. - UTF-8의 텍스트 프레임만 사용하며 프레임 하나가 메시지 하나입니다. 압축은 없습니다(서버의
permessage-deflate꺼짐). - 핸드셰이크:
open후 늦어도 5초 안에init을 보내며 서버가init_ok로 응답합니다. 8초 안에open이 없으면 실패로 간주하고 10초 안에init_ok가 없으면 다시 연결합니다. - 출처 검사: 서버는
Origin헤더의 호스트 이름을 프로젝트 도메인(계정의 허용된 출처)과 비교합니다. 스킴과 포트는 무관하며 서브도메인은 일치합니다(www.your-company.com은your-company.com에 일치하지만notyour-company.com은 아님). 실패하면origin_denied후 1008로 닫고 재시도하지 않습니다. 도메인을 하나도 입력하지 않으면 출처 검사를 하지 않으므로 반드시 도메인을 입력합니다.
프레임 형식
양방향의 모든 메시지는 다음 형식입니다.
{ "v": 1, "t": "msg", "id": "01JAB3C4D5E6F7G8H9JKMNPQRS", "ts": 1792500000000, "d": { } }
- v(정수, 필수) – 프로토콜 버전이며 현재
1입니다. - t(문자열, 필수) – 메시지 유형,
[a-z0-9_]{1,24}입니다. - id(문자열, 필수) – Crockford Base32 26자의 대문자 ULID이며 중복 제거 키이기도 합니다.
- ts(정수, 필수) – 1970년 이후 밀리초이며 진단용입니다.
- d(객체, 필수) – 페이로드입니다.
null은 허용하지 않으며 비어 있으면{}입니다.
알 수 없는 유형과 필드는 조용히 무시하므로 이전 위젯과 새 서버의 호환성을 유지합니다.
메시지 유형 개요
위젯에서 서버로:
- init – 연결 등록: 사이트 키, 세션·방문자·탭·대화 ID, 언어, 동의 상태, 선택적 서명된 신원(
user), 페이지 맥락(ctx), 재개 지점(resume). - msg – 방문자 메시지(텍스트, 최대 5개 첨부 파일, 빠른 답변 또는 양식 응답).
- typing – 입력 표시(
start/stop). 입력란 내용은 절대 전송하지 않습니다. - read –
seq까지 읽음. - ctx – SPA 탐색이나
identify후의 맥락 변경. - csat – 긍정·부정 평가, 최대 1000자 의견.
- upload_req – 업로드 승인 요청(이름, MIME 유형, 크기, SHA-256).
- transcript, forget, consent – 대화를 이메일로 받기, 자신의 데이터 삭제, 동의·철회.
- ping – 연결 유지.
서버에서 위젯으로:
- init_ok – 확인된 ID, 기록, 디자인, 기능과 제한(아래 참조), 신원 검사 결과(
identity.verified). - msg – 방문자 자신의 메시지 에코(
client_id)를 포함한 메시지. - bot_typing, bot_stream – AI 어시스턴트가 생각하거나 작성 중입니다.
bot_stream은 항상 지금까지 보이는 전체 텍스트를 최대 150 ms에 한 번 전송합니다. - typing, read, handoff, queue_pos, proactive, conv_list, error, pong.
오류 코드
오류는 t: "error" 프레임으로 오며 d.code, 개발자용 영어 d.message, d.retry(no, immediate, backoff, after_reinit), 필요시 d.retry_after_ms, d.detail이 있습니다. 목록은 고정되어 있으며 알 수 없는 코드는 internal처럼 처리합니다.
- bad_json – 유효하지 않은 JSON. 3회 발생 후 1008로 종료.
- bad_envelope – 프레임 형식 위반(ULID 시각이 24시간 넘게 차이 나는 경우 포함,
detail.reason: stale_or_future). - unsupported_version – 버전 협상 불가, 4001로 종료.
- unknown_site_key – 알 수 없는 키, 프로젝트 일시 중지·보관, 4002로 종료.
- origin_denied – 프로젝트 도메인에 없는 출처, 1008로 종료.
- visitor_blocked – 방문자 차단, 1008로 종료.
- auth_failed – 신원 검사 실패.
detail.reason은no_hash,bad_hash,bad_exp,expired,exp_too_far입니다. 세션은 익명으로 계속됩니다. - not_initialized / already_initialized –
init전 메시지 또는 두 번째init. - init_timeout – 5초 안에
init없음, 4004로 종료. - conversation_not_found / conversation_closed
- rate_limited – 빈도 제한,
retry_after_ms포함. 같은 연결에서 반복되면 서버가 4008로 닫습니다. - payload_too_large – 프레임이 64 KiB 초과.
- msg_too_long – 요금제 길이 제한 초과,
detail.max. - unsupported_kind, attachment_invalid
- upload_disabled, upload_too_large, upload_mime_denied, upload_expired
- consent_required – 동의가 필요한 작업.
- dnd_active – “방해 금지” 활성화.
- session_conflict – 같은 세션의 연결이 너무 많음(3개 초과), 4003으로 종료하며 탭이 팔로워가 됩니다.
- internal, unavailable – 일시적 오류, 대기 후 재시도.
종료 코드
- 1000, 1001, 1006, 1011, 1013, 4004 – 대기 후 다시 연결합니다.
- 1008 – 출처 거부, 반복된 잘못된 JSON, 악용 방지 차단입니다. 이 페이지에서 재시도하지 않습니다.
- 4001 – 버전 협상 불가, 재시도 없음.
- 4002 – 알 수 없는 사이트 키 또는 비활성 프로젝트, 재시도 없음.
- 4008 – 반복 빈도 제한.
retry_after_ms만큼 기다린 뒤 다시 연결합니다. - 4003 – 세션 충돌. 다른 탭의 팔로워로 실행됩니다.
연결별 제한
서버가 init_ok.limits와 init_ok.features로 실제 적용값을 보냅니다.
- heartbeat_ms 25 000 – 마지막 발신 메시지 25초 후
ping을 보냅니다. - pong_timeout_ms 10 000 –
pong이 두 번 없으면 다시 연결합니다. - idle_timeout_ms 70 000 – 수신 프레임 없이 70초가 지나면 서버가 1001로 닫습니다.
- max_frame_bytes 65 536 – 더 크면
payload_too_large. - msg_per_10s 20과 frames_per_10s 120 – 연결별 토큰 버킷이며 초과하면
rate_limited. - reconnect_cap_ms 30 000 – 재시도 간 최장 대기 시간.
- dedup_window_ms 86 400 000 – 24시간 중복 제거.
- features.max_msg_len – 방문자 메시지당 문자 수(Unicode 코드 포인트): Free 1000자, 유료 4000자입니다. 너무 긴 텍스트는 보내지 않습니다.
- features.max_upload_bytes, features.allowed_mime – 제한과 할당량 참조.
재연결, 순서와 중복
- 대기:
min(30000, 500 × 2^(attempt−1))ms의 상위 절반에서 임의 값을 사용합니다(1회: 250–500 ms, 7회부터: 15–30초). 백그라운드 탭에서는 3배입니다. 서버의retry_after_ms가 하한입니다. 재시도 횟수 제한은 없고init_ok에서만 카운터를 초기화합니다. 브라우저가online을 알리면 즉시 시도하고 오프라인이면 기다립니다. - 순서: 대화 메시지마다 1부터 연속된
seq가 있습니다. 시각이 아닌seq로 표시합니다. 재연결 시resume.last_seq와 최대 50개의 대기 ID를 보냅니다. 서버는 이후 모든 메시지(최대 100개, 더 있으면has_more)를 전달하고 이미 도착한 메시지의 확인도 반복합니다. - 중복 없음: 서버는 각 메시지
id를 24시간 기억합니다. 재전송을 다시 저장하지 않고 같은 확인을 보냅니다. 위젯의 “다시 보내기”도 같은id를 사용합니다. - 발신 메시지 상태:
pending→sent→delivered(서버 에코) →read입니다. 5회 시도 후, 24시간 후 또는retry: "no"이면failed입니다. - 오프라인 버퍼: 연결이 끊기면 최대 50개, 128 KiB, 24시간의 버퍼에 넣습니다. 동의가 있으면
localStorage, 없으면sessionStorage이며 다음init_ok에 전송합니다. - 여러 탭, 연결 하나: 웹사이트의 탭들이 리더를 정합니다(Web Locks, 그 외에는
localStorage연결 유지). 리더만 연결하고 나머지는BroadcastChannel로 통신합니다.
HTTP 대체 연결
네트워크가 WebSocket을 통과시키지 않으면 같은 프레임과 제한으로 일반 HTTPS 요청을 사용합니다. 위젯만 사용하며 상담원 화면은 항상 WebSocket입니다. 편집자용 간단한 안내는 HTTP 대체 연결에 있습니다.
적용 조건:
- 브라우저가 WebSocket을 지원하지 않음,
- 이 페이지에서 WebSocket 연결이 한 번도 성립하기 전에 두 번 연속 실패함(8초 동안 결과가 없어도 실패),
sessionStorage의rc_http가 30분 이내임(같은 탭의 이후 페이지),- 시작 시 문서에 있는 모든 요소의
data-rc-transport="http"또는 로더 앞의window.ReactiveChatSettings = { transport: 'http' }로 강제함.ReactiveChat('boot', { transport: 'http' })는 효과가 없습니다. 연결 방식은 시작할 때 고정됩니다.
브라우저에 fetch가 없으면 대체 연결도 없습니다. 기존 WebSocket이 끊기면 WebSocket으로 다시 연결합니다. 대체 연결 후 페이지마다 한 번 WebSocket이 이제 통과하는지 조용히 확인합니다. 성공하면 rc_http를 삭제하고 다음 페이지에서 WebSocket을 사용합니다.
요청(https://ws.reactive.chat/rc_wss/h/ 아래):
- POST h/auf – 연결을 엽니다. 응답은
{"v":1,"cid":"…","tok":"…","poll_ms":25000,"idle_ms":60000}입니다.tok는 단기 연결 토큰(32자)입니다. - GET h/<cid>/sse?tok=… – Server-Sent Events로 프레임마다
id:와data:, 15초마다 연결 유지 신호입니다. 누락된 프레임은Last-Event-ID로 전달됩니다. - GET h/<cid>/poll?tok=…&nach=<n> – 롱 폴링입니다. 서버가 최대 25초 열어 두고
{"v":1,"n":…,"rahmen":[…]}로 응답합니다. - POST h/<cid>/tx?tok=… –
text/plain전송, 프레임 하나 또는 최대 20개 배열이며 응답 204입니다. - POST h/<cid>/zu?tok=… – 종료(
sendBeacon사용).
먼저 SSE를 시도하며 5초 안에 첫 프레임이 없으면(예: 프록시 버퍼링) 롱 폴링으로 전환합니다. 404(unbekannt), 403, 413, 429 같은 오류는 연결 끊김으로 처리하고 대기 후 재시도합니다. 요청 없이 60초가 지나면 서버가 닫습니다. CORS의 Access-Control-Allow-Origin은 인증 정보 없이 출처를 반영합니다. 출처 검사는 WebSocket처럼 init에서 수행합니다.
운영자 측 요구 사항:
- CSP의
connect-src에 채팅 주소를 두 형식으로 넣습니다.wss://및https://. - 방화벽과 프록시가 최소 30초 요청을 허용합니다.
- 자체 프록시가
/rc_wss/를 버퍼링 없이 전달하고(nginxproxy_buffering off,proxy_cache off, Apacheflushpackets=on, Caddyflush_interval -1), 쿼리 토큰이 있는/rc_wss/h/를 접근 로그에 기록하지 않습니다. 자체 도메인 참조.
브라우저 저장: 개인정보 없이 HTTPS 연결 시각만 sessionStorage의 rc_http에 저장합니다.
활성 경로 확인
개발자 도구의 네트워크 탭:
- WebSocket:
wss://ws.reactive.chat/rc_wss/또는 자체 서브도메인의 WebSocket 연결입니다. 프레임에init,init_ok,ping/pong과 메시지가 보입니다. - HTTP 대체 연결: 먼저
h/auf, 이후 오래 유지되는…/sse(또는 반복…/poll), 전송 메시지마다…/tx입니다.
최종 업데이트: