Lua 언어 핵심 정리

Lua는 작고 빠른 스크립트 언어다. 브라질의 PUC-Rio에서 개발되었으며, 처음부터 독립적인 대규모 애플리케이션을 만드는 것보다 다른 프로그램에 삽입되어 기능을 확장하는 임베디드 스크립팅 언어를 중요한 목표로 두고 설계되었다.

그래서 Lua는 Python이나 JavaScript처럼 거대한 표준 라이브러리를 제공하는 방향보다는 언어 자체를 작게 유지하고, C/C++ 같은 호스트 프로그램과 결합하기 쉽게 만드는 데 집중한다.

게임 개발에서도 Lua가 많이 사용된다. 게임 엔진이나 서버 프로그램을 C/C++로 작성하고, 게임 규칙이나 퀘스트, UI 동작처럼 자주 변경되는 부분을 Lua 스크립트로 분리하는 방식이다.

Lua의 전체적인 구조는 다음과 같다.

Host Program -> Lua Runtime -> Lua Script -> 게임 로직 / 설정 / 이벤트

이 구조를 이해하면 Lua의 문법도 자연스럽게 연결된다.


Lua의 기본 특징

Lua는 동적 타입 언어다.

local value = 10

value = "Hello"

변수 자체에 타입을 고정하지 않고 런타임에 값의 타입이 결정된다.

함수도 값으로 취급할 수 있고, 테이블을 이용해 배열과 객체에 가까운 구조를 모두 표현할 수 있다.

Lua의 주요 타입은 다음과 같다.

number / string / boolean / nil / function / table / userdata / thread / light userdata

이 중 실질적인 Lua 코드에서 가장 많이 사용하게 되는 것은 tablefunction이다.


변수

Lua에서 지역 변수는 local을 사용하는 것이 기본이다.

local name = "Player"
local health = 100

local 없이 선언하면 전역 변수가 된다.

name = "Player"

전역 변수는 어디서든 접근할 수 있기 때문에 규모가 커질수록 관리가 어려워진다.

따라서 일반적인 Lua 코드에서는 특별한 이유가 없다면 local을 사용하는 것이 좋다.

local player = {}

이 습관 하나만으로도 전역 상태가 불필요하게 퍼지는 것을 상당히 줄일 수 있다.


타입

Lua는 동적 타입 언어이기 때문에 다음과 같은 코드가 가능하다.

local value = 10

value = "Hello"

value = true

타입은 값에 붙어 있다.

value -> 10 -> number
value -> "Hello" -> string
value -> true -> boolean

현재 값의 타입을 확인하려면 type()을 사용한다.

print(type(10))
print(type("Hello"))
print(type(true))

결과는 각각 number, string, boolean으로 나온다.


nil

Lua에서 nil은 값이 존재하지 않음을 나타내는 특별한 값이다.

local player = nil

테이블의 값을 삭제할 때도 nil을 사용한다.

player.name = nil

다만 Lua의 nil은 다른 언어의 null과 완전히 같은 개념으로 생각하면 안 된다.

특히 테이블에서는 nil이 중요한 의미를 가진다.

local player = {
    name = "Knight",
    health = 100
}

player.health = nil

이 경우 health 필드가 제거된 것과 같은 상태가 된다.


Boolean

Lua의 Boolean은 truefalse다.

다만 조건문을 작성할 때 다른 언어와 차이가 있다.

Lua에서는 falsenil만 거짓으로 취급한다.

if value then
    print("true")
end

다음 값들은 조건문에서 참으로 취급된다.

true / 0 / "" / {}

특히 0과 빈 문자열이 참이라는 점은 C/C++이나 JavaScript에 익숙하다면 주의해야 한다.


조건문

기본적인 조건문은 다음과 같다.

if health > 0 then
    print("Alive")
elseif health == 0 then
    print("Dead")
else
    print("Invalid")
end

Lua에서는 조건문의 끝을 end로 닫는다.

if -> elseif -> else -> end

중괄호를 사용하는 C 계열 언어와 문법 구조가 다르다.


반복문

Lua에는 while, repeat, for가 있다.

while

local count = 0

while count < 10 do
    count = count + 1
end

repeat

local count = 0

repeat
    count = count + 1
until count >= 10

