tools/vim

Learn Vimscript The Hard Way - 53. Autoloading

seul chan 2020. 5. 9. 18:49

Autoloading

이제 우리는 우리의 Potion 플러그인에 꽤 많은 기능을 추가하였다. 마치기 전에 중요한 기능을 더 추가하여 더 빛나게 만들어보자.

먼저 우리는 autoloading 기능을 추가하여 플러그인을 더 효율적으로 만들어 볼 것이다.

How Autoload Works

지금은 유저가 플러그인을 로드하면 (Potion 파일을 열면) 모든 기능이 한 번에 로드된다. 우리의 플러그인은 아직 작기 때문에 큰 문제가 되지 않지만, 큰 플러그인의 경우 모든 코드를 로드하는 데에는 꽤 많은 시간이 소요된다.

vim은 "autoload"를 사용하여 이 문제를 해결한다. Autoload는 코드 로딩을 실제로 필요할 때로 지연시킨다. 전체적으로는 별로 큰 퍼포먼스 향상이 이뤄지지 않을 수 있지만, 사용자가 당신의 플러그인의 모든 코드를 다 사용하지 않는다면 꽤 큰 속도 향상이 이뤄질 수 있다.

어떻게 동작하는지 알아보자. 다음 명령어를 살펴보자.

:call somefile#Hello()

해당 명령어를 실행할 때, vim은 일반 함수가 실행될 때와는 조금 다르게 동작한다.

만약 해당 함수가 이미 로드되었다면 vim은 단순히 이를 호출한다.

그렇지 않다면 vim은 ~/.vim 디렉토리(와 Pathogen, plug 등의 디렉토리)의 autoload/somefile.vim 파일을 찾아서 호출한다.

해당 파일이 존재하면, vim은 해당 파일을 load/source한다. 그리고 함수 호출을 시도한다.

파일 안에 함수는 다음과 같이 정의되어야 한다.

function somefile#Hello()
    " ...
endfunction

subdirectory를 명시하기 위해 함수명에 #를 여러 번 사용할 수 있다.

:call myplugin#somefile#Hello()

위 명령어는 autoload/myplugin/somefile.vim 파일을 찾아 autoload한다. 해당 파일 안에는 다음과 같이 경로가 구성되어야 한다.

function myplugin#somefile#Hello()
    " ...
endfunction

Experimenting

어떻게 동작하는지 느끼기 위해 한 번 시도해보자. ~/.vim/autoload/example.vim 파일을 만들고 다음을 추가해보자.

echom "Loading..."

function! example#Hello()
    echom "Hello, world!"
endfunction

echom "Done loading."

파일을 저장하고 :call example#Hello()를 호출해보자. 다음 내용이 출력되는 것ㅇ르 볼 수 있다.

Loading...
Done loading.
Hello, world!

이는 다음과 같은 내용을 보여준다.

  1. vim은 그때그때 확인하면서 example.vim을 호출한다. 이는 vim 이 열렸을 때에는 존재하지 않았기 때문에 시작할 때에는 로드되지 않는다.
  2. vim이 autoload가 필요할 때, 함수를 호출하기 전에 전체 파일을 로드한다.

"Vim을 닫지 말고," 함수의 정의를 다음과 같이 변경해보자.

echom "Loading..."

function! example#Hello()
    echom "Hello AGAIN, world!"
endfunction

echom "Done loading."

파일을 저장하고 "Vim을 닫지 말고" :call example#Hello()를 호출해보자. 다음이 출력될 것이다.

Hello, world!

vim은 이미 example#Hello 함수를 알고 있기 때문에 이를 다시 로드하지 않는다.

  1. 함수 밖의 코드는 다시 실행되지 않는다.
  2. 함수의 변경사항을 적용시키지 않는다.

이제 :call example#BadFunction()을 실행해보자. 에러가 나타나기 전에 loading message가 출력되는 것을 볼 수 있다. 다시 :call example#Hello()를 실행해보자. 업데이트 된 메세지가 출력되는 것을 볼 수 있다!

Loading...
Done Loading.
E117: 모르는 함수: example#BadFunction

이제 vim이 autoload-style 함수를 호출 할 때 어떤 일이 발생하는지 대략적으로 알게 되었을 것이다.

  1. 해당 이름의 함수가 이미 정의되었는지 확인한다. 만약 그렇다면, 이를 호출한다.
  2. 그렇지 않으면 함수 이름을 기반으로 파일을 찾아 source 시킨다.
  3. 그리고 함수 호출을 시도한다. 작동하면 잘 된 것이고, 실패하면 에러 메세지를 출력한다.

만약 개념이 완전히 잡히지 않았다면 다시 돌아가서 몇가지 실험을 해 보고 어떻게 작동하는지 이해해보자.

What to Autoload

Autoloading은 공짜가 아니다. 코드에 이상한 함수 이름이 사용되는 것은 물론이고 (조금의) 오버로드도 있다.

유저가 vim 세션을 열 대마다 사용하는 플러그인이 아니라면 함수를 최대한 autoload 파일로 옮기는 것이 좋다. 이는 당신의 플러그인이 사용자의 startup 시간(사용자가 많은 vim plugin을 설치하는 데 아주 중요한)에 미치는 영향을 감소시켜준다.

