사용자 A와 B가 같은 장비 온도를 보고 있습니다. B의 와이파이가 잠시 끊긴 동안 값이 20도에서 21도, 다시 22도로 바뀌었습니다. B가 재연결하면 다음 이벤트가 올 때까지 20도를 계속 보여 줘도 될까요?
이벤트를 받는 것과 현재 정답을 아는 것은 서로 다른 문제입니다.
상태 동기화 데모의 이벤트만 전송에서는 A가 v3, B가 v1로 갈라집니다. 버전 + 스냅숏에서는 B가 마지막 버전을 알려 주고 서버의 최신 상태를 받아 두 화면이 다시 v3으로 맞춰집니다.
이벤트는 변화이고 상태는 현재 정답입니다
둘을 쉽게 구분해 봅시다.
| 데이터 | 대답하는 질문 | 예 |
|---|---|---|
| 이벤트 | 무엇이 일어났나? | 온도가 1도 올라감 |
| 상태 | 지금 값은 무엇인가? | 현재 온도 22도 |
연결이 계속 살아 있다면 이벤트를 차례로 적용해 현재 상태를 만들 수 있습니다. 하지만 중간 이벤트를 하나라도 놓치면 클라이언트가 계산한 상태가 서버의 정답과 달라질 수 있습니다.
실시간 연결은 반드시 잠시 끊깁니다. 모바일 네트워크 전환, 노트북 절전, 프록시 제한, 서버 재시작 모두 정상 운영에서 생기는 일입니다. 재연결 버튼만 만드는 것으로는 상태가 복구되지 않습니다.
버전 번호가 뒤처짐을 알려 줍니다
서버가 상태를 확정할 때마다 증가하는 version 또는 revision을 붙입니다.
{
"type": "TEMPERATURE_CHANGED",
"value": 22,
"version": 3
}
각 클라이언트는 마지막으로 온전히 적용한 버전을 기억합니다. 재연결 handshake에서 그 번호를 서버에 보냅니다.
{
"type": "RESUME",
"lastAppliedVersion": 1
}
서버의 현재 버전이 3이라면 B가 두 변경만큼 뒤처졌다는 것을 즉시 알 수 있습니다. 버전이 없으면 화면 값만 비교해야 하고, 우연히 값이 같아도 중간에 중요한 변화가 있었는지 판단하기 어렵습니다.
버전의 범위도 계약에 포함해야 합니다. 장비마다 버전을 따로 올린다면
device-7의 v3처럼 대상 ID와 함께 비교하고, 방 전체 이벤트 스트림이라면 그 방의
offset을 사용합니다. 서로 다른 대상의 v3 두 개는 앞뒤를 비교할 수 없습니다.
서버를 재설치할 때 버전이 0으로 돌아갈 수 있다면 epoch나 스냅숏 ID를 함께 두어 이전
세대의 번호와 구분합니다.
작은 차이는 재생하고 큰 차이는 스냅숏으로 맞춥니다
복구 방식은 두 가지가 있습니다.
누락 이벤트 재생
마지막 offset 이후의 이벤트를 순서대로 다시 보냅니다. 각 변화가 중요한 알림, 로그, 거래 내역처럼 이력을 보여 줘야 할 때 적합합니다.
최신 스냅숏 전송
현재 상태 전체와 그 상태의 버전을 한 번에 보냅니다. 대시보드 카드, 접속자 목록, 장비의 현재 설정처럼 최종 상태가 중요한 경우에 단순합니다.
{
"type": "SNAPSHOT",
"state": { "temperature": 22 },
"version": 3
}
실무에서는 둘을 섞습니다. 오래된 스냅숏을 받은 뒤 그 이후 이벤트를 적용하거나, 차이가 작으면 이벤트만 재생하고 보관 기간을 벗어나면 새 스냅숏을 보냅니다.
스냅숏과 이벤트 사이에 틈을 만들지 않습니다
스냅숏을 읽는 사이 새 이벤트가 발생할 수 있습니다.
스냅숏 v10 읽기 시작
이벤트 v11 발생
스냅숏 v10 전송 완료
클라이언트가 v11을 놓치지 않으려면 스냅숏과 기준 offset을 같은 시점으로 묶어야 합니다.
예를 들어 snapshot v10을 전달한 뒤 반드시 v10 이후 이벤트를 이어서 보내는 계약을
만듭니다.
Socket.IO의 connection state recovery도 세션 ID와 마지막 offset을 사용해 일시적 연결 단절 동안 놓친 패킷과 방 정보를 복구합니다. 복구가 항상 성공하는 것은 아니므로 최종적으로 전체 상태를 다시 동기화할 경로가 필요하다고 명시합니다.
화면에는 연결 상태를 솔직하게 표시합니다
오프라인인데 마지막 값을 그대로 보여 주면 사용자는 그 값이 현재 정보라고 오해할 수 있습니다.
- 값 옆에
연결 끊김과 마지막 갱신 시각을 표시합니다. - 재연결 중에는 변경 입력을 막거나 임시 저장임을 알립니다.
- 복구가 끝나기 전에는
동기화 중상태를 사용합니다. - 스냅숏 적용이 완료된 뒤에만
최신으로 표시합니다.
상태 동기화는 네트워크 코드뿐 아니라 사용자에게 정보의 신선도를 전달하는 UI 문제이기도 합니다.
스냅숏은 현재 정답 전체를 담기 때문에 이벤트 한 건보다 민감한 정보가 많을 수 있습니다. 재연결 토큰이 유효하다는 이유만으로 이전 방의 스냅숏을 보내지 말고, 현재 사용자에게 그 대상의 조회 권한이 남아 있는지 다시 확인해야 합니다.
동시에 수정하는 문제는 별도입니다
이 글은 연결이 끊겨 변경을 놓친 화면을 복구하는 문제에 집중합니다. A와 B가 같은 문서를 동시에 수정하는 경우에는 추가 정책이 필요합니다.
- 서버 도착 순서로 마지막 쓰기 채택
- 예상 버전이 다르면 수정 거절
- 필드별 병합
- OT 또는 CRDT 같은 공동 편집 알고리즘
단순 대시보드에 복잡한 CRDT를 먼저 넣을 필요는 없습니다. 데이터가 읽기 중심인지, 동시 수정이 실제로 가능한지부터 확인하세요.
한쪽 연결을 끊었다 이어 보세요
나쁜 결과와 안전한 결과를 비교하세요
연결이 끊긴 두 상태판을 다시 맞춥니다
보는 순서① 이벤트만 선택→② B 끊기→③ 버전 비교
- 1같은 시작
- 2끊김
- 3변경
- 4불일치
LIVE EVENTS ONLY세 화면이 같은 상태입니다 ✓
이벤트만 전송 시나리오입니다. 실험 시작을 눌러 네 장면을 따라가세요.
화면에서 코드로 옮길 때
이 편의 원리 세 가지
이벤트는 무엇이 바뀌었는지, 상태는 지금 정답이 무엇인지 말합니다.
각 화면은 마지막으로 적용한 서버 버전을 기억합니다.
누락 이벤트를 모두 재생하기 어렵다면 현재 상태로 한 번에 맞춥니다.
구현 체크리스트
최소 네 조각으로 시작하세요.
데모의 시각 요소를 실제 프로토콜 필드와 런타임 동작으로 옮기면 됩니다.
- 1서버 기준 상태클라이언트 표시값과 구분되는 권위 있는 값
- 2증가 버전변경 순서를 비교할 수 있는 revision 또는 offset
- 3재연결 handshake클라이언트의 마지막 적용 버전을 전달
- 4스냅숏 + 이후 이벤트기준 시점이 맞는 두 데이터를 원자적으로 연결
데모만 따로 보려면 전체 화면으로 열기.
- 이벤트만 전송에서 처음에는 모두 v1인지 봅니다.
- B가 오프라인인 동안 서버와 A만 v3이 되는지 확인합니다.
- 재연결만 했을 때 B가 여전히 v1인지 봅니다.
- 버전 + 스냅숏으로 바꿉니다.
- B가
lastVersion=1을 보내고 스냅숏 22도 v3을 받는지 확인합니다.
스냅숏으로 갈아탈 기준을 정해야 합니다
놓친 것만 다시 보낼지, 지금 상태를 통째로 보낼지는 상황에 따라 다릅니다. 그런데 실제로 구현하려면 어느 쪽을 쓸지 판단하는 기준을 숫자로 정해 두어야 합니다.
기준으로 쓸 수 있는 값은 보통 둘입니다. 하나는 뒤처진 버전의 개수이고, 다른 하나는 서버가 이벤트를 보관하는 기간입니다. 놓친 개수가 적으면 그것만 재생하는 편이 가볍고, 개수가 많거나 이미 보관 기간이 지나 이벤트가 남아 있지 않으면 스냅숏 말고는 방법이 없습니다.
여기서 자주 놓치는 경우가 보관 기간이 지난 재연결입니다. 클라이언트는 자기가 마지막으로 본 버전을 들고 돌아오는데, 서버에는 그 시점 이후의 이벤트가 이미 지워지고 없습니다. 이때 서버가 "재생할 것이 없다"고만 답하면 클라이언트는 자기가 최신이라고 오해합니다. 남은 이벤트가 없다는 답과 이미 최신이라는 답은 반드시 구분해서 보내야 하고, 전자라면 스냅숏으로 넘어가야 합니다.
이벤트 보관 기간을 정할 때 기준이 되는 것도 이 지점입니다. 사용자가 흔히 겪는 끊김 시간보다 넉넉히 길게 잡아야, 대부분의 재연결이 가벼운 재생으로 끝납니다.
무엇을 정답으로 삼을 것인가
- 이벤트 스트림만으로는 현재 상태 복구를 항상 보장할 수 없습니다.
- 서버가 기준 상태와 증가 버전을 관리해야 합니다.
- 클라이언트는 마지막 적용 버전을 재연결할 때 보냅니다.
- 누락이 작으면 이벤트 재생, 크면 최신 스냅숏으로 맞춥니다.
다음 편에서는 서버 자체가 여러 대가 됩니다. 다중 서버 소켓 확장에서 한 채팅방이 서버 A와 B로 갈라지는 이유를 확인합니다.