repeat은 조건을 마지막에 검사하기 때문에 최소 한 번은 실행된다.


Numeric For

숫자 범위를 반복할 때 for를 사용할 수 있다.

for i = 1, 10 do
    print(i)
end

증가량도 지정할 수 있다.

for i = 10, 1, -1 do
    print(i)
end

Lua의 숫자 for는 반복 횟수가 명확한 경우 간단하게 사용할 수 있다.


Generic For

테이블이나 Iterator를 순회할 때는 Generic For가 중요하다.

local players = {
    "A",
    "B",
    "C"
}

for index, player in ipairs(players) do
    print(index, player)
end

키와 값을 가진 테이블이라면:

local stats = {
    health = 100,
    mana = 50
}

for key, value in pairs(stats) do
    print(key, value)
end

ipairspairs는 Lua 코드를 읽을 때 자주 보게 된다.


Table

Lua에서 가장 중요한 자료구조다.

local player = {
    name = "Knight",
    health = 100,
    level = 10
}

값을 가져온다.

print(player.name)
print(player.health)

대괄호를 사용할 수도 있다.

print(player["name"])

다음 두 표현은 같은 의미다.

player.name <-> player["name"]

Table은 배열 역할도 한다

Lua에서는 별도의 배열 타입이 없다.

테이블을 배열처럼 사용할 수 있다.

local items = {
    "Sword",
    "Shield",
    "Potion"
}

인덱스는 1부터 시작한다.

print(items[1])
print(items[2])

C/C++이나 JavaScript에 익숙하다면 이 부분은 처음에 헷갈릴 수 있다.

Lua -> 1, 2, 3, 4 ...
C / C++ -> 0, 1, 2, 3 ...

Table의 본질

Lua에서 테이블은 단순한 배열이 아니다.

하나의 자료구조로 다음과 같은 형태를 모두 표현할 수 있다.

Table -> 배열 / Dictionary / Map / Object / Struct 비슷한 구조 / Namespace

예를 들어:

local player = {
    name = "Knight",
    level = 10,
    inventory = {
        "Sword",
        "Potion"
    }
}

하나의 테이블 안에 다른 테이블을 넣을 수 있다.

이 때문에 Lua 코드에서 테이블을 중심으로 프로그램 구조가 만들어지는 경우가 많다.


Function

Lua의 함수는 값이다.

local function add(a, b)
    return a + b
end

함수를 변수에 저장할 수도 있다.

local operation = add

print(operation(10, 20))

익명 함수도 만들 수 있다.

local operation = function(a, b)
    return a + b
end

함수를 다른 함수에 전달하는 것도 가능하다.

이 특성 때문에 Lua에서 Callback이나 이벤트 시스템을 만들기 편하다.


Multiple Return

Lua 함수는 여러 개의 값을 반환할 수 있다.

local function getPlayer()
    return "Knight", 100
end

local name, health = getPlayer()

name에는 "Knight", health에는 100이 들어간다.

C++이나 Java 같은 언어에서는 구조체나 클래스를 만들어 여러 값을 묶는 경우가 많지만 Lua에서는 여러 반환값을 직접 사용할 수 있다.


Closure

Lua의 함수는 자신이 만들어진 환경을 기억할 수 있다.

local function createCounter()
    local count = 0

    return function()
        count = count + 1
        return count
    end
end

local counter = createCounter()

print(counter())
print(counter())

결과는 다음과 같다.

createCounter() -> count 생성 -> 내부 함수 반환 -> counter 유지 -> counter() -> count 증가 -> 1 -> counter() -> count 증가 -> 2

createCounter()가 종료된 이후에도 내부의 count가 유지된다.

이런 구조를 Closure라고 한다.

Callback, 상태 캡슐화, 간단한 Factory 등을 구현할 때 사용할 수 있다.


Metatable

Lua에서 객체지향적인 구조를 구현할 때 중요한 개념이다.

테이블은 기본적으로 일반적인 객체처럼 동작하지만, metatable을 연결하면 특정 연산이나 접근 방식을 변경할 수 있다.

local player = {}

local meta = {
    __index = function(table, key)
        return "Unknown"
    end
}

setmetatable(player, meta)

print(player.name)

존재하지 않는 필드에 접근하면 __index가 동작한다.

