서버 Authoritative 이동 보정과 텔레포트 핵 검증 구현 가이드

서버 authoritative 멀티플레이에서 클라이언트 이동 예측과 서버 보정을 안전하게 결합하고 속도·거리·지연 시간을 기준으로 텔레포트 핵을 검증하는 실전 설계 방법을 정리합니다.

✓ 복사 완료!
챕터일반 가이드
난이도중급
상태● 검토 완료
대상 엔진
Engine Agnostic
최종 업데이트
서버 Authoritative 이동 보정과 텔레포트 핵 검증 구현 가이드

서버 authoritative 구조에서는 클라이언트가 보낸 좌표를 그대로 신뢰하지 않고 서버가 입력과 물리 규칙으로 최종 위치를 결정한다. 이동 보정은 네트워크 지연으로 인한 체감 문제를 줄이고 텔레포트 핵 검증은 허용 가능한 시간 동안 실제로 이동할 수 있는 거리인지 판정하는 방식으로 구현한다.

서버 authoritative 이동 구조란 무엇인가?

서버 authoritative는 월드의 정답 상태를 게임 서버가 보유하는 구조다. 클라이언트는 현재 위치보다 이동 입력을 전송하고 서버는 해당 입력이 허용되는지 검증한 뒤 시뮬레이션 결과를 모든 클라이언트에 복제한다.

구분클라이언트 신뢰 구조서버 authoritative 구조
최종 위치 결정클라이언트서버
텔레포트 핵 대응사후 탐지 중심입력 단계에서 차단 가능
조작 난이도낮음상대적으로 높음
지연 체감낮게 보일 수 있음예측과 보정이 필요

클라이언트가 매 프레임 좌표를 보내는 방식은 구현은 빠르지만 위험하다. 공격자는 패킷의 position 값을 바꾸거나 전송 빈도를 조작해 벽 통과, 순간 이동, 속도 증가를 만들 수 있다. 따라서 서버는 MoveInput처럼 방향, 버튼 상태, 입력 시퀀스 번호를 받아 직접 위치를 계산하는 편이 안전하다.

이동 보정은 왜 발생하는가?

클라이언트와 서버는 같은 시간에 같은 상태를 보지 못한다. 왕복 지연 시간(RTT)이 80ms라면 클라이언트가 입력한 이동은 서버에 도착하기까지 시간이 걸리고 서버의 결과도 다시 클라이언트에 돌아와야 한다.

클라이언트 예측(client-side prediction)은 입력 직후 로컬 캐릭터를 먼저 움직여 조작감을 유지한다. 이후 서버 스냅샷의 권위 위치가 예측 위치와 다르면 클라이언트는 서버 상태로 되돌린 뒤 아직 서버가 처리하지 않은 입력을 다시 적용한다. 이것이 이동 보정(reconciliation)이다.

flowchart LR
    A[클라이언트 입력 생성] --> B[로컬 예측 이동]
    A --> C[입력 시퀀스와 함께 서버 전송]
    C --> D[서버 입력 검증 및 시뮬레이션]
    D --> E[권위 상태와 처리 시퀀스 전송]
    E --> F[클라이언트 상태 일치 여부 확인]
    F -->|불일치| G[서버 상태 복원 후 미확정 입력 재실행]
    F -->|일치| H[다음 입력 처리]
    G --> H

예측 오차가 작으면 화면상 위치를 짧은 시간에 보간해 부드럽게 보정할 수 있다. 단, 충돌 판정 실패나 큰 위치 차이처럼 게임 규칙에 영향을 주는 오차는 즉시 보정해야 한다.

서버 authoritative 이동 보정은 어떻게 구현할까?

1. 좌표 대신 입력을 전송한다

입력 패킷에는 이동 방향, 점프 여부, 클라이언트 입력 번호, 클라이언트 추정 틱을 담는다. 서버가 받은 순서대로 처리할 수 있도록 sequence은 단조 증가해야 한다.

interface MoveInput {
  sequence: number;
  clientTick: number;
  moveX: number; // -1.0 ~ 1.0
  moveZ: number; // -1.0 ~ 1.0
  jumpPressed: boolean;
}

interface PlayerState {
  position: { x: number; y: number; z: number };
  velocity: { x: number; y: number; z: number };
  lastProcessedSequence: number;
}

서버는 입력 범위를 먼저 제한한다. 예를 들어 moveX, moveZ를 각각 -1과 1 사이로 clamp하고 대각선 입력 벡터는 정규화한다. 그렇지 않으면 (1, 1) 입력이 축 이동보다 약 1.414배 빨라진다.

∥d∥=dx2+dz2\lVert \mathbf{d} \rVert = \sqrt{d_x^2 + d_z^2}

2. 고정 서버 틱으로 이동과 충돌을 계산한다

