루아 레퍼런스 매뉴얼 — 언어: 어휘 규약과 변수
루아 레퍼런스 매뉴얼 — 언어: 어휘 규약과 변수
이번 글에서는 루아 소스 코드가 어떤 글자들로, 어떤 규칙에 따라 이루어지는지(어휘 규약, Lexical Conventions)와, 값을 담는 그릇인 변수(Variables)의 종류를 살펴볼게요. 루아를 처음 접하는 분이라면 "토큰이 뭐지?" 하는 순간이 올 텐데, 걱정하지 마세요. 하나하나 옆에서 설명해 드릴게요.
출처: Lua 5.4 Reference Manual — §3.1 Lexical Conventions, §3.2 Variables (https://www.lua.org/manual/5.4/manual.html)
본문
3.1 어휘 규약 (Lexical Conventions)
루아는 자유 형식(free-form) 언어예요. 다시 말해 어휘 요소(토큰) 사이에 있는 공백과 주석은 무시하는데, 딱 한 가지 예외가 있어요. 두 토큰을 구분해 주는 구분자로 쓰일 때는 무시하지 않죠. 예를 들어 abcde라는 한 토큰과 abc de라는 두 토큰은 서로 다르다는 뜻이에요.
소스 코드에서 루아가 공백으로 인식하는 문자는 표준 ASCII 공백 문자들입니다. 즉 **공백(space), 폼 피드(form feed), 줄바꿈(newline), 캐리지 리턴(carriage return), 가로 탭(horizontal tab), 세로 탭(vertical tab)**이에요. 참고로 탭 같은 곳에 이스케이프라는 말이 나오는데, "특수 문자를 글자 그대로 표현하는 문법"이라고 이해하시면 돼요.
이름(Names), 즉 식별자(Identifiers)
루아에서 **이름(names)**은 **식별자(identifiers)**라고도 부르는데, 라틴 문자, 아랍-인도 숫자, 그리고 밑줄(_)로 이루어진 문자열이에요. 단, 두 가지 제약이 있어요.
- 숫자로 시작하면 안 됩니다.
- 예약어(reserved word)면 안 됩니다.
식별자는 변수 이름, 테이블 필드, 라벨을 붙이는 데 쓰여요. 지금은 "이름을 붙이는 데 쓰이는 표기" 정도로만 기억해 두셔도 좋아요.
예약 키워드
다음 키워드들은 예약되어 있어서 이름으로 쓸 수 없어요.
and break do else elseif end
false for function goto if in
local nil not or repeat return
then true until while
if, local, return 같은 것들이 전부 여기 들어 있네요. 만약 and 같은 걸 변수 이름으로 쓰려고 하면 루아가 오류를 내요.
대소문자 구분
루아는 대소문자를 구분하는 언어예요. 즉 and는 예약어지만, And와 AND는 서로 다른, 유효한 이름이에요. 여기서 알 수 있듯 루아에서 변수 이름을 지을 때는 대소문자 철자가 아주 중요하니 주의하셔야 해요.
한 가지 관례가 있는데, 밑줄(_)로 시작하면서 그 뒤에 대문자가 하나 이상 붙는 이름(예: _VERSION)은 만들지 않는 게 좋아요. 그런 이름들은 보통 루아 내부에서 특별한 용도로 쓰이거든요.
기타 토큰을 나타내는 문자열
다음 문자열들은 다른 종류의 토큰을 나타내요. 전부 연산자나 구두점이라고 보시면 돼요.
+ - * / % ^ #
& ~ | << >> //
== ~= <= >= < > =
( ) { } [ ] ::
; : , . .. ...
여기 있는 ..는 문자열 연결, ::는 라벨 선언, ...는 가변 인자(vararg) 같은 데 쓰여요. 지금 다 외울 필요는 없고, "이런 것들이 토큰으로 인식되는구나" 정도만 봐 두세요.
짧은 리터럴 문자열 (Short Literal Strings)
짧은 리터럴 문자열은 서로 맞는 짝의 작은따옴표 또는 큰따옴표로 감싸서 만들 수 있고, 다음과 같은 C 계열 이스케이프 시퀀스를 포함할 수 있어요.
'\a': 벨(bell)'\b': 백스페이스(backspace)'\f': 폼 피드(form feed)'\n': 새 줄(newline)'\r': 캐리지 리턴(carriage return)'\t': 가로 탭(horizontal tab)'\v': 세로 탭(vertical tab)'\\': 백슬래시(backslash)'\"': 큰따옴표(double quote)'\'': 작은따옴표(apostrophe, single quote)
백슬래시 뒤에 줄바꿈이 오면 그 결과는 문자열 안의 **새 줄(newline)**이 돼요. 그리고 이스케이프 시퀀스 '\z'는 그 뒤에 이어지는 공백 문자들의 연속(줄바꿈 포함)을 건너뛰어요. 이건 긴 리터럴 문자열을 여러 줄로 나누어 들여쓰기(인덴트)하면서도, 그 공백과 줄바꿈이 문자열 내용에 들어가지 않게 할 때 특히 유용해요.
짧은 리터럴 문자열은 이스케이프되지 않은 줄바꿈이나, 유효한 이스케이프 시퀀스를 이루지 못하는 이스케이프를 포함할 수 없어요.
문자열 안에서 숫자로 바이트 지정하기
짧은 리터럴 문자열 안에서는 숫자 값으로 어떤 바이트든 지정할 수 있어요. 포함된 0(\0)조차도요. 방법은 두 가지예요.
\xXX:XX는 정확히 두 자리 16진수\ddd:ddd는 최대 세 자리 10진수
한 가지 주의할 점: 만약 10진수 이스케이프 시퀀스 뒤에 숫자가 바로 이어지게 하려면, 반드시 정확히 세 자리를 써서 표현해야 해요. 그래야 어디까지가 이스케이프인지 루아가 구분할 수 있거든요.
유니코드 문자 넣기
유니코드 문자의 UTF-8 인코딩은 이스케이프 시퀀스 \u{XXX}로 리터럴 문자열에 넣을 수 있어요. 여기서 {}(중괄호)는 반드시 있어야 해요. XXX는 해당 문자의 코드 포인트(코드값)를 나타내는 한 자리 이상의 16진수예요. 이 코드 포인트는 2³¹보다 작은 어떤 값이든 될 수 있어요. (이 부분에서 루아는 원래의 UTF-8 명세를 사용하는데, 그래서 유효한 유니코드 코드 포인트로 제한되지 않습니다.)
긴 리터럴 문자열 (Long Literal Strings)
리터럴 문자열은 **긴 괄호(long bracket)**로 감싼 **긴 형식(long format)**으로도 정의할 수 있어요.
레벨 n의 여는 긴 괄호는 "여는 대괄호([) + n개의 등호(=) + 또 하나의 여는 대괄호([)"로 정의해요. 그래서:
- 레벨 0의 여는 긴 괄호는
[[ - 레벨 1의 여는 긴 괄호는
[=[ - 이런 식으로 계속돼요.
닫는 긴 괄호도 비슷하게 정의돼요. 예를 들어 레벨 4의 닫는 긴 괄호는 ]====]로 써요.
긴 리터럴은 어떤 레벨의 여는 긴 괄호로 시작해서, 그 레벨의 첫 번째 닫는 긴 괄호에서 끝나요. 사이에는 같은 레벨의 닫는 괄호만 빼고 어떤 텍스트든 들어갈 수 있어요.
이 긴 괄호 형식의 리터럴은 여러 줄에 걸쳐 쓸 수 있고, 이스케이프 시퀀스를 해석하지 않으며, 다른 레벨의 긴 괄호는 무시해요. 줄 끝 시퀀스(캐리지 리턴, 새 줄, 캐리지 리턴+새 줄, 새 줄+캐리지 리턴)는 모두 단순한 새 줄 하나로 변환돼요.
또 한 가지: 여는 긴 괄호 바로 뒤에 새 줄이 오면, 그 새 줄은 문자열에 포함되지 않아요. 처음 [[ 다음에 바로 줄바꿈을 하고 내용을 쓰면 그 첫 줄바꿈은 무시된다는 뜻이에요.
예를 들어, ASCII를 쓰는 시스템에서('a'는 97, 새 줄은 10, '1'은 49로 코딩됨) 아래 다섯 리터럴 문자열은 모두 같은 문자열을 나타내요.
a = 'alo\n123"'
a = "alo\n123\""
a = '\97lo\10\04923"'
a = [[alo
123"]]
a = [==[
alo
123"]==]
여기서 \97은 a를, \10은 새 줄을, \049는 1을 숫자로 지정한 거예요. 그리고 [[ ... ]]나 [==[ ... ]==]처럼 긴 괄호를 쓰면 이스케이프 없이 그대로 쓸 수 있다는 걸 보여주죠.
바이트 단위의 자기 표현
앞의 규칙들의 영향을 받지 않는 리터럴 문자열의 모든 바이트는 그 자체를 나타내요. 다만 주의할 점이 있어요. 루아는 파싱을 위해 파일을 텍스트 모드로 열고, 시스템의 파일 함수가 일부 제어 문자에서 문제를 겪을 수 있어요. 그래서 바이너리 데이터는 비(非)텍스트 문자에 명시적인 이스케이프 시퀀스를 쓴 따옴표 리터럴로 표현하는 것이 더 안전해요.
수치 상수 (Numeric Constants)
**수치 상수(또는 숫자, numeral)**는 선택적인 분수 부분과 선택적인 10진 지수(문자 e 또는 E로 표시)를 붙여서 쓸 수 있어요.
루아는 또한 16진수 상수도 받아들이는데, 0x 또는 0X로 시작해요. 16진수 상수 역시 선택적인 분수 부분과 선택적인 2진 지수를 붙일 수 있는데, 이 지수는 문자 p 또는 P로 표시하고 10진수로 써요. 예를 들어 0x1.fp10은 1984를 나타내는데, 이는 0x1f / 16에 2¹⁰을 곱한 값이에요.
이제 수치 상수가 정수(integer)가 될지 실수(float)가 될지 정하는 규칙을 볼게요.
- 기수점(소수점)이나 지수가 있는 수치 상수 → 실수(float)
- 그렇지 않고 값이 정수에 맞거나 16진수 상수라면 → 정수(integer)
- 그렇지 않으면(즉, 넘쳐 흐른 10진 정수 숫자라면) → 실수(float)
기수점도 지수도 없는 16진수 숫자는 항상 정수 값을 나타내요. 값이 넘치면 감싸서(wrap around) 유효한 정수에 맞춰져요.
유효한 정수 상수의 예는 다음과 같아요.
3 345 0xff 0xBEBADA
유효한 실수 상수의 예는 다음과 같아요.
3.0 3.1416 314.16e-2 0.31416E1 34e1
0x0.1E 0xA23p-4 0X1.921FB54442D18P+1
주석 (Comments)
주석은 문자열 밖 어디에서나 이중 하이픈(--)으로 시작해요. -- 바로 뒤의 텍스트가 여는 긴 괄호가 아니면 그 주석은 **짧은 주석(short comment)**이 되어 해당 줄 끝까지 이어져요. 반대로 여는 긴 괄호라면 **긴 주석(long comment)**이 되어 대응하는 닫는 긴 괄호까지 이어져요.
게다가 긴 주석은 긴 리터럴 문자열과 같은 긴 괄호 문법을 쓰기 때문에, --[[ 여러 줄 주석 ]]처럼 여러 줄짜리 주석을 쉽게 쓸 수 있어요.
3.2 변수 (Variables)
변수는 **값을 저장하는 곳(place)**이에요. 루아에는 세 종류의 변수가 있어요.
- 전역 변수(global variables)
- 지역 변수(local variables)
- 테이블 필드(table fields)
단일 이름 하나가 전역 변수 또는 지역 변수(혹은 함수의 형식 매개변수, 즉 지역 변수의 특별한 종류)를 나타낼 수 있어요.
var ::= Name
Name은 식별자를 나타내고(§3.1 참조), 어떤 변수 이름이든 명시적으로 지역 변수로 선언하지 않으면 전역으로 간주돼요(§3.3.7 참조).
지역 변수는 **어휘적 스코프(lexically scoped)**를 가져요. 즉 자신의 스코프 안에서 정의된 함수는 지역 변수를 자유롭게 접근할 수 있어요(§3.5 참조). 그리고 변수에 **첫 할당이 일어나기 전에는 그 값이 nil**이에요.
대괄호([])는 테이블을 인덱싱할 때 사용돼요.
var ::= prefixexp '[' exp ']'
테이블 필드 접근의 의미는 **메타테이블(metatable)**을 통해 바뀔 수 있어요(§2.4 참조).
그리고 var.Name 문법은 var["Name"]의 **단지 문법적 설탕(syntactic sugar)**이에요. 다시 말해 점 표기법을 쓰면 내부적으로는 대괄호 인덱싱으로 바뀐다는 뜻이에요.
var ::= prefixexp '.' Name
마지막으로, 전역 변수 x에 대한 접근은 _ENV.x와 동등해요. 청크(chunk)가 컴파일되는 방식 때문에, _ENV 변수 자체는 절대 전역이 아니에요(§2.2 참조).
더 알아보기
- 3.3 Statements — 실제로 변수에 값을 할당하고, 지역 변수를 선언하는 문법을 이어서 살펴봐요.
- 2.4 Metatables · 2.2 Environments and the Global Environment — 테이블 접근을 바꾸는 메타테이블과,
_ENV가 어떻게 전역 환경을 다루는지에 대한 배경을 확인할 수 있어요. - 공식 Lua 5.4 Reference Manual — 이 글의 원문으로, 용어의 정확한 정의와 상호 참조를 제공해요.