대표적인 Metamethod는 다음과 같다.

__index / __newindex / __add / __sub / __mul / __div / __eq / __lt / __le / __call

이를 이용하면 연산자 동작이나 객체 접근 방식을 변경할 수 있다.


객체지향 프로그래밍

Lua에는 Java나 C++처럼 전통적인 class 키워드가 없다.

대신 Table과 Metatable을 이용해 객체지향 구조를 구현할 수 있다.

local Player = {}
Player.__index = Player

function Player.new(name, health)
    local self = setmetatable({}, Player)

    self.name = name
    self.health = health

    return self
end

function Player:attack()
    print(self.name .. " Attack")
end

사용:

local player = Player.new("Knight", 100)

player:attack()

여기서:

function Player:attack()

:는 메서드 호출을 위한 문법이다.

실제로는 self가 자동으로 전달된다.

player:attack() -> player.attack(player)

와 같은 형태로 이해할 수 있다.


Module

Lua에서는 파일 단위로 코드를 모듈화할 수 있다.

예를 들어:

-- player.lua

local Player = {}

function Player.create(name)
    return {
        name = name
    }
end

return Player

다른 파일에서:

local Player = require("player")

local player = Player.create("Knight")

require()를 이용해 모듈을 가져온다.

Lua에서는 이런 식으로 기능을 파일 단위로 분리한다.


require

require()는 Lua의 모듈 시스템에서 중요한 함수다.

local Math = require("math_utils")

모듈을 로드하고 반환된 값을 가져온다.

보통 모듈 파일에서 테이블을 만들어 반환한다.

local M = {}

function M.add(a, b)
    return a + b
end

return M

이런 구조가 Lua에서 흔한 Module 패턴이다.


Error Handling

Lua에서는 error()를 이용해 오류를 발생시킬 수 있다.

error("Invalid player")

예외를 안전하게 처리해야 한다면 pcall()을 사용할 수 있다.

local success, result = pcall(function()
    error("Something went wrong")
end)

print(success)
print(result)

successfalse가 되고 오류 정보가 result에 들어간다.

복잡한 예외 시스템을 사용하는 언어와 비교하면 상당히 단순한 구조다.


Coroutine

Lua의 특징적인 기능 중 하나다.

Coroutine은 실행을 중간에 멈췄다가 다시 이어갈 수 있는 협력적 실행 구조다.

local co = coroutine.create(function()
    print("Start")

    coroutine.yield()

    print("Resume")
end)

coroutine.resume(co)
coroutine.resume(co)

실행 흐름은 다음과 같다.

resume -> Start -> yield -> 중단 -> resume -> Resume

일반적인 Thread와는 다른 개념이다.

Lua에서는 Coroutine을 이용해 상태 머신이나 비동기 처리, 스크립트 기반 연출 등을 구현할 수 있다.


Lua와 C/C++

Lua의 강점은 호스트 프로그램과 결합하기 쉽다는 것이다.

예를 들어 C++ 게임 서버나 게임 클라이언트가 있다고 하면 다음과 같은 구조를 만들 수 있다.

C++ Engine Core / Rendering / Network / Physics / Memory -> Lua Runtime -> Quest / NPC Logic / Event / Dialogue / Game Rules

무거운 시스템은 C++에서 처리하고 변경이 잦은 게임 로직은 Lua에서 처리하는 구조다.

C++ 코드에서 Lua VM을 실행하고 Lua 함수나 Lua 데이터를 호출하는 식이다.

이 구조의 장점은 게임을 다시 빌드하지 않고 스크립트만 변경해서 게임 로직을 수정할 수 있다는 것이다.


게임 개발에서 Lua를 사용하는 이유

게임에서 Lua가 사용되는 이유는 언어 자체가 특별히 게임 전용이어서가 아니다.

핵심은 호스트 프로그램과 스크립트를 분리할 수 있다는 것이다.

예를 들어:

function onPlayerEnter(player)
    if player.level >= 10 then
        giveQuest(player, 1001)
    end
end

게임 엔진의 핵심 시스템은 C++에 있고 Lua에서는 게임 규칙을 작성한다.

C++ -> Engine / Runtime / System
Lua -> Game Logic / Script / Event

