> For the complete documentation index, see [llms.txt](https://docs.overdare.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.overdare.com/korean/manual/script-manual/events-and-communication/remoteevent.md).

# 서버-클라이언트 통신

## 개요

OVERDARE의 월드는 서버와 클라이언트 간의 통신을 기반으로 동작합니다.

<div align="left"><figure><img src="https://2697870212-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRPi87oM9ttlk5nyu7L7%2Fuploads%2Fgit-blob-f1a6c72cbf4ac2db35611cbc1f3d451792c643fb%2FGroup%2011.png?alt=media" alt=""><figcaption></figcaption></figure></div>

* **서버**는 게임의 전역 상태를 관리하며, 모든 클라이언트(플레이어)와의 통신을 처리하는 **중앙 시스템** 역할을 합니다.
* **클라이언트**는 개별 플레이어의 디바이스에서 실행되는 **로컬 환경**으로, 플레이어의 조작, 시각적 효과, UI 등을 처리합니다.

서버와 클라이언트는 독립적으로 동작하는 환경이기 때문에, 멀티플레이 게임에서 **게임 로직**, **카메라 제어**, **플레이어 입력 처리** 등의 기능을 구현하고 통신하기 위해서는 **RemoteEvent** 또는 **RemoteFunction**을 활용해야 합니다.

## 통신 종류

서버와 클라이언트는 각각 사용할 수 있는 기능이 다르기 때문에, 서로 간의 상호작용을 구현하려면 RemoteEvent나 RemoteFunction을 이용한 통신이 필요합니다.

예를 들어, 버튼과 같은 GUI 요소는 클라이언트에서만 처리되며, 게임 로직은 서버에서 처리해야 합니다. 즉, 클라이언트에서 스킬 버튼 클릭이 발생했을 때, 서버로 스킬 사용에 대한 이벤트를 발송하여 서버가 해당 로직을 처리하도록 요청해야 합니다.

이 오브젝트들은 서버와 클라이언트 간의 역할을 연결해주며, 멀티플레이 게임에서 핵심적인 상호작용을 가능하게 합니다.

<table><thead><tr><th width="248">통신 종류</th><th width="88">발송</th><th width="88">수신</th><th width="130">사용 오브젝트</th><th>예시</th></tr></thead><tbody><tr><td>서버에서 모든 클라이언트에게 이벤트 발송</td><td>Server</td><td>Client</td><td>RemoteEvent</td><td>게임 종료</td></tr><tr><td>서버에서 특정 클라이언트에게 이벤트 발송</td><td>Server</td><td>Client</td><td>RemoteEvent</td><td>레벨업시 레벨업 UI 표시</td></tr><tr><td>클라이언트에서 서버로 이벤트 발송</td><td>Client</td><td>Server</td><td>RemoteEvent</td><td>스킬 버튼 클릭</td></tr><tr><td>클라이언트에서 서버로 요청하고 결과를 돌려받음</td><td>Client</td><td>Server</td><td>RemoteFunction</td><td>아이템 구매 성공 여부와 잔액 조회</td></tr><tr><td>서버에서 클라이언트로 요청하고 결과를 돌려받음 (지양)</td><td>Server</td><td>Client</td><td>RemoteFunction</td><td>아래 <strong>주의 사항</strong> 참고</td></tr></tbody></table>

서로 다른 플레이어의 클라이언트끼리는 직접 통신할 수 없습니다. 클라이언트 A의 값을 클라이언트 B에게 전달하려면 **클라이언트 A ➡ 서버 ➡ 클라이언트 B** 순서로 서버가 중계해야 하며, 이때도 서버에서 값을 검증해야 합니다.

## RemoteEvent와 RemoteFunction 오브젝트

**RemoteEvent**는 서버와 클라이언트 간의 이벤트를 처리하기 위해 제공되는 오브젝트로 **단방향 통신**을 지원합니다. 발송한 쪽은 멈추지 않고 바로 다음 줄로 넘어갑니다.

**RemoteFunction**은 서버와 클라이언트 간의 요청과 응답을 처리하기 위해 제공되는 오브젝트로 **양방향 통신**을 지원합니다. 호출한 쪽은 결과가 도착할 때까지 **대기(yield)**&#xD569;니다. 아이템 구매 결과나 서버가 가진 데이터 조회처럼 **응답을 받아야 다음 동작을 결정할 수 있는 경우**에 사용합니다.

<table><thead><tr><th width="180">항목</th><th width="240">RemoteEvent</th><th>RemoteFunction</th></tr></thead><tbody><tr><td>통신 방향</td><td>단방향 (요청만)</td><td>양방향 (요청과 응답)</td></tr><tr><td>발송 측 동작</td><td>멈추지 않고 계속 실행</td><td>반환값이 올 때까지 대기</td></tr><tr><td>연결 방식</td><td><code>OnServerEvent:Connect(함수)</code></td><td><code>OnServerInvoke = 함수</code> — 하나만 설정 가능</td></tr><tr><td>반환값</td><td>받을 수 없음</td><td><code>Tuple</code>로 여러 값 수신</td></tr><tr><td>여러 클라이언트에 전송</td><td><code>FireAllClients</code> 지원</td><td>지원하지 않음</td></tr></tbody></table>

결과를 받을 필요가 없는 **통보성 통신에는 RemoteEvent를 사용하세요.** 동기 함수는 호출한 쪽을 멈추게 하므로, 서버 처리가 길어지면 해당 클라이언트의 입력 처리나 UI 동작이 함께 지연됩니다.

두 오브젝트 모두 **서버와 클라이언트 양쪽에서 접근**할 수 있어야 합니다. 이를 위해 서버와 클라이언트가 데이터를 공유할 수 있는 스토리지인 **ReplicatedStorage**에 배치됩니다. ReplicatedStorage는 서버와 클라이언트 간에 오브젝트를 안전하게 동기화하며, 두 환경에서 모두 접근 가능하도록 보장합니다. 서버 전용 저장소처럼 한쪽만 볼 수 있는 위치로 옮기면 반대편 스크립트가 참조하지 못합니다.

<img src="https://2697870212-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRPi87oM9ttlk5nyu7L7%2Fuploads%2Fgit-blob-b6834cd8073dc06839a7fc060ef0191775c3f2e8%2Fremote-placement.png?alt=media" alt="" width="300">

> **💡 Tip.** 용도가 서버에서 클라이언트로의 통신인지(Server to Client), 클라이언트에서 서버로의 통신인지를(Client to Server) 명확히 구분하기 위해, **이름에 접두어**로 **S2C\_** 또는 **C2S\_**&#xB97C; 사용하는 것을 권장합니다. 이는 오브젝트의 역할을 직관적으로 이해할 수 있게 하여 코드의 가독성과 유지보수성을 높입니다.

![](https://2697870212-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRPi87oM9ttlk5nyu7L7%2Fuploads%2Fgit-blob-68b559b75020552f202f007bd41a3a43d6717a2a%2Fremote-naming.png?alt=media)

## RemoteEvent를 이용한 통신 구현

RemoteEvent로 이벤트 발송시 **인자(Arguments)**&#xB97C; 함께 전송할 수 있습니다. 인자는 FireServer, FireClient, FireAllClients 메서드 호출 시 전달되며, 수신 측에서 해당 데이터를 콜백 함수로 받을 수 있습니다.

### FireAllClients (Server ➡ All Client)

서버에서 **모든 클라이언트**로 이벤트를 발송합니다. 이 방식은 게임의 전역 상태를 동기화하거나 모든 플레이어에게 동일한 정보를 전달할 때 유용합니다.

**Script에서**

```lua
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage") 
local S2C_GameEnd = ReplicatedStorage:WaitForChild("S2C_GameEnd")

local function TimeOver()
    local isWin = false
    S2C_GameEnd:FireAllClients(isWin) -- Passing arguments
end
```

**LocalScript에서**

```lua
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage") 
local S2C_GameEnd = ReplicatedStorage:WaitForChild("S2C_GameEnd")

local function OnGameEnd(isWin)
    print("[OnGameEnd] ", Players.LocalPlayer.Name, " / isWin : ", isWin)
end
S2C_GameEnd.OnClientEvent:Connect(OnGameEnd)
```

### FireClient (Server ➡ 특정 Client)

서버에서 **특정 클라이언트**로 이벤트를 발송하는 방식입니다. 이 방식은 개별 플레이어와 관련된 작업을 처리할 때 사용됩니다.

**Script에서**

```lua
local Players = game:GetService("Players")

local ReplicatedStorage = game:GetService("ReplicatedStorage") 
local S2C_LevelUp = ReplicatedStorage:WaitForChild("S2C_LevelUp")

local function LevelUp(player)
    local prevLevel = 1
    local curLevel = 2
    S2C_LevelUp:FireClient(player, prevLevel, curLevel) -- Passing arguments
end
```

**LocalScript에서**

```lua
local Players = game:GetService("Players")

local ReplicatedStorage = game:GetService("ReplicatedStorage") 
local S2C_LevelUp = ReplicatedStorage:WaitForChild("S2C_LevelUp")

local function OnLevelUp(prevLevel, curLevel)
    print("[OnLevelUp] ", Players.LocalPlayer.Name, " / LevelUp : ", prevLevel, " -> ", curLevel)
end
S2C_LevelUp.OnClientEvent:Connect(OnLevelUp)
```

### FireServer (Client ➡ Server)

클라이언트에서 **서버**로 이벤트를 발송합니다. 이 방식은 사용자의 입력이나 특정 이벤트(예: 버튼 클릭, 스킬 사용 요청)를 서버가 처리해야 할 때 사용됩니다.

**LocalScript에서**

```lua
local ReplicatedStorage = game:GetService("ReplicatedStorage") 
local C2S_UseSkill = ReplicatedStorage:WaitForChild("C2S_UseSkill")

local function ClickSkillButton()
    local skillID = 1
    C2S_UseSkill:FireServer(skillID)
end
```

**Script에서**

```lua
local ReplicatedStorage = game:GetService("ReplicatedStorage") 
local C2S_UseSkill = ReplicatedStorage:WaitForChild("C2S_UseSkill")

local function OnUseSkill(player, skillID)
    print("[OnUseSkill] ", player.Name, " / skillID : ", skillID)
end
C2S_UseSkill.OnServerEvent:Connect(OnUseSkill)
```

## RemoteFunction을 이용한 통신 구현

서버 Script가 `OnServerInvoke`에 처리 함수를 **대입**하고, 클라이언트 LocalScript가 `InvokeServer` 메서드로 호출합니다. 호출 시 전달한 **인자(Arguments)**&#xB294; 처리 함수의 **두 번째 인자부터** 전달되며, 처리 함수의 `return` 값이 클라이언트로 돌아갑니다.

### InvokeServer (Client ➡ Server ➡ Client)

**Script에서 (처리 측)**

```lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local C2S_BuyItem = ReplicatedStorage:WaitForChild("C2S_BuyItem")

local function OnBuyItem(player, itemId, amount)
    -- player는 서버가 호출 출처에서 직접 확인한 값이므로 신뢰할 수 있습니다
    print("[BuyItem] ", player.Name, " / itemId : ", itemId, " / amount : ", amount)

    local isSuccess, balance = Purchase(player, itemId, amount)
    return isSuccess, balance -- 여러 값을 한 번에 반환
end
C2S_BuyItem.OnServerInvoke = OnBuyItem -- Connect가 아닌 대입
```

**LocalScript에서 (호출 측)**

```lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local C2S_BuyItem = ReplicatedStorage:WaitForChild("C2S_BuyItem")

local function ClickBuyButton(itemId, amount)
    -- 서버 응답이 도착할 때까지 이 줄에서 대기
    local isSuccess, balance = C2S_BuyItem:InvokeServer(itemId, amount) -- Passing arguments

    if isSuccess then
        print("[BuyItem] Success / balance : ", balance)
    else
        print("[BuyItem] Failed")
    end
end
```

**`OnServerInvoke`의 첫 번째 인자 `player`는 클라이언트가 보낸 값이 아닙니다.** 서버가 요청이 들어온 경로에서 직접 확인해 넣어주는 값이므로 권한 검증의 기준으로 사용할 수 있습니다. 반면 그 뒤의 인자는 **전부 클라이언트가 보낸 값**이므로 신뢰할 수 없습니다.

### 처리 함수(OnServerInvoke) 규칙

`OnServerInvoke`는 이벤트가 아니라 **함수를 담는 프로퍼티**입니다. `OnServerEvent`와 사용법이 다르므로 아래 세 가지를 기억하세요.

* **`:Connect()`를 사용할 수 없습니다.** `OnServerInvoke = 함수` 형태로 대입합니다.
* **하나의 함수만 유지됩니다.** 여러 번 대입하면 마지막에 대입한 함수만 실행됩니다.
* **`return`을 빠뜨리면 호출 측이 `nil`을 받습니다.** 받은 값을 그대로 사용하면 이후 코드에서 오류가 발생할 수 있습니다.

호출이 겹쳐도 처리 함수는 각각 실행됩니다. 처리 중에 `OnServerInvoke`를 다른 함수로 바꾸면, 이미 진행 중이던 호출은 **바꾸기 전 함수로 끝까지 처리되고** 그 이후의 새 호출부터 새 함수가 사용됩니다.

## 서버 검증

엔진은 클라이언트가 보낸 값을 검사하지 않고 처리 함수로 **그대로 전달합니다.** 타입이 맞는지, 범위가 올바른지 확인하는 일은 전적으로 스크립트의 몫입니다. **이는 RemoteFunction의 `OnServerInvoke`와 RemoteEvent의 `OnServerEvent` 양쪽에 똑같이 해당합니다.**

아래 세 단계를 순서대로 확인하세요.

1. **권한** — 이 `player`가 지금 이 요청을 할 자격이 있는지 확인합니다. (거리, 상태, 소유 여부, 쿨타임)
2. **타입과 구조** — `typeof()`로 실제 타입을 확인합니다. 클라이언트는 인스턴스처럼 보이는 **테이블을 위조해서 보낼 수 있으므로**, `typeof(item) == "Instance"`로 걸러야 합니다.
3. **값의 범위** — 최솟값과 최댓값을 확인합니다. `NaN`은 모든 크기 비교가 `false`라 범위 검사를 그냥 통과하고, `Inf`는 상한 검사가 없으면 통과합니다.

```lua
local function OnBuyItem(player, item, amount)
    -- 1. 권한
    if not CanPurchase(player) then
        return false
    end

    -- 2. 타입과 구조 (위조 테이블 방어)
    if typeof(item) ~= "Instance" or not item:IsA("Tool") then
        return false
    end

    -- 3. 값의 범위
    if typeof(amount) ~= "number" then
        return false
    end
    if amount ~= amount then -- NaN : 자기 자신과도 다름
        return false
    end
    if math.abs(amount) == math.huge then -- Inf
        return false
    end
    if amount < 1 or amount > 99 then
        return false
    end

    return Purchase(player, item, amount)
end
C2S_BuyItem.OnServerInvoke = OnBuyItem
```

* 플레이어별로 **호출 간격과 횟수를 제한**하여 과도한 요청을 막습니다.
* 클라이언트 측 검증에만 의존하지 않습니다. 클라이언트 코드는 변조될 수 있으므로 서버 검증이 항상 필요합니다.
* 서버가 이미 알고 있는 값은 클라이언트에게 받지 않습니다. (예 : 가격, 보유 여부)

## 주의 사항

### 처리 함수를 설정하지 않으면 영원히 멈춥니다

`OnServerInvoke`가 설정되지 않은 RemoteFunction을 호출하면 **오류도 타임아웃도 없이 호출한 스레드가 영구히 멈춥니다.** 대기를 중단시키는 수단은 제공되지 않습니다.

* 클라이언트의 호출보다 서버의 처리 함수 대입이 먼저 끝나도록 초기화 순서를 설계하세요.
* 처리가 오래 걸릴 수 있다면 RemoteEvent로 요청을 보내고, 완료 시 별도 이벤트로 결과를 알리는 방식을 검토하세요.

**멈춤과 오류는 증상으로 구분할 수 있습니다.** 처리 함수 자리에 함수가 아닌 값(모듈 테이블 등)을 대입하면 **대입 자체는 조용히 통과하고**, 호출하는 순간 오류가 발생합니다. 즉 아무 반응 없이 멈추면 처리 함수가 **설정되지 않은 것**이고, 호출하자마자 오류가 나면 **함수가 아닌 값이 대입된 것**입니다.

### 스크립트 종류를 지켜야 합니다

`InvokeServer`는 LocalScript에서만, `InvokeClient`는 Script에서만 호출할 수 있습니다. 잘못된 위치에서 호출하면 조용히 무시되지 않고 오류가 발생합니다.

### 처리 함수의 오류는 호출한 쪽으로 전파됩니다

서버 처리 함수에서 발생한 오류는 호출한 클라이언트로 그대로 전달되어 클라이언트 코드를 중단시킵니다. 실패할 수 있는 처리라면 `pcall()`로 감싸거나, 오류 대신 성공 여부를 반환값으로 돌려주세요.

```lua
local isSuccess, result = pcall(function()
    return C2S_BuyItem:InvokeServer(itemId, amount)
end)

if not isSuccess then
    print("[BuyItem] Error : ", result)
end
```

RemoteEvent는 이런 전파가 없습니다. 수신 측 콜백에서 난 오류는 발송한 쪽에 영향을 주지 않습니다.

### InvokeClient는 사용하지 않습니다

서버가 클라이언트를 호출하는 `InvokeClient`에는 세 가지 위험이 있어 **사용을 강하게 지양합니다.**

* 클라이언트에서 발생한 오류가 **서버로 전파되어** 서버 코드가 중단됩니다.
* 호출 도중 해당 클라이언트의 **연결이 끊기면 오류**가 발생합니다.
* 클라이언트가 값을 반환하지 않으면 **서버가 영구히 멈춥니다.** 악의적인 클라이언트가 의도적으로 유발할 수 있습니다.

`player` 인자가 올바르지 않으면 처리 함수가 실행되지 않고 호출 시점에 오류가 발생합니다. 다음 네 가지 경우입니다.

* `nil`인 경우
* Player가 아닌 Instance인 경우
* Instance가 아닌 값인 경우 (Player처럼 보이도록 위조한 테이블 포함)
* 이미 퇴장한 Player의 참조인 경우

특히 **이미 퇴장한 플레이어의 참조를 변수에 보관했다가 호출하는 경우**를 주의하세요. 보관한 참조는 사용하기 전에 아직 `Players` 아래에 있는지 확인해야 합니다.

**대안** : `RemoteEvent:FireClient()`로 요청을 보내고, 결과가 필요하면 클라이언트가 별도의 RemoteEvent로 돌려보내는 방식을 사용하세요.

```lua
-- Script에서
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local S2C_RequestSetting = ReplicatedStorage:WaitForChild("S2C_RequestSetting")
local C2S_ReplySetting = ReplicatedStorage:WaitForChild("C2S_ReplySetting")

local function RequestSetting(player, settingKey)
    S2C_RequestSetting:FireClient(player, settingKey)
end

local function OnReplySetting(player, settingKey, value)
    -- 반환값 대신 별도 이벤트로 결과를 수신
    print("[ReplySetting] ", player.Name, " / ", settingKey, " : ", value)
end
C2S_ReplySetting.OnServerEvent:Connect(OnReplySetting)
```

### 경계를 넘을 때 값이 변형됩니다

서버와 클라이언트 경계를 넘는 값은 전달 과정에서 변환됩니다. **RemoteEvent의 인자, RemoteFunction의 인자와 반환값 모두에 똑같이 적용되며, 오류나 경고는 발생하지 않습니다.**

* **함수는 전달되지 않습니다.** 인자로 직접 넘기면 `nil`이 되고, 테이블 값으로 넣으면 **그 키가 통째로 사라집니다.** 콜백을 넘겨야 한다면 문자열 이름으로 약속하고 받는 쪽에서 연결하세요.
* **`ServerStorage` 하위처럼 복제되지 않는 인스턴스**도 같은 방식으로 사라집니다.
* **테이블은 복사본으로 전달됩니다.** 받는 쪽에서 인자 테이블을 채워도 보낸 쪽 원본은 비어 있으므로, 필요한 값은 반환값으로 받으세요. 메타테이블은 전달되지 않습니다.
* **테이블 키는 모두 문자열로 통일하세요.** 배열 원소와 이름을 붙인 필드를 한 테이블에 섞으면 이름 필드가 사라지고, 숫자 키는 문자열로 바뀌며, 인스턴스나 함수를 키로 쓰면 원래 키로 조회할 수 없습니다.

```lua
-- 위험 — callback 키가 사라지고, owner도 함께 사라집니다
C2S_SomeFunction:InvokeServer({ "칼", "활", owner = "Diva", callback = OnDone })

-- 안전 — 배열을 한 겹 안으로 넣고, 콜백은 이름으로 약속합니다
C2S_SomeFunction:InvokeServer({ items = { "칼", "활" }, owner = "Diva", callbackId = "OnDone" })
```

### 스트리밍이 켜진 월드에서 인스턴스를 주고받을 때

`StreamingEnabled`가 켜져 있으면, 서버가 만들어 반환한 인스턴스가 그 시점에 클라이언트에 **아직 존재하지 않을 수 있습니다.** 이 경우 `nil`을 받게 되며, 영구히 도착하지 않을 수도 있습니다.

* 반환값을 사용하기 전에 `nil` 여부를 확인합니다.
* `WaitForChild(name, timeout)`처럼 **제한 시간을 함께 지정해** 대기합니다.
* 필요하다면 해당 영역을 미리 불러오는 방법을 검토합니다.

```lua
local Part = C2S_CreatePart:InvokeServer(position)

if Part == nil then
    print("[CreatePart] Not streamed yet")
    return
end
```

## 고급 활용

* 반환값이 필요 없는 통신에는 RemoteFunction 대신 **RemoteEvent**를 사용합니다. 동기 호출은 호출한 쪽을 멈춥니다.
* 클라이언트에서 전송된 데이터는 신뢰할 수 없으므로, 항상 **서버에서 검증**해야 합니다. (위 **서버 검증** 참고)
* 필요한 정보만 서버로 보내어 네트워크 부하를 줄입니다. (클라이언트에서 **최소 데이터만 전송**)
  * 너무 많은 RemoteEvent나 RemoteFunction을 생성하지 않도록 설계합니다. 동일한 맥락에서 처리 가능한 작업은 하나로 묶어 처리합니다.
  * RemoteEvent의 연결은 필요할 때만 생성하고, 사용이 끝나면 **연결을 해제**하여 메모리 누수를 방지합니다. (`Disconnect()` 함수)
  * 하나의 오브젝트로 여러 작업을 처리할 때는 **작업 유형**을 나타내는 첫 번째 인자(EventType)를 추가합니다. (EventType 사용 예시 : PlayerActionType과 ActionID)
* 처리 함수 안에서 오래 대기하는 작업을 수행하지 않습니다. 호출한 클라이언트가 그만큼 함께 멈춥니다.
* 같은 환경 내(서버끼리 또는 한 클라이언트 내)의 통신에는 BindableEvent나 BindableFunction을 사용합니다.
