포스트

Lua 종합 가이드 (Neovim 컨텍스트)

Neovim 설정·플러그인을 읽고 고치는 수준까지 Lua 문법을 한 번에 정리. 타입·스코프·테이블·문자열 패턴, 자주 쓰는 vim.* 헬퍼와 함정까지.

Lua 종합 가이드 (Neovim 컨텍스트)

Neovim 설정·플러그인을 읽고 고치는 데 필요한 Lua 문법을 한 번에 정리한 글이다. LuaJIT(Lua 5.1 호환) 기준 — 5.3에서 추가된 //, <const>, bitwise ~/&/|는 쓸 수 없다. 비트 연산은 LuaJIT 내장 bit 모듈(bit.band, bit.bor)을 쓴다.

심화 주제(메타테이블·모듈·에러 처리)는 별도 글에서 따로 다룬다 — 본문 끝의 Lua 시리즈 섹션 참고.

8개 기본 타입

type(x)로 확인한다.

타입예시비고
nilnil부재. 변수 선언만 하면 nil
booleantrue / falsefalsenil만 falsy. 0, "", {}는 모두 truthy
number1, 1.5LuaJIT은 정수/실수 구분 없음 (IEEE 754 double)
string"hi", 'hi', [[...]]불변. s:upper() 가능
functionfunction(x) ... end일급 값
table{1, 2}, {a=1}자료구조의 거의 전부
userdata(C 핸들)vim.loop 핸들 등
threadcoroutine.create(...)코루틴

변수와 스코프

local을 안 붙이면 전역으로 샌다. Neovim 설정에서는 거의 모든 변수를 local로 둔다.

1
2
3
4
x = 10              -- 전역 (_G.x)
local y = 20        -- 지역 (블록 단위)
local a, b, c = 1, 2, 3   -- 다중 할당
a, b = b, a         -- 스왑 (다중 반환과 결합 시 강력)

스코프는 do ... end, if ... end, for ... end, while ... end, function ... end 단위다.

제어 흐름

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if x > 0 then
elseif x < 0 then
else
end

while cond do ... end

repeat ... until cond    -- 조건이 참이면 멈춤 (while과 반대)

for i = 1, 10 do ... end       -- 끝값 포함
for i = 1, 10, 2 do ... end    -- step
for i = 10, 1, -1 do ... end   -- 역방향

for i, v in ipairs(t) do ... end   -- 배열 (nil에서 멈춤)
for k, v in pairs(t) do ... end    -- 전체 (순서 X)

and/or마지막 평가값을 반환한다(boolean 강제 X). 삼항 흉내:

1
2
local label = (count > 0) and "있음" or "없음"
-- 가운데가 falsy면 무너진다 — 그땐 if 사용

continue가 없다. 우회 두 가지:

1
2
3
4
5
6
7
8
9
10
11
-- 1. if로 본문 감싸기
for _, item in ipairs(items) do
  if item.enabled then ... end
end

-- 2. goto continue (Lua 5.2+, LuaJIT 지원)
for _, item in ipairs(items) do
  if not item.enabled then goto continue end
  ...
  ::continue::
end

return블록의 마지막이어야 한다. 중간 종료는 do return end 트릭으로.

함수

1
2
3
function f(x) return x + 1 end           -- 전역
local function g(x) return x + 1 end     -- 지역 (재귀 안전)
local h = function(x) return x + 1 end   -- anonymous (재귀 위험)

다중 반환

1
2
local function divmod(a, b) return math.floor(a/b), a%b end
local q, r = divmod(10, 3)

다중 반환은 표현식의 마지막 위치일 때만 전부 보존된다. 중간 위치면 첫 값만 전달:

1
2
3
4
print(f())         -- 1 2 3
print(f(), 99)     -- 1 99   (중간 → 첫 값만)
local t = { f() }      -- {1, 2, 3}
local t = { f(), 99 }  -- {1, 99}

가변 인자

