Lua에 대해 간단하게 알아봅시다.

참고: 이 글은 Python을 깊게 다루지 않으며 Python을 주로 ‘C처럼’ 다루는 사람들을 대상으로 작성했습니다. 메타테이블, 상속, 코루틴과 같이 깊은 주제는 의도적으로 배제되었습니다. 제목이 for Python Users인 이유도 그래서입니다. 작성자는 Lua를 단 일주일 해봤으니 너그럽게 봐 주시고, 잘못된 점이 있으면 이메일 [email protected]로 연락주시기 바랍니다. 또한, 이 글은 제가 사용하는 LuaJIT 2.1.0-beta3를 기준으로 작성되었습니다.

Why?

Lua는 문법이 간단하며, 동적이라 자유도가 높고, 런타임이 아주 가벼우면서 빠릅니다(특히 LuaJIT을 사용할 경우). 그리고 C/C++/Rust 등의 정적 언어와 아주 잘 붙어서 이들의 보조적인 역할로 제격입니다.

맛보기

print("hello, world!")
for i=1,100 do
    if i%3 == 0 and i%5 ~= 0 then
    print("fizz")
    elseif i%3 ~= 0 and i%5 == 0 then
        print("buzz")
	elseif i%3 == 0 and i%5 == 0 then
        print("fizzbuzz")
    else
        print(i)
	end
end
function wow_iterator(max)
    local i = 0
    return function()
        i = i + 1
        if i <= max then
            return i
        end
        return nil -- optional but recommended
    end
end

for i in wow_iterator(3) do
    print(i)
end

기본 문법

변수

두 가지 종류가 있습니다.

  • 전역 변수: Python과 동일하게 name = "global"처럼 다른 키워드 없이 대입하면 자동으로 정의됩니다. 지역 변수보다 (아주 살짝) 느리기도 하고, Python과 다르게 모든 파일에 변수의 존재가 드러나므로 (유식하게 말해 모든 파일이 전역 변수의 이름공간을 공유하므로) 거의 사용하지 않습니다. 부득이하게 사용하게 되면 SCREAMING_SNAKE_CASE를 사용합시다.
  • 지역 변수: local name = "local"처럼 대입식 앞에 local 을 붙입니다. 해당 파일, 함수, 혹은 제어문 안에서만 유효합니다. local a, b = 1, 2 처럼 한 번에 여러 변수를 정의할 수도 있습니다. a=1, b=2는 동작하지 않습니다. 목록의 끝에 있지 않은 값을 버릴 때에는 주로 _를 변수 이름으로 하여 대입하는 편입니다. 예를 들자면, local _, a = something() 과 같이 작성합니다.

함수

함수도 동일하게 지역/전역 구분이 존재합니다. local 키워드가 없으면 전역이므로 지역 함수만 서술합니다.

local function say_hello(name)
    print("Hello, " .. name .. "!")
    return 1, 2, 3
end
a, b = say_hello("junghyun")

먼저, def 대신 function 키워드를 사용해 정의합니다. Python과 다르게 코드 묶음은 들여쓰기로 표현하지 않으며 그 끝을 end로 표시하기만 하면 됩니다. Python과 비슷하게 줄바꿈으로 코드 조각 하나의 끝을 표시합니다. 세미콜론(;)을 써서 한 줄에 여러 코드를 표시합니다.

뒤에서도 볼 수 있는데, 문법에 특수 문자를 거의 사용하지 않는 것이 특징입니다.

함수는 여러 값을 반환할 수 있습니다. Python과 다른 점은 ‘튜플’ 타입이 따로 존재하지 않고 그저 ‘여러 값을 반환하는 함수’ 가 존재할 뿐이라는 것입니다. 그 결과값을 여러 변수에 각각 담을 수 있는 것도 동일합니다. 수량이 맞지 않으면 (변수가 너무 많을 경우) nil로 채워지거나, (변수가 너무 적을 경우) 값을 버립니다. 위에서는 3 값이 버려졌습니다.

제어 구문

if

분기문입니다.

if condition then
    run-if-condition-is-true
elseif another-condition then
    run-if-another-condition-is-true
else
    run-otherwise
end

if ... then ... end가 기본 세트입니다. elif 대신 elseif라고 부릅니다.

while

반복문입니다.

while condition do
    do-while-condition-is-true
end

while ... do ... end가 한 세트입니다. Python과 동일하게 break을 사용할 수 있으나, continue는 존재하지 않습니다.

repeat

C의 do-while입니다. 조건과 관계없이 안의 코드를 최소 1회는 실행합니다.

repeat
    some-code
until condition

자주 쓰이지는 않습니다.

for (numeric)

범위 반복문입니다. 아래 코드는 Python의 for i in range(start, end_+1):와 대응됩니다. (end는 키워드이므로 변수명으로 사용할 수 없습니다.)

