
네트워크 예측과 랙 보정: 조작감과 공정성을 함께 지키는 방법
클라이언트 예측, 서버 권위, 상태 재조정, 서버 측 히트 판정을 연결해 실시간 멀티플레이의 지연을 다루는 방법을 정리합니다.
왜 입력을 서버 응답까지 기다리면 안 될까
실시간 게임에서 클라이언트가 입력을 서버에 보내고 서버의 결과를 받은 뒤에만 화면을 갱신하면 조작감은 왕복 지연 시간만큼 늦어진다. 왕복 지연이 100ms라면 점프, 이동, 발사 모두 최소 100ms 뒤에 반응한다.
이를 해결하는 기본 원칙은 두 가지다.
- 플레이어 자신이 조작하는 대상은 클라이언트가 먼저 움직인다.
- 최종 판정과 게임 상태의 권한은 서버가 가진다.
첫 번째가 클라이언트 예측(Client Prediction)이고 두 번째를 보완하는 과정이 서버 재조정(Server Reconciliation)이다. 여기에 총알이나 근접 공격처럼 즉시 판정이 필요한 경우에는 랙 보정(Lag Compensation)을 더한다.

전체 흐름
flowchart LR
A[클라이언트 입력 생성] --> B[로컬에서 즉시 시뮬레이션]
A --> C[입력 번호와 함께 서버 전송]
C --> D[서버 권위 시뮬레이션]
D --> E[권위 상태와 처리한 입력 번호 전송]
E --> F[클라이언트 상태 교체]
F --> G[아직 확인되지 않은 입력 재실행]
G --> B
클라이언트는 입력마다 증가하는 번호를 붙인다. 서버는 입력을 순서대로 처리하고 응답할 때 마지막으로 처리한 입력 번호를 함께 보낸다. 클라이언트는 그 번호보다 오래된 입력을 버린 뒤 아직 서버가 확인하지 않은 입력만 다시 적용한다.
클라이언트 예측
예측은 서버의 결과를 기다리지 않고 로컬 시뮬레이션을 먼저 수행하는 방식이다. 단순히 화면 위치만 앞당겨 바꾸는 것보다 서버와 가능한 한 같은 이동 규칙을 실행해야 한다.
예를 들어 고정 시간 간격을 , 속도를 , 이동 방향을 정규화한 벡터를 라고 하면 가장 단순한 적분은 다음과 같다.
실제 게임에서는 가속도, 감속, 중력, 충돌, 경사면, 점프 상태까지 포함한다. 클라이언트와 서버의 계산이 조금만 달라도 시간이 지날수록 결과가 어긋난다. 따라서 이동 코드는 공유 가능한 C# 라이브러리나 C++ 모듈로 분리하는 편이 안전하다.
public struct PlayerInput
{
public uint Sequence;
public Vector2 Move;
public bool Jump;
}
public static PlayerState Simulate(PlayerState state, PlayerInput input, float dt)
{
var direction = Vector3.ClampMagnitude(
new Vector3(input.Move.x, 0f, input.Move.y), 1f);
state.Velocity.x = direction.x * state.MoveSpeed;
state.Velocity.z = direction.z * state.MoveSpeed;
if (input.Jump && state.IsGrounded)
state.Velocity.y = state.JumpSpeed;
state.Velocity += Physics.gravity * dt;
state.Position += state.Velocity * dt;
return state;
}
위 코드는 개념 설명용이다. Physics.Raycast나 물리 엔진의 실제 충돌 결과에 직접 의존하면 클라이언트와 서버에서 동일한 결과를 재현하기 어렵다. 경쟁성이 중요한 게임이라면 예측 가능한 캐릭터 컨트롤러를 별도로 두거나 서버의 결과를 더 자주 받아 오차를 빠르게 줄이는 방식을 검토해야 한다.
서버 재조정과 입력 재실행
서버는 클라이언트가 보낸 위치를 그대로 신뢰하지 않는다. 입력만 받고 권위 시뮬레이션을 수행한 뒤 다음 정보를 돌려준다.
- 서버가 계산한 위치와 속도
- 마지막으로 처리한 입력 번호
- 해당 상태의 서버 틱
클라이언트는 받은 상태를 기준점으로 삼는다. 그 뒤 서버가 아직 처리하지 않은 입력을 저장된 순서대로 다시 시뮬레이션한다.
void ApplyAuthoritativeState(PlayerState serverState, uint acknowledgedSequence)
{
predictedState = serverState;
pendingInputs.RemoveAll(input => input.Sequence <= acknowledgedSequence);
foreach (var input in pendingInputs)
predictedState = Simulate(predictedState, input, fixedDeltaTime);
}
상태를 즉시 교체하면 오차가 클 때 캐릭터가 순간이동하듯 보일 수 있다. 게임 화면에는 재조정된 논리 상태를 그대로 쓰되 렌더링 위치만 짧은 시간 동안 보간해 차이를 흡수할 수 있다. 다만 보간은 시각적 완화일 뿐이며 충돌과 판정에 쓰는 실제 상태를 숨기면 안 된다.
랙 보정은 무엇을 되돌리는가
클라이언트 예측은 주로 내 캐릭터의 이동 반응을 개선한다. 반면 랙 보정은 발사 시점에 맞는 피격 판정을 위해 사용한다.
예를 들어 플레이어 A가 120ms 지연 환경에서 움직이는 플레이어 B를 보고 발사했다고 하자. 서버가 발사 요청을 받았을 때 B는 이미 더 앞으로 이동해 있을 수 있다. 서버가 현재 위치만으로 판정하면 A의 화면에서 맞은 공격도 빗나간 것으로 처리될 수 있다.
서버는 각 플레이어의 과거 충돌 정보와 서버 틱을 짧게 저장한다. 발사 요청에는 클라이언트가 공격을 본 시점 또는 서버 시간으로 환산 가능한 시간이 포함된다. 서버는 그 시점의 B 충돌체를 복원하거나 과거 상태를 대상으로 레이캐스트를 수행해 명중을 판정한다.
여기서 는 되감기를 허용하는 최대 시간이다. 이 제한이 없으면 매우 높은 지연을 악용해 지나치게 오래된 위치를 공격 판정에 사용할 수 있다.
서버 측 히트 판정의 구현 포인트
과거 상태는 전체 월드를 복제할 필요가 없다. 보통 플레이어별 히트박스의 위치, 회전, 자세 상태를 틱 단위로 기록한다. 기록 길이는 게임의 목표 네트워크 환경과 최대 허용 지연에 맞춰 정한다.
struct HitboxSnapshot
{
uint32_t tick;
Vector3 position;
Quaternion rotation;
Stance stance;
};
bool ValidateShot(const ShotRequest& shot)
{
uint32_t targetTick = ClampToHistory(shot.estimatedServerTick);
ScopedHitboxRewind rewind(targetTick);
Ray ray = MakeServerValidatedRay(shot);
return RaycastPlayers(ray, shot.maxDistance);
}
ScopedHitboxRewind는 판정이 끝난 뒤 반드시 현재 상태로 되돌려야 한다. 판정 중인 과거 충돌체가 다른 시스템의 현재 충돌, 시각 효과, AI 감지에 섞이지 않도록 서버 내부 처리 순서를 분리하는 것도 중요하다.
발사 방향과 원점도 검증 대상이다. 서버는 플레이어의 현재 또는 허용 범위 안의 과거 무기 위치를 기준으로 발사선을 구성해야 한다. 클라이언트가 보낸 명중 대상이나 피해량을 그대로 받으면 랙 보정과 무관하게 치팅에 취약해진다.
자주 생기는 문제와 대응
입력이 순서대로 도착하지 않는 문제
UDP 기반 전송에서는 입력 패킷이 늦게 도착하거나 순서가 바뀔 수 있다. 입력 번호를 두고 이미 처리한 번호 이하의 입력은 무시한다. 중요한 입력을 한 번만 보내지 않고 최근 입력 몇 개를 함께 실어 보내는 중복 전송도 실용적이다.
프레임 시간 차이로 인한 누적 오차
예측과 서버 시뮬레이션은 렌더링 프레임이 아니라 고정 네트워크 틱으로 실행한다. 틱 간격과 이동 상수의 단위를 명확히 통일하고 부동소수점 오차가 민감한 게임에서는 결정론적 계산 또는 서버 상태의 잦은 재조정을 고려한다.
다른 플레이어가 끊겨 보이는 문제
원격 플레이어에는 보통 예측보다 보간을 사용한다. 최근 두 서버 스냅샷 사이를 약간 과거 시점에서 재생하면 패킷 간격의 흔들림을 줄일 수 있다. 반대로 너무 오래 과거를 보여 주면 상대 움직임이 둔하게 느껴진다.
재조정이 너무 잦은 문제
오차가 반복된다면 보간 시간을 늘리기 전에 원인을 확인해야 한다. 클라이언트와 서버의 충돌 규칙 차이, 틱 불일치, 입력 적용 순서, 이동 상수 불일치가 흔한 원인이다. 보간만 강화하면 문제를 보기 어렵게 만들 뿐 권위 상태와 예측 상태의 불일치는 남는다.
적용 순서
처음부터 모든 시스템에 예측과 랙 보정을 넣을 필요는 없다. 다음 순서가 구현과 디버깅에 유리하다.
- 서버 권위 이동과 입력 번호를 먼저 만든다.
- 내 캐릭터에 클라이언트 예측과 재조정을 적용한다.
- 원격 캐릭터에는 스냅샷 보간을 적용한다.
- 총기처럼 체감 차이가 큰 판정에 제한된 시간 범위의 랙 보정을 추가한다.
- 패킷 손실, 높은 지연, 순서 변경을 인위적으로 만들어 검증한다.
핵심은 지연을 없애는 것이 아니라 지연이 있어도 일관된 규칙으로 보이게 만드는 데 있다. 내 조작은 즉시 반응해야 하고 결과는 서버가 확정해야 하며 공격 판정은 플레이어가 실제로 보았던 시간에 가능한 한 공정하게 맞춰야 한다.