서버 틱률을 30Hz로 정했다면 한 틱의 시간은 다음과 같다.

Δt=130 seconds\Delta t = \frac{1}{30}\text{ seconds}

최대 이동 속도가 vMax = 6m/s이면 충돌과 감속을 무시한 한 틱의 최대 수평 이동 거리는 다음과 같다.

dtick,max=vmax×Δt=6×130=0.2md_{tick,max} = v_{max} \times \Delta t = 6 \times \frac{1}{30} = 0.2m

실제 구현에서는 이동 벡터, 가속도, 중력, 지면 여부, 캡슐 충돌을 모두 서버 시뮬레이션에 포함한다. 클라이언트도 같은 규칙을 예측용으로 실행할 수 있지만 최종 판정은 항상 서버 결과를 사용한다.

function simulateMove(state: PlayerState, input: MoveInput, dt: number) {
  const direction = normalizeOrZero({ x: input.moveX, z: input.moveZ });
  const targetSpeed = 6.0;

  state.velocity.x = moveToward(state.velocity.x, direction.x * targetSpeed, 24.0 * dt);
  state.velocity.z = moveToward(state.velocity.z, direction.z * targetSpeed, 24.0 * dt);

  if (input.jumpPressed && isGrounded(state.position)) {
    state.velocity.y = 5.5;
  }

  state.velocity.y += -9.81 * dt;
  state.position = moveWithCapsuleCollision(state.position, state.velocity, dt);
  state.lastProcessedSequence = input.sequence;
}

3. 처리 완료 입력 번호와 권위 상태를 반환한다

서버 스냅샷에는 위치만 넣지 말고 lastProcessedSequence을 반드시 넣는다. 클라이언트는 이 번호보다 큰 로컬 입력만 재실행해야 이미 서버가 계산한 입력을 중복 적용하지 않는다.

interface AuthoritativeSnapshot {
  serverTick: number;
  position: { x: number; y: number; z: number };
  velocity: { x: number; y: number; z: number };
  lastProcessedSequence: number;
}

function reconcile(snapshot: AuthoritativeSnapshot) {
  predictedState = restore(snapshot);
  pendingInputs = pendingInputs.filter(
    (input) => input.sequence > snapshot.lastProcessedSequence,
  );

  for (const input of pendingInputs) {
    simulateMove(predictedState, input, CLIENT_FIXED_DT);
  }
}

서버 권위 위치와 클라이언트 예측 위치의 차이를 보정하는 과정

텔레포트 핵은 어떤 기준으로 검증해야 하는가?

텔레포트 핵 검증은 단순히 두 위치의 거리가 멀다는 이유만으로 차단하면 오탐이 생긴다. 서버 틱 지연, 패킷 묶음, 재접속, 이동 스킬, 서버가 허용한 워프를 구분해야 한다.

권장 방식은 서버가 마지막으로 승인한 상태를 기준으로 경과 시간 동안 가능한 최대 이동 거리와 실제 이동 거리를 비교하는 것이다.

dactual=∥pnew−papproved∥d_{actual} = \lVert \mathbf{p}_{new} - \mathbf{p}_{approved} \rVert dallowed=vmax×Δt+12amaxΔt2+mnetworkd_{allowed} = v_{max} \times \Delta t + \frac{1}{2}a_{max}\Delta t^2 + m_{network}

여기서 mNetwork는 지연과 부동소수점 오차를 위한 작은 허용치다. 이 값은 고정된 큰 숫자보다 서버 틱 간격과 관측된 지연을 반영해 제한적으로 산정하는 편이 좋다. 예를 들어 vMax = 6m/s, aMax = 24m/s², Δt = 0.1s, mNetwork = 0.15m일 때 허용 거리는 약 0.87m다.

검증 항목확인 방법실패 시 처리
입력 순서sequence이 마지막 처리 번호보다 큰지 확인중복 또는 오래된 입력 폐기
입력 빈도사용자별 초당 입력 수 제한초과 패킷 무시 및 로그
입력 범위축 값, 카메라 각도, 버튼 조합 검증clamp 또는 폐기
속도와 거리시간 기반 dAllowed와 실제 이동 거리 비교서버 위치로 보정
충돌 경로시작점부터 목표점까지 sweep test벽 앞 위치로 이동
합법적 순간 이동스킬, 포털, 운영자 명령 토큰 확인검증된 경우만 허용

텔레포트 핵 검증을 3단계로 설계하기

1. 서버가 승인한 이동 경로만 사용한다

가장 강력한 방어는 클라이언트 위치 보고 자체를 게임 상태 갱신에 사용하지 않는 것이다. 클라이언트가 좌표를 보내야 하는 구조라면 그 값은 판정의 근거가 아니라 이상 징후 탐지용 관측값으로 취급한다.

2. 시간, 속도, 충돌을 함께 검사한다

