늦게 도착한 과거 좌표는 버리는 편이 낫습니다

순서를 보장하지 않는 WebRTC 위치 채널에서 늦게 도착한 과거 좌표를 폐기하고, 최신 위치 사이를 보간하는 방법을 배웁니다.

먼저 전제를 분명히 하겠습니다. 이 글과 데모는 ordered: false로 설정한 WebRTC DataChannel 위치 채널을 가정합니다. 단일 TCP·WebSocket 연결은 애플리케이션에 전송 순서대로 데이터를 전달하므로, 네트워크 지연만으로 뒤 메시지가 앞 메시지를 추월하지 않습니다.

순서를 보장하지 않는 위치 채널에서 서버가 x30, x50, x70을 보냈다면 x70이 먼저 도착하고 x50이 나중에 도착할 수 있습니다. 받은 좌표를 모두 도착 순서대로 화면에 적용하면 캐릭터는 x70까지 갔다가 갑자기 x50으로 뒤로 움직입니다.

실시간 상태에서는 늦게 도착한 과거 데이터가 없는 데이터보다 나쁠 수 있습니다.

지연 대응 데모에서 도착 순서대로 적용을 먼저 실행하면 화면 위치가 과거로 돌아갑니다. 순서 + 보간 적용에서는 2번 좌표를 버리고 최신 위치까지 부드럽게 이동합니다.

순서를 보장하지 않는 채널에서는 도착 순서가 달라집니다

unordered WebRTC DataChannel은 뒤 메시지가 앞 메시지를 기다리지 않습니다. 패킷마다 지연이 다르거나 오래된 메시지가 재전송되면 애플리케이션이 보는 순서가 발생 순서와 달라질 수 있습니다. 서로 다른 채널·연결·서버의 결과를 하나로 합칠 때도 동일한 문제가 생길 수 있습니다.

서버 발생:  #1 x30 → #2 x50 → #3 x70
화면 도착:  #1 x30 → #3 x70 → #2 x50

좌표만 보면 x50이 새 데이터인지 오래된 데이터인지 판단할 수 없습니다. 그래서 위치 메시지에 증가하는 sequence 또는 서버 tick을 붙입니다.

{
  "type": "POSITION",
  "entityId": "player-4",
  "x": 70,
  "seq": 3
}

마지막 적용 번호보다 작으면 버립니다

클라이언트는 개체별 lastAppliedSeq를 기억합니다.

if message.seq <= lastAppliedSeq:
  drop(message)
else:
  lastAppliedSeq = message.seq
  apply(message)

데모에서 #3을 적용한 뒤 lastAppliedSeq는 3입니다. 뒤늦게 #2가 오면 2 ≤ 3이므로 폐기합니다. 이 정책은 자주 새 값이 오는 위치, 조준 방향, 임시 센서 수치에 잘 맞습니다.

모든 메시지를 버려도 되는 것은 아닙니다

데이터의 의미에 따라 채널 정책을 분리해야 합니다.

데이터오래된 값의 가치일반적인 처리
플레이어 위치최신 값이 곧 덮어씀오래된 좌표 폐기
조준 방향·커서최신 상태가 중요일부 유실 허용
채팅 메시지각 메시지가 독립적으로 중요신뢰·순서 보장
아이템 획득·결제부작용과 기록이 중요ACK·재시도·중복 제거

하나의 소켓 연결에서도 메시지 종류별 애플리케이션 규칙을 다르게 둘 수 있습니다. WebRTC DataChannel을 사용하면 신뢰 또는 부분 신뢰, 순서 있음 또는 순서 없음을 채널별로 선택할 수도 있습니다.

반대로 WebSocket·Socket.IO 한 연결에서 순서가 뒤집히는 예제로 이 데모를 해석하면 안 됩니다. 그 환경에서도 sequence는 재연결 경계의 오래된 상태, 여러 생산자의 갱신, 중복 메시지와 애플리케이션 비동기 처리 순서를 판별하는 데 유용합니다.

최신 좌표를 바로 점프시키면 화면이 떨립니다

오래된 좌표를 버렸다고 화면이 곧바로 부드러워지는 것은 아닙니다. 새 좌표가 불규칙한 간격으로 도착하면 캐릭터가 한 칸씩 순간 이동해 보입니다.

표시 위치와 네트워크의 최신 위치를 분리합니다.

논리 위치: 서버에서 받은 최신 x70
표시 위치: 현재 x58 → x62 → x66 → x70

짧은 시간 동안 두 점 사이를 잇는 것을 보간(interpolation)이라고 합니다. 약간 늦게 보여 주는 대신 움직임을 일정하게 만듭니다.

작은 보간 버퍼가 지연 흔들림을 흡수합니다

클라이언트가 아주 짧은 시간의 위치 샘플을 모아 과거의 두 확정 지점 사이를 그리면, 도착 간격이 조금 흔들려도 화면 속도는 안정됩니다.

  • 버퍼가 너무 작으면 네트워크 지터를 흡수하지 못합니다.
  • 너무 크면 조작 반응이 늦게 보입니다.
  • 경쟁 게임과 관제 지도는 허용 가능한 지연이 다릅니다.
  • 실제 네트워크의 지연 분포를 측정해 크기를 조정합니다.

로컬 플레이어는 입력 예측으로 먼저 움직이고 서버 응답 뒤 보정할 수도 있습니다. 이 글의 보간은 주로 다른 플레이어나 원격 객체를 보여 주는 가장 기초적인 방법입니다.

순간 이동은 보간하지 않을 수 있습니다

모든 위치 차이를 천천히 잇는 것도 잘못입니다. 포털 이동, 리스폰, 큰 오차 보정은 중간 경로가 의미 없을 수 있습니다.

