콘텐츠로 건너뛰기
Home » vim 플러그인의 키맵과 함수 이름

vim 플러그인의 키맵과 함수 이름

vim 플러그인을 작성하는 것을 간단히 요약하면 어떤 기능을 수행하는 함수를 작성하고, 이 함수를 호출할 수 있는 방법을 제공해주는 것이라 할 수 있다. 이 때 고려해야하는 중요한 요소 중 하나는 사용자는 내가 작성하는 플러그인 말고도 엄청나게 많은 다른 플러그인을 사용하고 있을 것이라는 것이다. 따라서 함수의 이름이나 사용자 정의 명령의 이름, 키 맵핑등이 온전히 내가 원하는대로 사용자가 쓸 수 있을 것이라는 생각을 하는 것은 위험하다.

플러그인에서 어떤 함수를 작성했고, 이 함수를 호출하기 위한 사용자 명령을 제공한다고 가정해보자.

" SomePlugin.vim
function MySecretFunc() abort
  echom "hello"
endfunction
command SayHello call MySecretFunc()

간단해서 별로 문제가 되지 않을 것 같지만, 실제로는 예상대로 작동하지 않을 경우가 있다. 먼저 사용자가 vim에 MySecretFunc() 라는 이름의 함수를 이미 정의해 놓았다면, 위 함수 정의 구문은 적용되지 않을 것이다. 그렇다고 function! MySecretFunc() 라고 정의한다면 사용자의 기존 환경을 멋대로 바꾸는 것이 되어 문제를 일으키게 된다. 사용자 정의 명령이나 키 이름 같은 경우도 마찬가지이다. 중복된 이름이 있어서 에러가 나거나, 기존 사용자의 맵핑을 덮어 쓰게되어 의도치 않은 동작을 일으키게 될 것이다.

그래서 오늘은 vim 플러그인을 만들 때 이런 이름 충돌을 피하는 방법에 대해서 알아보도록 하겠다.

함수의 이름 충돌을 피하는 가장 좋은 방법은 함수가 스크립트 파일 밖에서는 보이지 않도록 하는 것이다. 함수를 정의할 때 함수의 이름 앞에 s: 를 붙이면 해당 함수는 스크립트 범위의 스코프를 가지게 되고, 이는 곧 해당 함수는 해당 스크립트 범위 내에서만 보이게 된다는 것이다. 함수의 범위가 스크립트로 제한된다면 다른 스크립트 파일에서는 같은 이름으로 된 함수를 정의하는 것이 전혀 문제가 되지 않을 것이다. 하지만 다른 문제가 있다. 스크립트 범위 내에 있는 함수를 어떻게 외부에서 호출할 수 있을까?

스크립트 범위 내의 함수는 스크립트 범위에서만 보인다. 하지만 키맵은 스크립트 스코프가 따로 존재하지 않는다. 이 말은 곧 키 맵을 적절히 사용하면, 함수 자체는 감추고 키맵으로만 해당 함수를 호출하게 하는 것이 가능하다는 이야기이다. s:....로 시작하는 함수는 키 맵에서는 <SID>를 붙여주면 된다. <SID>는 스크립트 ID를 말한다. 모든 vim 함수는 자신이 정의된 스크립트 문맥 상에서 실행되며 vim은 <SNR>*****_ 과 같은 접두어를 붙여서 스크립트 번호를 붙여서 다른 스크립트간 함수 이름의 충돌을 방지한다.

function s:my_secret_func() abort
  echom "hello world!"
endfunction
nnoremap <leader>sf :call <SID>my_secret_func()<CR>

이런 식으로 키맵을 만들면 해당 키 맵만 export 하는 효과가 있다. 하지만 여전히 키맵 이름이 중복되는 문제가 남는다.

플러그인 내부 키맵 정의하기

이를 위해서 vim은 <Plug>라는 내부 키맵핑을 따로 두고 있다. 이는 어떤 키 시퀀스에도 할당되지 않는 내부적인 맵핑으로 플러그인 내부에서 정의하고, 사용자는 자신의 고유한 키맵핑을 해당 <Plug> 맵핑에 연결하는 방식으로 사용할 수 있다. 앞서 말했듯이 플러그인에서 정의하는 모든 키맵과 마찬가지로 <Plug> 맵핑은 외부에 노출된다. <Plug> 자체는 <Esc><CR> 처럼 특정한 키를 가리키는 것처럼 생겼는데, 실제 키보드의 키가 만들지 않는 특별한 코드값에 해당된다.

<Plug>로 시작하는 맵핑은 외부에 노출되기 때문에 다른 플러그인과는 여전히 충돌할 수 있는 위험이 있다. 따라서 플러그인 작성자는 자신의 스크립트 이름과 맵 이름을 연결하여 고유한 이름이 만들어질 수 있도록 해야한다. 스크립트 이름이 “Typecorr” 이고 맵 이름을 “Add”로 하고자 한다면 해당 기능에 대한 맵은 <Plug>TypecorrAdd<Plug>Typecorr_add와 같은 식으로 정의해주면 된다. vim 도움말(:h using-<Plug>)에서는 <Plug> 다음에는 대문자가 와야한다고 하는데 괄호가 와도 상관없기 때문에 <Plug>(typecorr-add)와 같은 식으로 만들어도 된다.

function s:my_plugin_func() abort
    echom "hello"
endfunction
noremap <script> <Plug>(my-hello) :call <SID>my_plugin_func()<CR>

이렇게 생성된 플러그인용 맵핑은 사용자가 직접 해당 맵핑으로 다시 맵핑하여 사용하면 된다.

nmap <leader>aa <Plug>(my-hello)

autoload 활용하기

함수 이름을 피하는 또 다른 방법은 autoload 스크립트를 활용하는 것이다. autoload 폴더에 들어있는 스크립트들은 vim 실행시점에 로드되지 않고 특정한 규칙을 가진 이름의 함수가 호출될 때 로드된다. 그 이름 규칙은 # 문자로 구분되는 경로/파일 아래에 함수가 있는 구조이다.

예를 들어 myplugin#remove_tailing_spaces() 라는 함수를 호출하면 vim은 자동으로 autoload 디렉토리 아래에서 myplugin.vim 파일을 로드하고 해당 파일 내에 정의되어 있는 remove_trailing_spaces() 함수를 찾아서 호출한다. (만약 없다면 런타임 패스에 등록된 모든 autoload 디렉토리들을 뒤져볼 것이다.) autoload 디렉토리 아래에 다시 서브 디렉토리를 두는 것도 얼마든지 가능하며, 이 경우 myplugin#libs#helper#open_util_menu() 와 같은 식으로 autoload로부터 해당 함수까지 이르는 경로를 모두 적어주면 된다.

autoload 에서 사용되는 함수의 이름은 경로를 모두 포함하는 구조로 되어 있으므로, 이름 충돌이 일어날 가능성이 비교적 낮고 해당 함수는 스크립트 외부에서도 보이는 구조이기 때문에 적용해보는 것이 권장된다. 특히 vim의 초기 로딩시에 로드되지 않기 때문에 플러그인을 설치할 때 부담이 적다는 것도 장점이라 할 수 있겠다.