그럼, 어떤 종류의 것들이 안전하게 autoload 될 수 있을까? 답은 기본적으로 유저가 직접 호출하지 않는 모든 것이다. 매핑이나 custom command는 autoload 될 수 없다. (유저가 직접 호출하면 autoload는 작동하지 않기 때문에?)

우리의 potion plugin을 살펴보고 어떤 것들이 autoload 될 수 있는지 살펴보자.

Adding Autoloading to the Potion Plugin

먼저 complie and run 기능부터 시작할 것이다. 저번 장에서 작업한 ftplugin/potion/running.vim 파일은 다음과 같다.

if !exists("g:potion_command")
    let g:potion_command = "potion"
endif

function! PotionCompileAndRunFile()
    silent !clear
    execute "!" . g:potion_command . " " . bufname("%")
endfunction

function! PotionShowBytecode()
    " Get the bytecode.
    let bytecode = system(g:potion_command . " -c -V " . bufname("%"))

    " Open a new split and set it up.
    vsplit __Potion_Bytecode__
    normal! ggdG
    setlocal filetype=potionbytecode
    setlocal buftype=nofile

    " Insert the bytecode.
    call append(0, split(bytecode, '\v\n'))
endfunction

nnoremap <buffer> <localleader>r :call PotionCompileAndRunFile()<cr>
nnoremap <buffer> <localleader>b :call PotionShowBytecode()<cr>

이 파일은 Potion 파일이 로드되면 이미 한 번 호출되기 때문에 vim이 시작될 때 따로 오버헤드를 주지는 않는다. 하지만 몇몇 사용자들은 이 기능이 필요없을 수 있기 때문에 이를 autoload로 변경하여 Potion 파일을 열 때 마다 몇 밀리세컨드 정도를 절약해 줄 수 있다.

그렇다. 이 케이스에서는 큰 절약이 이뤄지지는 않는다. 하지만 로드되는데 아주 많은 시간이 필요한 몇천줄 짜리 코드에서는 효과가 훨씬 큰 것을 상상할 수 있다.

autoload/potion/running.vim 파일을 열고 아래 두 함수를 이름을 바꿔 옮겨보자.

echom "Autoloading..."

function! potion#running#PotionCompileAndRunFile()
    silent !clear
    execute "!" . g:potion_command . " " . bufname("%")
endfunction

function! potion#running#PotionShowBytecode()
    " Get the bytecode.
    let bytecode = system(g:potion_command . " -c -V " . bufname("%"))

    " Open a new split and set it up.
    vsplit __Potion_Bytecode__
    normal! ggdG
    setlocal filetype=potionbytecode
    setlocal buftype=nofile

    " Insert the bytecode.
    call append(0, split(bytecode, '\v\n'))
endfunction

potion#running 부분은 해당 파일이 존재하는 위치에 따라 상대적이라는 것을 기억하라. 이제 ftplugin/potion/running.vim 파일에서 함수를 제거하고 다음과 같이 수정해보자.

if !exists("g:potion_command")
    let g:potion_command = "potion"
endif

nnoremap <buffer> <localleader>r
            \ :call potion#running#PotionCompileAndRunFile()<cr>

nnoremap <buffer> <localleader>b
            \ :call potion#running#PotionShowBytecode()<cr>

파일을 저장하고, vim을 닫고 factorial.pn 파일을 열어보자. <localleader>b, <localleader>r 매핑을 사용하여 여전히 잘 작동하는지 확인해보자.

우리가 추가한 Autoloading... 메세지가 잘 나오는지 확인해보자. 첫 매핑을 사용할 때만 나올 것이다. (메세지를 보려면 :messages 명령어를 실행해야 할 것이다.) 해당 메세지가 잘 나오고 autoload가 잘 동작하는 것을 확인한 이후에 해당 메세지를 제거해주자.

위에서 본 것처럼, 우리는 nnoremap은 그대로 남겨두었다. 이를 autoloading 시켜버리면 유저가 해당 함수를 initiate 할 방법이 없기 때문이다!

이는 vim plugin들에서 쉽게 볼 수 있는 흔한 패턴이다. 함수의 많은 부분은 autoloaded 함수로 구성되 어 있고, nnoremap이나 command 명령어들만 vim이 매번 로드하는 파일에 남아있다. vim 플러그인을 작성할 때마다 이를 기억하자.

Exercises

  • Read :help autoload.

  • Experiment a bit and find out how autoloading variables behaves.

아래와 같이 경로를 변수로 설정하여 사용해도 된다. 긴 서브디렉토리를 중복으로 사용할 때 이런 방식으로 사용하면 좋을듯.

let runningFile = "potion#running"

nnoremap <buffer> <localleader>r
    \ execute ":call " . runningFile . "PotionCompileAndRunFile()<cr>"
  • Suppose you wanted to programatically force a reload of an autoload file Vim has already loaded, without bothering the user. How might you do this? You may want to read :help :silent. Please don't ever do this in real life.