for i=start, end_ do
    some-code
end

여기서 주의할 점은, i의 범위가 닫힌 구간이라는 것입니다. 즉 start 이상 end_ 이하로, 이상-미만의 ‘반열린 구간’을 채택하는 대부분의 언어와는 조금 다릅니다.

Python과 동일하게 세 번째 값으로 step을 제시할 수도 있습니다.

for (generic)

Python의 for은 반복자(iterator)를 받아 반복자를 순회하는 제어문입니다. Lua의 generic for이 이에 대응됩니다.

for var_1, var_2, ..., var_n in iterator do
    some-code
end

이는 다음과 비슷합니다.

local var_1, var_2, ..., var_n = iterator()
some-code
local var_1, var_2, ..., var_n = iterator()
some-code
...

iterator가 꼭 단순 값일 필요는 없고 함수를 호출하는 수식일 수 있다는 점을 유의해야 합니다. 가장 많이 쓰는 함수 중 하나인 pairs의 예시를 들어보면:

for k, v in pairs(tbl) do
    some-code
end

local iterator = pairs(tbl);
local k, v = iterator()
some-code
local k, v = iterator()
some-code
...

와 대응됩니다.

실제 generic for의 의미는 조금 더 확장되어 있습니다. 궁금하시면 https://www.lua.org/pil/7.2.html 를 참고하시기 바랍니다.

do (보너스)

자주 쓰이지는 않지만, C의 { ... }처럼 scope를 지정하고 싶을 때 do ... end를 사용할 수 있습니다. 지역 변수와 함께 조합하면 지역 변수가 유효한 범위를 제한할 수 있으므로 유용합니다.

주석

한 줄 주석은 --으로 표시합니다. 여러 줄 주석은 --[[ ... ]] 사이에 글을 씁니다.

lua-language-server을 사용한다면, doccomment는 함수나 변수 선언 위에 작성하고, ---으로 시작하면 되며 --- 뒤에 띄어쓰기가 있어서는 안 됩니다.

자료형

어떤 값 value의 자료형은 type(value) 로 검사할 수 있습니다. "nil", "boolean", … "thread" 처럼 문자열 값이 반환됩니다.

nil

Python의 None에 대응됩니다. 존재하지 않는 값을 나타냅니다. 전역 변수나 테이블의 필드에 nil을 대입하면 해당 변수/필드를 없애는 의미가 있습니다.

boolean

true 혹은 false입니다. C와 비슷하게 소문자로 시작합니다.

number

숫자입니다. Lua에는 별도의 정수 타입이 없습니다1. 즉 Python의float라고 생각하면 됩니다. 실수 타입이어도 2^53-1까지는 정수와 동일한 성질을 가지기 때문에 큰 문제가 없습니다. 더 큰 정수가 필요하면 Lua 객체로 구현하거나 C 등으로 따로 정의해서 사용할 수도 있습니다2.

string

문자열입니다. UTF-8이어야 한다는 보장은 없고 아무 바이트열이나 담을 수 있습니다. 파이썬의 b""와 더 유사해 보입니다. ""''을 사용해 나타냅니다. Stylua 포매터의 기본값에 따르면 ""이 좀 더 선호됩니다.

function

함수입니다. Lua의 함수는 값으로 사용될 수 있습니다(유식하게 말해서 first-class value입니다). PIL의 표현을 빌리자면 변수에 저장될 수도 있고, 함수에 전달될 수도 있으며 반환값으로 쓰일 수도 있습니다.

Lua의 함수는 맥락을 보존합니다. 이를 closure라 합니다. 자세한 내용은 https://www.lua.org/pil/6.1.html 를 참고하시기 바랍니다.

table

가장 중요한 자료형입니다. Python에서 list에도 대응되고 dict에도 대응됩니다. 클래스 정의와 객체 지향 프로그래밍에도 사용됩니다. 위에서 나열한 기본적인 자료형을 묶을 수 있는 가장 보편적인 방법입니다.

테이블은 키-값 쌍입니다. 여기서 키에는 nil 빼고 아무 것이나 들어갈 수 있습니다: boolean, number, string, function, … 대부분은 number 혹은 string입니다.

테이블 정의에는 중괄호({, })를 사용합니다.

배열 혹은 리스트처럼 테이블을 정의하려면, 키 없이 순서대로 나열하면 됩니다. 이 때, 각 원소들의 인덱스는 1부터 시작합니다.

local array = {1, 2, 3}

딕셔너리처럼 테이블을 정의하려면, 키와 값을 나열하고 = 를 사이에 두면 됩니다.

local dict = { name = "John", age = 32 }

키에 문법적으로 표현 불가능한 문자가 있으면 [""]로 감쌉시다.

local dict = { ["first-name"] = "John", ["last-name"] = "Doe" }