거리만 비교하면 순간 가속이나 벽 통과를 놓친다. 서버는 마지막 승인 시각부터의 경과 시간, 현재 버프가 적용된 최대 속도, 가속 제한, 지형 충돌을 함께 적용해야 한다. 이동 스킬은 일반 이동 검증을 우회하는 것이 아니라 서버가 발급한 movementOverrideId 또는 스킬 실행 상태로 명시적으로 허용한다.

function validateReportedPosition(
  approved: { position: Vec3; serverTimeMs: number },
  reportedPosition: Vec3,
  nowMs: number,
  maxSpeed: number,
  maxAcceleration: number,
) {
  const elapsed = Math.min((nowMs - approved.serverTimeMs) / 1000, 0.25);
  const allowedDistance =
    maxSpeed * elapsed + 0.5 * maxAcceleration * elapsed * elapsed + 0.15;
  const actualDistance = distance(approved.position, reportedPosition);

  return actualDistance <= allowedDistance &&
    hasWalkableCollisionPath(approved.position, reportedPosition);
}

elapsed에 상한을 두는 이유는 오래 멈췄다가 보낸 패킷 하나로 긴 거리를 합법화하지 못하게 하기 위해서다. 단, 패킷 손실이 심한 환경에서 정상 사용자가 반복 보정되지 않도록 입력 버퍼와 서버 시뮬레이션을 우선 사용해야 한다.

3. 즉시 영구 제재하지 말고 증거를 누적한다

단일 검증 실패는 패킷 순서 역전, 서버 부하, 상태 동기화 오류에서도 발생할 수 있다. 첫 실패에는 서버 권위 위치로 되돌리고 의심 점수(suspicion score)를 올린 뒤 짧은 시간 창에서 반복 여부를 판단한다.

if (!isValidMove) {
  player.suspicionScore += 1;
  sendCorrection(player, authoritativeState);
  logMovementViolation(player.id, actualDistance, allowedDistance);
}

if (player.suspicionScore >= 5 && violationsWithin(player.id, 60_000) >= 5) {
  restrictMatchmaking(player.id, "movement_validation_failed");
}

제재 판단에는 서버 틱, 입력 시퀀스, 승인 위치, 보고 위치, RTT, 허용 거리, 충돌 결과를 함께 남긴다. 재현 가능한 로그가 있어야 오탐을 줄이고 탐지 규칙을 조정할 수 있다.

이동 보정에서 피해야 할 구현 실수

  • 서버가 클라이언트 위치를 그대로 저장한다. 서버 authoritative의 핵심 보안 경계가 사라진다.
  • 보정 오차를 항상 즉시 스냅한다. 작은 오차까지 순간 이동처럼 보여 조작감이 나빠진다.
  • 보정을 전부 보간한다. 벽 통과나 낙하 판정처럼 게임 규칙을 깨는 오차가 화면에 남는다.
  • RTT를 이유로 무제한 이동 거리를 허용한다. 지연은 제한된 허용치의 근거일 뿐, 큰 거리 이동의 면책 사유가 아니다.
  • 이동 스킬을 클라이언트 플래그 하나로 허용한다. 스킬 시작과 종료, 쿨다운, 목표 지점을 서버 상태에서 검증해야 한다.

자주 묻는 질문 (FAQ)

클라이언트 예측을 사용하면 서버 authoritative가 약해지지 않는가?

약해지지 않는다. 예측은 로컬 화면 반응을 위한 임시 결과일 뿐이며 충돌과 최종 위치는 서버 스냅샷으로 확정한다. 클라이언트 예측 결과를 서버가 신뢰하지 않는 것이 핵심이다.

RTT가 높은 사용자는 텔레포트 핵으로 오탐되지 않는가?

입력 기반 서버 시뮬레이션을 사용하면 RTT가 높아도 서버는 도착한 입력을 자신의 틱에서 처리한다. 좌표 보고를 추가 검증할 경우에는 작은 지연 허용치를 더하되 경과 시간과 허용 거리의 상한을 반드시 둔다.

정상적인 포털과 대시 스킬은 어떻게 구분하는가?

클라이언트가 선언한 값이 아니라 서버가 관리하는 스킬 상태와 포털 트리거로 구분한다. 서버가 유효한 teleportId 또는 이동 오버라이드 상태를 가진 경우에만 일반 거리 검증을 예외 처리한다.

정리

안전한 멀티플레이 이동은 클라이언트가 좌표를 제출하고 서버가 승인하는 방식보다 클라이언트가 입력을 보내고 서버가 직접 시뮬레이션하는 방식에 가깝다. 여기에 입력 시퀀스 기반 재조정, 시간·속도·충돌을 결합한 검증, 반복 위반에 대한 증거 기반 대응을 더하면 텔레포트 핵과 속도 핵에 강한 이동 시스템을 만들 수 있다.