1
2
3
4
5
6
local function logger(level, ...)
  local args = { ... }
  print(#args)              -- nil 섞이면 부정확
  print(select("#", ...))   -- 정확한 개수
  print(select(2, ...))     -- 2번째부터
end

클로저

1
2
3
4
5
6
7
8
9
local function counter()
  local n = 0
  return function()
    n = n + 1
    return n
  end
end
local c = counter()
print(c(), c(), c())   -- 1  2  3

같은 호출에서 만든 함수는 같은 upvalue를 공유하고, 다른 호출은 별개 upvalue를 가진다.

메서드 호출 — : vs .

t:method()t.method(t)의 sugar다. 정의도 :로 했어야 self 인자가 매칭된다.

1
2
3
function t:greet(name)  return "hi " .. name end
t:greet("neo")          -- self가 자동 전달
t.greet(t, "neo")       -- 위와 동등

테이블

1
2
3
4
5
local arr = { 10, 20, 30 }                 -- 배열 (1-indexed)
local map = { name = "neo", age = 30 }
local mixed = { 1, 2, name = "neo" }       -- 섞어도 됨
local k = "dynamic"
local t = { [k] = 1, ["with-hyphen"] = 2 } -- 동적 키

인덱스는 1부터 시작한다. arr[0]은 nil.

#t 함정

#t연속된 정수 키의 길이다. 중간에 nil이 있는 sparse table에서는 부정확하다.

1
2
print(#{1, 2, 3})         -- 3 (안전)
print(#{1, nil, 3})       -- 1 또는 3 (구현 의존)

ipairs vs pairs

  • ipairs(t): 1부터 nil까지, 정수 키만, 순서 보장
  • pairs(t): 모든 키, 순서 보장 없음
  • 가장 단순한 형태: for i = 1, #t do
1
2
for i, v in ipairs({1, 2, nil, 4}) do print(i, v) end
-- 1 1 / 2 2 (nil에서 멈춤, 4는 못 봄)

table.* 라이브러리

함수역할
table.insert(t, v)끝에 추가
table.insert(t, pos, v)위치 삽입
table.remove(t) / table.remove(t, pos)제거하고 반환
table.concat(t, sep)배열 → 문자열
table.sort(t, cmp)제자리 정렬
table.unpack(t) / unpack(t)가변 인자로 펼침

참조 vs 복사

1
2
3
local b = a              -- 참조 (같은 객체)
print(a == b)            -- true (같은 참조)
print(a == { 1, 2, 3 })  -- false (다른 객체)

깊은 복사는 vim.deepcopy(t).

문자열

기본 함수

함수역할
#s / string.len(s)바이트 길이 (UTF-8 코드포인트 X)
string.sub(s, i, j)부분 문자열 (1-indexed, j 포함)
string.upper(s) / string.lower(s)대소문자
string.rep(s, n, sep)반복
string.reverse(s)뒤집기
string.byte(s, i) / string.char(n)코드 ↔ 문자
string.format(fmt, ...)printf 스타일

string.format 지정자

지정자의미
%d / %i정수
%f / %.2f실수 / 소수점 2자리
%s문자열
%q안전한 따옴표 escape
%x / %X16진수
%%리터럴 %
%5d / %-5d너비 5 우/좌 정렬

Lua 패턴 (정규식이 아님)

escape가 \가 아니라 %다. |(or), {n,m}, lookahead가 없다.

문자 클래스:

클래스의미반대
%a 알파벳%d 숫자%A / %D
%w 영숫자%s 공백%W / %S
%l / %u 소/대문자%p 구두점%L / %U
%x 16진수. 아무 문자%X

수량자: * 0+ greedy, + 1+ greedy, - 0+ lazy, ? 0/1.

앵커·캡처:

  • ^pattern / pattern$ — 시작/끝
  • %b() — 짝 맞춘 괄호
  • %f[set] — frontier
  • (pattern) — 캡처
1
2
3
print(string.match("name=neo", "(%w+)=(%w+)"))   -- name  neo
print(string.match("<b>x</b>", "<.->"))           -- <b>    (lazy)
print(string.match("<b>x</b>", "<.*>"))           -- <b>x</b> (greedy)

매칭 함수

1
2
3
4
5
6
7
8
9
10
11
12
string.match(s, pat)            -- 첫 매치 (또는 캡처)
string.find(s, pat)             -- 위치 반환 (i, j)
string.find(s, pat, 1, true)    -- plain (리터럴 검색)

for w in string.gmatch(s, "%w+") do ... end   -- 이터레이터

s, n = string.gsub(s, pat, repl)
-- repl: 문자열("%1:%2"), 함수, 테이블
print((string.gsub("a1b2", "%d", function(d) return d*10 end)))
-- a10b20
print((string.gsub("hello", "%w+", { hello = "안녕" })))
-- 안녕

에러 처리 (요약)

1
2
3
4
5
6
local ok, err = pcall(require, "optional-plugin")
if not ok then return end

local ok, result = pcall(function()
  return compute()
end)

pcall/xpcall로 잡고, error/assert로 던지는 게 기본. 에러 객체 패턴, traceback 핸들러, Neovim 플러그인의 에러 관행은 별도 글에서 자세히 다룬다 → Lua 문법 8 — 에러 처리.

Neovim 자주 쓰는 헬퍼

vim.tbl_*

함수역할
vim.tbl_extend(mode, ...)얕은 병합
vim.tbl_deep_extend(mode, ...)깊은 병합
vim.tbl_contains(t, v)값 포함
vim.tbl_keys(t) / vim.tbl_values(t)키/값 배열
vim.tbl_filter(fn, t) / vim.tbl_map(fn, t)필터 / 매핑
vim.tbl_isempty(t)비었는지
vim.deepcopy(t)깊은 복사

mode: "force" 뒤쪽 우선 / "keep" 앞쪽 우선 / "error" 충돌 시 에러.

1
local merged = vim.tbl_deep_extend("force", defaults, user)

vim.* 자주 쓰는 것

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local opt = vim.opt
local g = vim.g
local bo = vim.bo[bufnr]                -- 버퍼 옵션
local wo = vim.wo[winid]                -- 윈도우 옵션

vim.keymap.set("n", "<leader>w", function() vim.cmd.write() end, { desc = "Save" })

vim.api.nvim_create_autocmd("BufWritePre", {
  pattern = "*.py",
  callback = function(args) ... end,
})

vim.schedule(function() ... end)        -- 다음 이벤트 루프
vim.notify("msg", vim.log.levels.INFO)

vim.fn.matchstr("abc123", "\\d\\+")     -- Vim regex (Lua 패턴 아님)

Lua 패턴 활용

1
2
3
4
5
6
7
8
-- basename
local function basename(path) return path:match("([^/\\]+)$") or path end

-- 확장자
local function ext(path) return path:match("%.([^.]+)$") end

-- 메시지 파싱
local file, line, col, text = msg:match("([^:]+):(%d+):(%d+):%s*(.+)")

함정 정리

  1. local을 빠뜨리면 전역으로 샌다.
  2. 0, "", {}truthy.
  3. 다중 반환은 표현식 마지막 위치에서만 전부 보존된다.
  4. {...}는 nil이 섞이면 부정확 → select("#", ...).
  5. :로 호출하면 self가 자동 전달되며, 정의도 :로 해야 매칭된다.
  6. 인덱스 1부터다. #t는 sparse table에서 부정확.
  7. ipairs는 nil에서 멈춘다.
  8. escape는 \가 아니라 %. |(or) 없음.
  9. #s바이트 길이, UTF-8 코드포인트 수가 아니다.
  10. gsub은 다중 반환(new, count) → print((s:gsub(...)))로 두 번째 값을 잘라낸다.

Lua 시리즈

다루는 것
Lua 종합 가이드 (Neovim 컨텍스트) (현재 글)LuaJIT(5.1) 문법 한 번에 정리 — 타입·스코프·테이블·문자열 패턴·vim.* 헬퍼
Lua 모듈require/package.path, local M = {} return M 패턴, Neovim lua/ 자동 등록
Lua 메타테이블__index/__newindex/__call, OOP 클래스 패턴, vim.opt가 일반 테이블처럼 보이는 이유
Lua 에러 처리error/assert로 던지고 pcall/xpcall로 잡기. Neovim 플러그인의 에러 관행
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.