눈치채셨겠지만, 문자열이 아닌 값이나 심지어 변수를 [] 자리에 넣어도 됩니다. 묘기니까 하지는 맙시다.

local a = "first-name"
local b = "last-name"
local dict = { [a] = "John", [b] = "Doe" }

당연히 빈 테이블을 선언하고( = {}) 나중에 값을 채울 수도 있습니다.

Python과 동일하게 테이블의 값에 접근할 때는 [] 으로 접근합니다. tbl["xyz"]tbl.xyz로 축약 가능합니다. tbl[xyz]와 혼동하지 않도록 주의하시기 바랍니다.

테이블의 값으로 함수를 주고 싶을 때는 다음과 같이 정의할 수도 있습니다.

function Table.function_name()
    some-code
end

Python의 ‘메서드’에 해당하는 함수 정의와 호출에는 구두점 대신 콜론을 사용합니다. self를 사용하는 점은 Python과 동일합니다. 아래 두 코드는 동일한 의미를 가집니다:

function Human.say_hello(self, name)
    self.last_name = name;
    print("hello, " .. name .. "!")
end

human = Human.create() -- creates a Human instance
human.say_hello(human, "Brian")
function Human:say_hello:(name) -- `self` is implicitly defined
    self.last_name = name;
    print("hello, " .. name .. "!")
end
human = Human.create() -- creates a Human instance
human:say_hello("Brian")

Python의 객체와 같이, 테이블 자체는 값이지만 그 안의 내용은 레퍼런스처럼 취급되기 때문에, Python에서 하던 것처럼 링크드 리스트나 트리를 구성하는 것도 가능합니다.

userdata

C 등 외부에서 유래한 값입니다.

thread

코루틴 객체로, LuaJIT에는 없는 타입입니다. 궁금하신 분들은 https://www.lua.org/pil/9.1.html 를 참고하시기 바랍니다.

자주 쓰는 함수

require

Python의 import와 같은 기능을 합니다. require("foo.bar")foo/bar.lua를 읽어서 실행합니다. 즉 그 파일이 반환하는 값이 결과값이 됩니다. 그래서 대부분 모듈을 정의할 때는 테이블을 만들고 반환하는 식으로 표현합니다. 이 때는 변수 M이 주로 사용됩니다.

local M = {}
function M.say_hello()
    print("hello")
end
return M

string.format(fmt, arg1, arg2, ...)

C의 sprintf와 유사합니다. 포맷 문자열을 사용해서 문자열에 값을 주입합니다. C와 서식 문자열을 동일하게 사용하므로 이를 참고하시기 바랍니다. https://en.cppreference.com/w/c/io/fprintf

pairs(tbl)

테이블의 키-값 쌍을 순회할 때 사용합니다. Python dict.items()를 생각하면 쉽습니다.

for k, v in pairs(tbl):
end

는 Python의

for k, v in dictionary.items():
    pass

와 비슷합니다.

ipairs(tbl)

테이블의 정수 키-값 쌍만 순회할 때 사용합니다.

for i, v in ipairs({1, 2, 3})
    print(string.format("%d -> %d", i, v))
end

error(message)

오류를 발생시킵니다. Python의 throw 구문과 비슷하게 pcall/xpcall을 만날 때까지 호출되었던 기록을 거슬러 올라갑니다. message는 꼭 문자열일 필요는 없습니다.

ok, ret = pcall(func, arg1, arg2, ...)

func(arg1, arg2, ...)을 실행시켜서 오류가 발생하면 첫 반환값을 false로 하고 오류의 내용을 두 번째 반환값으로 합니다. 오류가 발생하지 않으면 첫 반환값이 true가 되고 func의 반환값이 pcall의 두 번째 반환값이 됩니다.

이 외에도 xpcall, debug 내장 모듈, math 내장 모듈을 찾아보면 좋습니다.

자주 쓰는 도구

mlua

Rust와 Lua를 잇는 다리입니다. 사용이 상당히 간편해 작성자는 애용하고 있습니다.

lua-language-server

Lua 언어의 LSP 구현체입니다. VSCode나 Vim에 결합되어 IDE와 같은 경험을 제공합니다. 확장된 문법으로 어노테이션을 지원합니다.

Stylua

Black과 비슷한 Lua 언어의 포매터입니다. 포매팅 속도가 상당히 빠릅니다.

References


  1. Lua 5.3에 추가되었으나, 상술한 바 대로 여기서는 Lua 5.1 및 일부 5.2 문법을 차용한 LuaJIT을 기준으로 하여 설명합니다. ↩︎

  2. 여기에는 박싱으로 인한 성능 패널티가 존재하기 때문에 성능만을 위해 이렇게 하는 것은 큰 의미가 없을 것으로 생각됩니다(벤치마크해보지는 않았습니다). 작성자는 금융 계산을 위해 rust_decimal을 래핑해서 사용하고 있습니다. ↩︎