# 스크립트 최적화 실전 가이드

## 개요

대규모 RPG나 액션 게임처럼 복잡한 구조의 게임에서는, 스크립트의 설계와 동작 방식이 게임의 전반적인 퍼포먼스에 중대한 영향을 미칩니다. 최적화를 고려하지 않은 스크립트는 **서버 다운**, **클라이언트 프레임 드롭**, **메모리 누수, 예기치 않은 크래시** 등 치명적인 문제를 유발할 수 있으며, 이는 곧 **게임의 리텐션 저하**와 **수익 감소**로 직결될 수 있습니다.

이 문서는 **스크립트의 기본 동작 원리**를 이해하고, 실전에서 적용할 수 있는 다양한 **성능 최적화 기법**들을 소개합니다. 이를 통해 복잡한 로직, 대량의 오브젝트, 빈번한 통신 처리 상황에서도 안정적인 성능을 유지하며 쾌적한 플레이 환경을 제공하는 데 도움이 될 것입니다.

## 주의 사항

스크립트는 **최적화와 코드 가독성 사이의 균형을 고려하여 작성해야 합니다.** 지나치게 성능만을 의식한 코드는 이해하기 어려워지고, 반대로 과도한 추상화나 장황한 구조는 성능에 악영향을 줄 수 있습니다.

## 기본 동작 이해

최적화된 코드를 작성하려면 Lua의 기본 동작 방식을 깊이 이해하는 것이 중요합니다. 코드 실행, 메모리 관리, 데이터 처리 방식에 대한 이해는 자원 낭비를 줄이고 성능 저하를 방지하는 데 도움이 됩니다.

### 변수 타입

Lua에서는 변수에 저장되는 값이 **값 자체(Value Type)**&#xC778;지, **객체 참조(Reference Type)**&#xC778;지에 따라 동작이 달라집니다.

<table><thead><tr><th width="209.00006103515625">유형</th><th width="137.7720947265625">예시</th><th>특징</th></tr></thead><tbody><tr><td>값 타입 (Value)</td><td>number<br>string<br>boolean<br>nil</td><td><ul><li>대입 시 <strong>복사</strong>됨</li></ul></td></tr><tr><td>참조 타입 (Reference)</td><td><strong>table</strong><br><strong>function</strong><br><strong>coroutine</strong><br><strong>Instance</strong></td><td><ul><li>대입 시 <strong>참조 공유</strong></li><li>복사가 아닌 참조이므로, 하나에서 값을 변경하면 다른 쪽에도 영향을 줌</li></ul></td></tr></tbody></table>

참조 타입은 변수에 값 자체가 아닌 데이터의 참조(주소)가 저장되기 때문에, 하나의 참조를 여러 변수에서 공유할 수 있습니다. 이로 인해 한 변수에서 값을 변경하면 동일한 참조를 가진 다른 변수에도 영향을 미치게 됩니다. 이러한 특성은 데이터 구조를 유연하게 다룰 수 있게 해주지만, 의도하지 않은 변경이나 버그의 원인이 될 수 있으므로 주의가 필요합니다.

```lua
local t1 = { score = 100 }
local t2 = t1     -- 참조가 공유됨

t2.score = 200    -- t1에도 영향을 줌

print(t1.score)   -- 출력: 200
print(t2.score)   -- 출력: 200
```

### 메모리 구조

Lua는 내부적으로 **스택(stack)**&#xACFC; **힙(heap)** 메모리 구조를 사용하여 변수를 관리합니다. 변수의 타입에 따라 값이 저장되는 위치, 생명 주기, 메모리 해제 방식 등 동작 방식이 달라집니다.

<table><thead><tr><th width="194.96484375">유형</th><th width="128.631591796875">저장 공간</th><th>특징</th></tr></thead><tbody><tr><td>값 타입 (Value)</td><td><strong>스택(Stack)</strong></td><td><ul><li><strong>생명 주기 :</strong> 함수나 블록이 실행되는 동안 유효하며, 해당 범위를 벗어나면 자동으로 소멸</li><li><strong>메모리 해제 :</strong> 명시적인 정리가 필요 없으며, 실행 흐름에 따라 자동으로 제거</li></ul></td></tr><tr><td>참조 타입 (Reference)</td><td><strong>힙(Heap)</strong></td><td><ul><li><strong>생명 주기 :</strong> 객체에 대한 참조가 존재하는 동안 유지되며, 참조가 하나라도 남아 있으면 메모리에 유지</li><li><strong>메모리 해제 :</strong> 모든 참조가 끊기면 GC에 의해 메모리에서 제거됨</li><li><strong>특징 :</strong> 하나의 객체를 여러 변수에서 참조할 수 있어 구조가 유연하지만, 메모리 관리에 주의 필요</li></ul></td></tr></tbody></table>