거리 < 작은 임계값  → 보간
거리 ≥ 큰 임계값    → 즉시 스냅
메시지 type=TELEPORT → 즉시 스냅

프로토콜에 이벤트 의미를 넣으면 화면이 숫자 차이만 보고 잘못 추측하지 않아도 됩니다.

시계 대신 sequence부터 시작하세요

서로 다른 기기의 절대 시각은 오차가 있을 수 있습니다. 단순히 “더 큰 timestamp가 최신”이라고 가정하면 기기 시계 차이 때문에 순서가 틀어질 수 있습니다.

한 서버가 상태를 확정하는 구조라면 먼저 서버가 증가시키는 tick이나 entity별 sequence를 사용하는 편이 쉽습니다. timestamp는 지연 측정과 보간 시점 계산에 추가로 사용할 수 있습니다.

지연을 올려 보세요

나쁜 결과와 안전한 결과를 비교하세요

unordered WebRTC 위치 패킷을 재생합니다

보는 순서비순서 채널 확인#3 먼저 수신#2 지연 확인

  1. 1이동
  2. 2역전
  3. 3과거 도착
  4. 4떨림
실험 시작을 누르면 첫 장면부터 움직입니다비순서 위치 채널
#1#2#3lastApplied #1
STARTGOAL
서버 최신 x70
화면 x30
도착 패킷#1처리최신 적용화면 이동즉시 점프

도착 순서대로 적용 시나리오입니다. 실험 시작을 눌러 네 장면을 따라가세요.

시작 전

화면에서 코드로 옮길 때

이 편의 원리 세 가지

01위치에는 시간이 있습니다.

좌표만 보내지 말고 sequence나 server tick을 함께 보냅니다.

02오래된 상태는 폐기합니다.

마지막 적용 번호보다 작은 패킷으로 현재 화면을 되돌리지 않습니다.

03표시와 논리를 나눕니다.

논리는 최신 좌표를 유지하고 화면은 짧게 보간해 부드럽게 이동합니다.

구현 체크리스트

최소 네 조각으로 시작하세요.

데모의 시각 요소를 실제 프로토콜 필드와 런타임 동작으로 옮기면 됩니다.

  1. 1서버 tick모든 위치 업데이트의 비교 가능한 순서
  2. 2stale 판정seq가 lastApplied 이하이면 즉시 폐기
  3. 3작은 보간 버퍼짧은 지연을 감수하고 두 좌표 사이를 연결
  4. 4중요 이벤트 분리결제·아이템 획득은 신뢰 채널과 ACK 사용
이 데모는 네트워크 동작을 단계별로 보여주는 개념 시뮬레이션입니다.

이 시나리오는 ordered: false인 WebRTC DataChannel을 가정합니다. 단일 TCP·WebSocket 연결은 전송 순서를 보존하며, 중요한 게임 이벤트에는 신뢰 채널과 별도 ACK를 사용하세요.

데모만 따로 보려면 전체 화면으로 열기.

  1. ordered: false 위치 채널을 가정한 도착 순서대로 적용에서 #3 x70이 먼저 도착하는지 봅니다.
  2. 뒤늦은 #2 x50을 적용해 화면이 뒤로 이동하는지 확인합니다.
  3. 순서 + 보간 적용으로 바꿉니다.
  4. #3을 받은 뒤 표시 위치가 x58, x66처럼 목표로 이동하는지 봅니다.
  5. #2가 도착해도 #2 < #3이라 폐기되고 최종 x70을 유지하는지 확인합니다.

번호가 한 바퀴 돌면 생기는 문제

순서 번호로 오래된 좌표를 걸러 내는 방식에는 잘 드러나지 않는 함정이 하나 있습니다. 번호가 무한히 커질 수 없다는 점입니다.

번호를 정해진 크기의 정수에 담으면 언젠가 최댓값을 넘어 0 으로 돌아옵니다. 그 순간 새로 온 좌표의 번호가 마지막으로 적용한 번호보다 작아지므로, 단순히 크기만 비교하는 코드는 정상적인 최신 좌표를 오래된 것으로 보고 버립니다. 그때부터 화면이 멈춘 것처럼 보입니다.

초당 30 번씩 좌표를 보낸다고 하면 16 비트 번호는 대략 반나절 만에 한 바퀴를 돕니다. 짧은 대전 한 판만 시험해 보고 넘어가면 개발 중에는 절대 마주치지 않는 종류의 문제입니다.

대응은 둘 중 하나입니다. 번호를 충분히 큰 정수로 두어 서비스 수명 안에 한 바퀴가 돌지 않게 하거나, 두 번호의 차이가 절반을 넘으면 한 바퀴 돈 것으로 보는 비교 방식을 쓰는 것입니다. 뒤쪽은 오래된 통신 규약들이 오래전부터 쓰던 방법이고, 번호를 아껴야 할 때 선택합니다.

응용 시리즈 전체 요약

이번 5부작은 소켓 API보다 그 위의 애플리케이션 규칙을 다뤘습니다.

  1. 전달 보장 — ACK와 메시지 ID로 중복 부작용을 막습니다.
  2. 흐름 제어 — 느린 수신자 앞에서 멈추고 이어받습니다.
  3. 상태 동기화 — 버전과 스냅숏으로 현재 정답을 맞춥니다.
  4. 서버 확장 — 브로커와 저장소로 서버 사이를 잇습니다.
  5. 지연 대응 — 과거 상태를 버리고 최신 상태를 부드럽게 표시합니다.

소켓은 통로를 만들어 줍니다. 그 통로 위에서 무엇을 재시도하고, 얼마나 쌓아 두고, 어떤 상태를 정답으로 삼고, 어디까지 공유하고, 무엇을 버릴지는 애플리케이션이 결정합니다.

확인한 자료


관련 글