실시간 소셜 룸 상태 관리: 입장·이탈·재접속 처리
실시간 소셜 룸은 접속자 목록에 사용자를 추가하고 제거하는 기능만으로 유지되지 않습니다. 모바일 네트워크가 잠시 끊기거나 앱이 백그라운드로 가면 연결 종료 이벤트가 늦게 도착할 수 있고, 재접속한 사용자가 기존 아바타와 별도 참가자로 생성될 수도 있습니다.
룸 상태를 안정적으로 관리하려면 사용자 계정, 룸 참가자와 현재 네트워크 연결을 서로 다른 식별자로 다뤄야 합니다. 전체 Snapshot과 변경 이벤트를 결합하고, 연결 단절에는 짧은 복구 구간을 제공해야 Ghost User와 중복 아바타를 줄일 수 있습니다.
계정과 참가자와 연결은 같은 것이 아닙니다
한 사용자가 같은 계정으로 재접속해도 네트워크 연결은 새로 만들어집니다. 계정 ID만으로 연결을 관리하면 이전 연결의 종료 이벤트가 새 연결을 제거할 수 있습니다.
userId
서비스 사용자
participantId
특정 룸에서의 참가자
sessionId
현재 네트워크 연결 세션아바타 Entity는 participantId에 연결하고, Socket이나 Transport 상태는 sessionId로 구분합니다. 재접속으로 Session이 바뀌면 같은 참가자와 아바타에 새 연결을 연결합니다. 이전 Session에서 늦게 도착한 이벤트는 현재 Session과 다르므로 무시할 수 있습니다.
룸을 완전히 나갔다가 다시 입장하는 경우에는 새 Participant를 만들 수 있습니다. 재접속과 재입장의 의미를 분리해야 이전 위치와 권한을 복구할지 처음부터 생성할지 결정할 수 있습니다.
입장은 하나의 상태 전이입니다
서버가 참가자를 승인한 시점과 Unity Scene이 준비된 시점은 다릅니다. 승인 직후 다른 사용자에게 아바타를 표시하면 아직 에셋을 로드하지 못한 참가자가 공간에 멈춰 있는 것처럼 보일 수 있습니다.
JOIN_REQUESTED
↓
ADMITTED
↓
LOADING
↓
READY
↓
ACTIVE클라이언트는 룸 Snapshot과 공간 에셋, 아바타 Prefab을 준비한 뒤 READY를 보냅니다. 서버는 그 시점부터 이동과 상호작용 이벤트를 전달하고 다른 참가자에게 활성 상태를 알립니다.
준비 중에도 채팅이나 룸 설정이 바뀔 수 있습니다. 이벤트를 무조건 버리지 않고 Snapshot 버전 이후의 변경만 순서대로 적용합니다. 준비 시간이 명령 유효 시간을 넘으면 입장을 취소하고 리소스를 정리합니다.
전체 Snapshot과 변경 이벤트를 함께 사용합니다
변경 이벤트만 구독하면 연결 전에 발생한 입장과 퇴장을 알 수 없습니다. 반대로 전체 참가자 목록을 계속 조회하면 변경이 없을 때도 같은 데이터를 반복해서 받습니다.
룸에 들어갈 때는 현재 Snapshot을 받고, 이후에는 작은 변경 이벤트를 적용하는 구조가 자연스럽습니다.
Room Snapshot
revision = 42
participants = [...]
Room Event
revision = 43
type = PARTICIPANT_JOINED이벤트는 지연되거나 순서가 바뀔 수 있습니다. 현재 Revision보다 작은 이벤트는 버리고, 하나 이상 건너뛴 이벤트가 오면 누락 구간을 요청하거나 최신 Snapshot을 다시 가져옵니다.
Snapshot 조회와 이벤트 구독 사이에도 빈 구간이 생길 수 있습니다. 구독을 먼저 열고 Snapshot 로딩 중의 이벤트를 버퍼링하거나, Snapshot Revision 이후 이벤트를 다시 요청할 수 있어야 합니다.
Heartbeat는 연결 생존과 사용자 활동을 구분합니다
사용자가 움직이지 않는다고 연결이 끊긴 것은 아닙니다. 반대로 Socket이 열려 있다고 앱이 정상적으로 룸을 처리하고 있다는 보장도 없습니다.
클라이언트와 서버는 Heartbeat나 Transport 상태로 연결 생존을 확인합니다. 마지막 응답이 기준 시간을 넘으면 즉시 퇴장시키기보다 RECONNECTING 상태로 전환하고 짧은 유예 시간을 둘 수 있습니다.
ACTIVE
↓ 응답 지연
RECONNECTING
↓ 복구 성공 ↓ 유예 시간 종료
ACTIVE LEFT유예 시간에는 아바타를 그대로 둘지 흐리게 표시할지 서비스 정책으로 정합니다. 중요한 점은 활동 없음, 일시 단절과 명시적 퇴장을 같은 상태로 처리하지 않는 것입니다.
Ghost User는 제거 이벤트 하나로 해결되지 않습니다
앱 강제 종료나 네트워크 단절에서는 정상적인 퇴장 요청이 오지 않을 수 있습니다. 서버는 참가자 Lease를 갱신하고, 유예 시간이 끝난 참가자를 만료시켜야 합니다.
클라이언트도 서버의 퇴장 이벤트만 기다리지 않습니다. 새 Snapshot에서 참가자가 사라졌다면 아바타와 관련 구독을 정리합니다. 아바타 GameObject만 제거하고 위치 버퍼, 음성 채널과 리액션 구독을 남기면 보이지 않는 리소스가 계속 유지될 수 있습니다.
Despawn은 여러 번 호출돼도 같은 결과가 되도록 만듭니다. 이미 제거한 Participant에 퇴장 이벤트가 다시 도착해도 오류를 만들지 않아야 합니다.
재접속은 새 입장이 아니라 기존 상태 복구입니다
재접속 요청에는 기존 Participant와 현재 Session을 연결할 수 있는 복구 정보가 필요합니다. 서버는 해당 참가자가 아직 유예 상태인지, 룸에 남아 있는지와 사용자의 권한이 유효한지 확인합니다.
복구가 허용되면 다음 정보를 다시 전달할 수 있습니다.
- 현재 룸 Revision
- 참가자 전체 또는 누락된 변경
- 본인 아바타의 확정 위치와 상태
- 진행 중인 상호작용
- 새 Session ID
클라이언트는 메모리의 상태를 그대로 신뢰하지 않고 서버 Snapshot을 기준으로 보정합니다. 이미 처리한 Event ID를 기억하면 재전송된 채팅과 리액션을 중복 표시하지 않을 수 있습니다.
유예 시간이 끝났거나 룸이 사라졌다면 재접속을 새 입장으로 자동 변환하지 않는 편이 좋습니다. 사용자에게 룸이 종료됐음을 알리고 명시적인 입장 절차를 다시 시작해야 권한과 에셋 상태가 섞이지 않습니다.
상호작용 이벤트에도 멱등성이 필요합니다
좋아요, 손들기, 오브젝트 배치와 채팅 전송은 연결 복구 중 다시 전송될 수 있습니다. 요청마다 Event ID나 Client Request ID를 부여하고 서버가 처리한 결과를 기억하면 같은 사용자 동작이 두 번 생성되는 것을 막을 수 있습니다.
상태를 바꾸는 이벤트에는 대상 Revision을 포함할 수 있습니다. 오래된 룸 설정을 기준으로 보낸 요청이라면 현재 상태와 충돌하는지 확인하고 새 Snapshot을 요구합니다.
순간적인 리액션처럼 잃어도 되는 이벤트와 오브젝트 소유권 변경처럼 반드시 확인해야 하는 이벤트의 전송 정책은 다릅니다. 모든 이벤트를 같은 큐와 재시도 규칙으로 처리하면 중요하지 않은 이벤트가 복구 작업을 막을 수 있습니다.
앱 생명 주기와 룸 생명 주기를 분리합니다
모바일 앱이 백그라운드로 가면 렌더링과 네트워크 처리가 제한될 수 있습니다. 화면이 보이지 않는 동안 무조건 ACTIVE 상태를 유지한다고 가정해서는 안 됩니다.
백그라운드 진입 시 룸 연결을 유지할지, 일시 중단 상태로 전환할지와 얼마 동안 복구를 허용할지를 정합니다. 포그라운드로 돌아오면 Session 생존 여부를 확인하고 현재 Revision부터 상태를 복구합니다.
Unity Scene이 언로드돼도 서버 참가자가 바로 사라지지 않을 수 있습니다. Scene 오브젝트의 수명, 네트워크 Session과 서버 Participant의 수명을 별도로 종료해야 합니다. 각 계층의 정리 작업은 여러 번 호출돼도 안전해야 합니다.
존재 상태는 연결 이벤트보다 길게 유지됩니다
실시간 소셜 룸의 핵심 데이터는 Socket 목록이 아니라 현재 룸에 속한 참가자 상태입니다. 연결은 끊겼다가 다시 만들어질 수 있지만 Participant는 복구 구간 동안 유지될 수 있습니다.
계정, 참가자와 Session을 분리하고 Snapshot Revision과 이벤트 순서를 관리하면 입장, 이탈과 재접속을 같은 규칙으로 처리할 수 있습니다. Ghost User를 줄이는 방법은 퇴장 이벤트를 더 빨리 보내는 것이 아니라 연결 단절을 감지하고 만료시키며 최신 Snapshot으로 보정하는 전체 흐름을 만드는 것입니다.
함께 읽기
- Unity 가상공간 로딩 최적화: Addressables와 Additive Scene 설계Unity 가상공간은 첫 화면에 필요한 UI와 사용자가 입장할 공간의 에셋이 다릅니다. 모든 Scene과 아바타, 텍스처를 시작 시점에 함께 로드하면 진입 시간이 길어지고 사용하지 않는 공간까지 메모리를 차지합니다.
- Unity 아바타 동기화: 위치·회전 보간과 네트워크 지터 처리Unity에서 원격 아바타의 위치를 네트워크로 받아 Transform에 바로 적용하면 움직임이 쉽게 끊깁니다. 화면은 매 프레임 렌더링되지만 위치 패킷은 더 낮은 빈도로 도착하고, 각 패킷의 간격도 일정하지 않기 때문입니다.
- 차량 상태 동기화: Push, WebSocket과 Polling을 함께 쓰는 이유커넥티드 카 앱의 차량 상태는 한 번 조회하고 끝나는 데이터가 아닙니다. 도어, 공조, 충전과 차량 온라인 상태는 앱 밖에서도 계속 바뀝니다. 사용자가 원격 명령을 실행하는 동안에도 이전 상태 이벤트와 새로운 조회 응답이 다른 순서로 도착할 수 있습니다.
- 커넥티드 카 원격 명령: 요청부터 차량 응답까지의 상태 관리커넥티드 카 앱에서 도어 잠금이나 공조 시작 버튼을 누르면 요청은 여러 시스템을 통과합니다. 앱이 서버 요청에 성공했다고 차량 기능까지 실행된 것은 아닙니다. 명령이 차량에 도착하고 제어 장치가 결과를 반환해야 비로소 실행 여부를 판단할 수 있습니다.
- BLE 차량 제어: GATT 연결과 저전력 통신을 안정화하는 방법BLE는 차량과 휴대폰이 가까이 있을 때 작은 데이터를 주고받는 데 적합합니다. 디지털 키, 근거리 도어 제어와 차량 식별처럼 연결 시간이 짧고 배터리 소비를 줄여야 하는 기능에서 자주 사용됩니다.