값 타입은 스코프가 끝나면 메모리에서 자동으로 해제되지만, 참조 타입은 GC에 의해 참조 유무에 따라 주기적으로 정리됩니다. 이 차이로 인해 참조 타입은 불필요한 참조를 해제하지 않으면 메모리 누수, 성능 저하가 발생할 수 있습니다.

단, 참조 타입이라 하더라도 **스코프 내에서만 사용되고 더 이상 다른 곳에서 참조되지 않는 경우**, 자동으로 GC 대상이 되어 메모리에서 해제됩니다. 하지만 전역 변수나 클로저, 테이블 간의 순환 참조처럼 참조가 유지되는 구조에서는 명시적인 참조 해제가 필요합니다.

```lua
do
    local t = { 1, 2, 3 } 
    print(t[1])           
end
-- 여기서 t는 스코프를 벗어났기 때문에 더 이상 참조되지 않음
-- 이후 GC 사이클에 따라 자동으로 메모리에서 해제됨
```

### GC (Garbage Collection)

Lua는 자동 메모리 관리를 위해 **Mark-and-Sweep(표시-삭제)** 방식의 **Garbage Collection(GC)**&#xC744; 사용합니다. GC는 더 이상 사용되지 않는 객체를 자동으로 감지하고 메모리를 해제해주므로, C++ 같은 언어처럼 개발자가 직접 메모리를 할당하거나 해제할 필요가 없습니다.

즉, 메모리 관리의 부담을 Lua가 대부분 대신 처리해주지만, **GC가 효율적으로 동작하려면 불필요한 참조를 남기지 않는 구조와 접근 방식이 중요**합니다. 참조가 남아 있는 한 객체는 "사용 중"으로 간주되어 메모리에서 제거되지 않기 때문에, 개발자가 참조 해제 타이밍을 신중하게 설계하지 않으면 메모리 누수가 발생할 수 있습니다.

**Mark-and-Sweep(표시-삭제)**

1. **Mark 단계** : 전역 변수, 지역 변수, 실행 중 함수 스택 등 루트 객체에서 출발해, 연결된 객체들을 따라가며 참조 중인 객체를 표시합니다.
2. **Sweep 단계** : 표시되지 않은, 즉 어디에서도 참조되지 않는 객체는 메모리에서 해제(수거) 됩니다.

**메모리가 해제되는 조건**

* 참조하는 곳이 전혀 없는 객체
* Destroy() 후에 참조도 nil로 변경된 객체
* Disconnect() 후에 참조도 nil로 정리된 이벤트 연결 객체

### GC 이해의 중요성

GC 동작을 이해해야 하는 이유는 단순히 “메모리를 알아서 정리해주니까 편하다”는 수준을 넘어, 성능, 안정성, 유지보수성 측면에서 실제로 큰 차이를 만들기 때문입니다.

* GC는 자동으로 메모리를 정리해주지만, **참조가 남아 있으면 절대 수거하지 않습니다**.
* GC 대상 객체를 명확히 제거하면, 불필요한 메모리 점유를 줄이고 **메모리 사용량을 예측 가능하게 유지**할 수 있습니다.
* GC는 힙 객체가 많을수록 자주 실행되며, 실행 시 프레임 드롭이 발생할 수 있습니다.\
  **반복적인 객체 생성과 삭제를 피하고, 풀링 같은 재사용**으로 GC 발생 자체를 줄이는 것이 중요합니다.
* “왜 Destroy했는데도 메모리가 안 줄어들지?”, “왜 게임이 점점 느려지지?” 와 같은 **문제 원인을 빠르게 파악하고 구조를 개선**할 수 있습니다.

## 최적화 기초 가이드라인

### 성능 최적화

* 오브젝트는 Instance.new, Clone 같은 **런타임 생성/파괴 대신 오브젝트 풀링 기반**으로 처리\
  (예: 총알을 Instance.new로 매번 생성하지 말고, 미리 만들어둔 오브젝트를 꺼내 사용하고 사용 완료시 다시 보관 처리)
* 한 번에 너무 많은 오브젝트를 생성/파괴하는 구조 지양\
  (예: 스폰 시 몬스터 50마리를 동시에 생성하는 대신, 순차적 생성으로 렉 분산)