이렇게 역할을 나누면 기획 변경이 잦은 부분을 스크립트로 관리하기 쉬워진다.

특히 NPC AI, 퀘스트, 대화, 이벤트, 스킬 로직, 맵 이벤트 같은 영역에서 활용할 수 있다.


Lua의 장점

Lua는 언어 자체가 작다.

문법이 복잡하지 않고 런타임도 비교적 가볍다.

그리고 C API를 통해 C/C++ 프로그램에 임베드하기 쉽다는 점이 큰 장점이다.

Lua -> 작은 Runtime -> Host Application에 삽입

이 구조 때문에 게임 엔진, 임베디드 시스템, 확장 스크립트 등에서 사용하기 좋다.

또한 동적 타입과 First-Class Function, Closure, Coroutine, Metatable 같은 기능을 제공하기 때문에 작은 언어임에도 상당히 유연한 프로그램을 만들 수 있다.


Lua의 한계

반대로 대규모 프로그램을 Lua만으로 구성하면 타입 안정성과 코드 구조 관리가 문제가 될 수 있다.

local player = {
    name = "Knight"
}

print(player.health)

health가 존재하지 않아도 코드를 작성하는 단계에서는 타입 시스템이 잡아주지 않는다.

프로젝트가 커지면 다음과 같은 문제가 생길 수 있다.

Dynamic Typing -> 런타임 오류 가능성 증가
전역 상태 -> 의존성 추적 어려움
Table 중심 구조 -> 데이터 구조 명확성 저하
스크립트 증가 -> 코드 관리 복잡도 증가

그래서 실제 엔진에서는 Lua를 모든 것을 구현하는 언어로 사용하기보다 C/C++ 같은 호스트 언어와 역할을 나누는 방식이 잘 맞는다.


Lua의 실행 구조

Lua를 하나의 구조로 정리하면 다음과 같다.

Host Application -> Lua Runtime -> Lua Script -> Function / Table / Coroutine -> Game Logic

게임 엔진이라면:

C++ Engine / Rendering / Physics / Network / Memory -> Lua Runtime -> Quest / NPC / Event / Skill / Dialogue

Lua는 이 구조에서 엔진 자체를 대체하기보다 엔진 위에서 동작하는 스크립트 계층으로 들어가는 경우가 많다.


Lua에서 기억할 것

Lua를 공부하면서 우선적으로 잡아야 할 개념은 많지 않다.

local -> 지역 변수
Table -> 배열 / Map / Object
Function -> First-Class Value
Closure -> 함수가 외부 상태를 유지
Metatable -> Table의 동작 확장
require -> Module
Coroutine -> 중단 / 재개 가능한 실행
C API -> Host Program과 연동

특히 Lua를 게임 개발 관점에서 본다면 Table, Function, Metatable, Coroutine, 그리고 C/C++과의 연동 구조를 이해하는 것이 중요하다.


정리

Lua는 문법이 작은 스크립트 언어지만, 내부 구조는 생각보다 유연하다.

Lua -> Dynamic Typing -> Table -> Function -> Closure -> Metatable -> Coroutine -> Module -> Host Application Integration

Lua의 핵심을 하나만 꼽으면 다른 프로그램에 삽입해서 동작시키기 좋은 언어라는 점이다.

C++로 엔진을 만들고 Lua로 게임 로직을 작성하는 구조를 생각하면 Lua가 왜 이런 문법과 기능을 가지고 있는지 이해하기 쉽다.

C++ -> 빠른 실행이 필요한 시스템 / 엔진 / 메모리 / 렌더링 / 네트워크
Lua -> 자주 변경되는 로직 / 퀘스트 / 이벤트 / NPC / 대화 / 게임 규칙

Lua는 C++ 같은 시스템 언어를 대체하려고 만들어진 언어가 아니다. 호스트 프로그램에 스크립팅 능력을 추가하고, 실행 중인 시스템의 일부 로직을 유연하게 교체하거나 확장하기 위한 언어에 가깝다.

그래서 Lua를 제대로 이해하려면 문법만 외우는 것보다 TableFunction을 중심으로 언어의 객체 모델을 이해하고, MetatableCoroutine, 그리고 C/C++과 Lua Runtime이 어떻게 연결되는지를 보는 것이 훨씬 중요하다.