* 미사용 객체는 반드시 **Destroy() 후 nil** 처리
* 미사용 이벤트는 반드시 **Disconnect() 후 nil** 처리
  * 특히, 플레이어와 캐릭터에 연결된 이벤트는 반드시 명시적으로 해제
* 사용하지 않는 UI는 화면에서 숨김 처리
* 오브젝트, 객체, 서비스는 **참조를 캐싱해서 재사용**\
  (예: game:GetService("Players")를 매번 호출하지 말고, 변수에 저장해서 사용)
* 모듈 스크립트는 require() 결과를 **캐싱해서 재사용**
* 글로벌 변수의 사용은 지양하고, 로컬 범위에서 처리\
  (예: 스크립트 전체에 영향을 줄 수 있는 myData 같은 전역 변수 사용 지양)
* 사용되지 않는 변수는 반드시 제거
* Vector3, CFrame과 같은 객체는 미리 계산하여 캐싱\
  (예: 발사체 오프셋과 같은 고정 값은 매번 계산하지 않고, 초기화 시 미리 계산해두고 재사용 지양)
* 복잡한 참조 관계(서로를 참조하는 구조) 대신 **단방향 참조로 설계**\
  (예: 캐릭터와 총이 서로 참조하는 구조보다는 총이 캐릭터를 참조)
* 테이블은 가능하면 재사용하는 구조로 설계\
  (예: 루프마다 {}를 새로 만들기보다, 외부에서 재사용하는 방식이 메모리 절약에 유리)
* **익명 함수는 남용하지 않고**, 불필요한 클로저 사용을 줄일 것\
  (특히 for 루프 안에서 이벤트 연결을 위한 새로운 함수를 매번 정의하지 않기)
* 단순한 테이블은 key보다 index 기반으로 처리\
  ({ "a", "b", "c" } 형태는 pairs보다 ipairs가 더 빠르게 순회됨)
* 나눗셈은 곱셈으로 대체 가능한 경우 \* 연산자 사용\
  (예: x \* 0.5는 x / 2보다 빠름)
* 긴 문자열 병합은 "a" .. "b" 대신 **table.concat()**&#xC73C;로 처리하여 성능 향상\
  (.. 연산자는 문자열마다 새로운 메모리를 할당하여 병합하기 때문에, 반복이 많을수록 GC 부하 발생)
* for, while 등 루프는 무조건 실행하지 말고 조건부 또는 간헐적으로 동작하도록 설계\
  (특히 루프 간격 조절)
* 많은 양의 반복 처리는 한 번에 몰아서 하지 말고, 코루틴이나 분할 로직으로 분산 처리
* 과도한 코루틴 생성을 피하고, 사용한 코루틴은 재사용하거나 종료 후 nil 처리
* Heartbeat와 같은 프레임 단위 이벤트는 꼭 필요한 경우에만 사용하고 사용 완료시 해제 처리

### 통신 및 이벤트 최적화

* 클라이언트에서 가능한 처리는 클라이언트에서 수행하여 서버 부하 최소화
* RemoteEvent 통신은 꼭 필요한 경우에만 사용하고, **유사한 동작은 하나의 RemoteEvent**로 통합 처리\
  (자주 호출되는 처리는 묶어서 전송)
* **데이터 전송량**은 가능한 최소화하여 설계\
  (예: 높이와 관련된 데이터는 Vector3 전체 대신 number 전송)
* 속성 전달은 Attribute 사용\
  (부하 순위: RemoteEvent > ValueInstance > Attribute)
* 통신 처리에 우선순위 큐를 도입하여, 필수 업데이트와 선택 업데이트를 구분해서 효율적으로 처리\
  (이펙트, 사운드 같은 연출은 후순위 처리)
* 이벤트 기반(Event-Driven) 구조로 필요한 시점에만 데이터가 처리되도록 설계
  * 이벤트가 너무 자주 호출되는 상황에서는 반대로 get 함수 기반으로 설계
* DataStore와 같은 서버 통신 기반 API는 호출 제한이 존재하므로, 분당 호출 수 제한하여 설계

### 구조 설계

* 여러개의 KillPart처럼 같은 기능을 처리하는 경우, 오브젝트마다 각각 스크립트를 구성하기보다는 하나의 **매니저 스크립트**에서 여러 오브젝트를 통합 관리
* **MVC**와 같은 구조 설계를 적용하여 코드 간 의존성을 줄이고 중복 코드를 최소화\
  (예: UI는 필요한 시점에만 갱신되도록 책임을 명확히 분리하여 불필요한 연산 부하 감소)
* 게임 로직, 몬스터 등에 **상태머신**(State Machine)을 도입하여 필요한 로직만 실행되고 계산되게 설계
* 데이터 처리와 시각화의 역할을 서버와 클라이언트로 명확히 분리하여, 부하 집중 분산

## 도입 우선순위가 높은 성능 개선 전략

이 항목들은 비교적 간단한 적용만으로 **렉, 메모리 누수, 성능 불안정성**과 같은 주요 문제를 효과적으로 줄이는 데 실질적인 도움이 됩니다.

### Destroy + nil 정리

미사용 객체는 GC에 의해 수거되도록 명시적으로 참조를 제거합니다.

```lua
local Effect = Instance.new("ParticleEmitter")
...
Effect.Parent = Part

wait(3)
Effect:Destroy()  -- 객체 제거
Effect = nil      -- 참조 제거 (GC 가능)
```

### 이벤트 Disconnect + nil 정리

미사용 이벤트 역시 명시적으로 참조를 제거해야 하며, 플레이어/캐릭터 관련 이벤트는 특히 명확한 해제가 중요합니다.

```lua
local Connection = nil

local function OnDied()
    if Connection then
        Connection:Disconnect()  -- 이벤트 연결 해제
        Connection = nil         -- 참조 제거 (GC 가능)
    end
end
Connection = Humanoid.Died:Connect(OnDied)
```

### 오브젝트 풀링 (Object Pooling)

오브젝트를 반복적으로 생성/삭제하는 대신, 재사용 가능한 구조를 구축하여 총알, 이펙트, UI 슬롯 등의 오브젝트를 효율적으로 재활용합니다.

이를 통해 불필요한 메모리 할당과 해제를 줄이고, GC의 부담을 최소화하여 프레임 드랍이나 일시적인 랙 현상을 방지할 수 있습니다.

```lua
local BulletManager = {}

-- 총알 오브젝트를 저장하는 풀
local PoolList = {}

-- 총알 템플릿 오브젝트 생성
local template = Instance.new("Part")
template.Size = Vector3.new(0.2, 0.2, 2)
...
template.Name = "Bullet"

-- 지정된 수만큼 총알을 생성해 풀에 저장
function BulletManager:Init(count)
    for i = 1, count do
        local bullet = template:Clone()
        ...
        bullet:SetAttribute("Active", false)

        table.insert(PoolList, bullet)
    end
end

-- 풀에서 사용 가능한 총알 하나를 반환
function BulletManager:GetFromPool()
    for _, bullet in ipairs(PoolList) do
        if not bullet:GetAttribute("Active") then
            bullet:SetAttribute("Active", true)
            ...

            return bullet
        end
    end
    return nil
end

-- 사용이 끝난 총알을 풀로 되돌림
function BulletManager:Release(bullet)
    bullet:SetAttribute("Active", false)
    ...
end

return BulletManager
```

```lua
local PoolCount = 30 -- 재사용 오브젝트 최대 갯수
BulletManager:Init(PoolCount) -- 총알 풀을 초기화하고 오브젝트를 미리 생성합니다.

local Bullet = BulletManager:GetFromPool() -- 사용 가능한 오브젝트를 가져옵니다.

if Bullet then
    Bullet.Position = startPos
    ...

    wait(1)
    BulletManager:Release(Bullet) -- 사용이 끝난 총알을 풀에 반환하여 재사용할 수 있도록 합니다.
end
```

## 활용 예시

이 매뉴얼은 단순히 외워야 할 규칙을 나열한 문서가 아닙니다. **프로젝트 성격, 구조 복잡도, 퍼포먼스 요구 수준에 따라** 유연하게 참고하고 적용할 수 있는 **‘기준점’** 역할을 합니다.

* **새로운 기능을 구현할 때는** 구조와 처리 흐름이 성능에 어떤 영향을 줄 수 있는지 미리 고려해보는 체크리스트로 활용하세요.
* **디버깅 중 성능 문제가 의심될 때는**, 해당 섹션을 빠르게 훑어보며 개선 지점을 찾는 기준으로 삼을 수 있습니다.
* **협업 시에는** 팀원 간 코드 스타일과 최적화 수준을 맞추는 데 기반 문서로 활용할 수 있습니다.

**꼭 모든 항목을 강제로 적용할 필요는 없습니다.** 다만, 이 문서에 담긴 기준들은 프로젝트가 커질수록 반드시 마주치게 될 문제를 미리 방지하기 위한 지침임을 기억해 주세요.
