콘텐츠로 건너뛰기
Home » vim의 키 맵 설정 팁

vim의 키 맵 설정 팁

vim에서 키맵을 잘 활용하는 것의 중요성은 딱히 말하지 않아도 될 것 같으니, 키 맵을 설정하는데 있어서 몇 가지 알아두면 좋은 팁들을 소개하고자 한다.

키 맵 작성 방법

키 맵은 :map 명령을 사용해서 정의할 수 있다. :map A B의 형식이다. A는 사용할 키 시퀀스이고 B는 A를 입력했을 때 작동할 키 시퀀스이다. 여기서 A, B가 모두 키 시퀀스라는 점을 잘 알아두자. 특정한 키 하나를 눌러서 동작을 실행하는게 아니라, 두 개 이상의 키를 순서대로 눌러서 동작을 실행할 수 있다. 따라서 실제로 vim의 키맵은 단순한 단축키 이상의 의미를 갖는다 할 수 있겠다.

" <F2> 키를 눌러서 현재 버퍼를 저장하고 닫기
:map  <F2>  :wq<CR>

특수키 표현

일반적인 키들은 문자 그대로와 매칭한다. 예를 들어 k는 k 키를 누르는 것을 의미한다. K는 shift + K를 누른것으로 볼 수 있다. 그외에 스페이스 바, ESC, 엔터키나 F1, F2와 같은 기능키들은 < > 안에 해당 키의 이름을 쓴다. (키 이름은 대소문자를 구분하지 않는다.)

  • <CR> : 엔터키
  • <Esc> : ESC 키
  • <Tab> : 탭 키
  • <BS> : 백스페이스 키
  • <Space> : 스페이스 바
  • <F1> : F1 키

그외에 Ctrl, Alt키와 조합한 키도 사용할 수 있다. <c-j>는 Ctrl+j 에 해당하며, <a-j>, <m-j>는 Alt+j에 해당하는 식이다.

맵의 모드

vim에서는 같은 키라도 현재 모드에 따라서 작동하는 방식이 다르다. 예를 들어 j키는 커서를 한 줄 아래로 내리는 동작으로 알고 있지만, 이것은 일반 모드일 때이고, 입력모드에서 j는 문자 “j”를 삽입한다. 기본적으로 :map 명령으로만 키 맵을 정의하면 이것은 일반 모드와 비주얼모드(선택모드)에서 작동하게끔 등록된다.

  • :map : 일반, 비주얼
  • :map! : 입력, 명령줄 모드
  • :imap : 입력모드
  • :nmap : 일반모드
  • :vmap : 비주얼모드
  • :cmap : 명령줄 모드
  • :tmap : 터미널 모드

noremap

vim 키맵 설정에서 가장 중요한 지점이라 할 수 있다. :map 명령을 그냥 절대로 사용하지 말아야 한다. 예를 들어 :map dd jdd 라는 키 맵을 설정했다고 가정하자. 이 상황에서 dd를 누르면 먼저 j에 의해서 한 줄 아래로 내려간 다음 dd가 다시 입력된다. 그런데 ddjdd로 맵핑되어 있으므로 계속해서 j만 눌러지는 상황이 발생한다. 이런 순환을 피하기 위해서는 :noremap 명령을 사용한다. noremap A B 명령은 B에서 눌려지는 키시퀀스에 의해서는 키맵이 발동되지 않도록 한다. 기억하자. 항상 :map 대신 :noremap을 사용하자.

한가지 예외라면 <Plug>로 시작하는 플러그인용 키맵을 호출하는 경우인데, 이 경우를 제외하면 항상 nnoremap을 써야 한다.

키 맵 옵션

:map 옵션 A B 의 형태로 키맵을 정의하여 키맵의 옵션을 지정할 수 있다. 사용 가능한 옵션은 다음과 같다.

  • <silent> : 키 맵이 발동되었을 때, 입력되는 키 시퀀스가 명령줄에 표시되지 않도록 한다.
  • <buffer> : 해당 키 맵을 현재 버퍼에 지역적으로 정의한다.
  • <nowait> : 키맵 입력 후에 더 긴 키맵의 입력을 기다리지 않는다.
  • <expr : 표현식으로 키 시퀀스를 대체할 수 있다.
  • <script> : 플러그인이나 스크립트에서 사용하는 키맵을 정의한다.

:map ,h /Header<CR> 이라는 키 맵을 정의해서 호출하면, 화면 아래 명령 입력 줄에 /Header라고 입력한 내용이 표시된다. :map <silent> ,h /Header<CR>으로 정의하면 이 내용이 에코되지 않는다.

<buffer>는 버퍼 범위에서만 사용 가능한 키맵을 정의한다. 전역 키맵과 동일한 키맵이 있는 경우에는 해당 버퍼에서는 로컬 버퍼가 더 우선해서 사용된다.

<nowait> 키맵은 이후 입력을 기다리지 않는다. 예를 들어서 , 라는 키맵이 있고 콤마로 시작하는 다른 키맵이 없다면, ,를 입력하는 시점에 맵핑된 키 시퀀스가 즉시 발동된다. 그런데 ,h,j 같은 키맵이 추가적으로 존재하는 경우에는 사용자가 ,키를 눌렀을 때, vim은 사용자가 , 키맵을 사용하려는지, ,h, ,j 중 하나를 사용하려는지 알 수 없다. 따라서 다음 키가 눌려질 때까지 기다리게 된다. 이 값은 'timeout' 옵션에 의존한다. 예를 들어 ,h, ,j, , 키 맵이 있는 경우에 ,를 사용하려는 경우에는 사용자는 강제로 1초 정도를 기다려야 키맵이 발동되는 것이다. <nowait>는 해당 키맵이 완성되는 순간에 그보다 더 긴 키맵을 무시하고 바로 키맵을 발동한다.

<expr> 키맵

:map 명령에 <expr> 옵션이 주어지면, 문자열로 키 시퀀스를 할당하며 이 때의 문자열은 vimscript의 표현식이다. 즉 문자열 표현이나, 함수의 실행, 연산 결과로 동적으로 키 시퀀스를 만드는 것이 가능하다. 이 키맵은 주로 삽입 모드용 키 맵에서 흔히 쓰인다. 예를 들어 다음 명령은 삽입 모드에서 자동완성 중에 팝업이 떠 있는 상황에서 탭 키를 누르면 <c-p> 처럼 작동하게끔 하는 키 맵이다.

:inoremap <expr> <Tab>  pumvisible() ? "\<C-p>" : "\<Tab>"

pumvisible() 함수는 팝업 메뉴가 표시중인지를 체크한다. 따라서 삽입 모드에서 탭 키를 누를 때마다 이 표현식이 평가되면서 팝업 메뉴가 떠 있는 상태라면 <C-p>키가 발동되고, 그렇지 않은 경우에는 <Tab> 키가 발동된다. 특별히 키 맵 문자열 표현식에서 < .. > 으로 둘러싸는 부분은 백슬래시로 시작해야 하는 부분은 기억해두자.

<script> 키맵

<script> 옵션은 도움말을 읽어봐도 잘 이해가 가지 않는 부분이다. 먼저 :map 명령에서 정의하는 키 맵에 대해 누를 키를 좌변, 발동할 키 시퀀스를 우변이라고 하자. 이 때 <script>는 우변 중에서 <SID>로 시작하는 다른 맵핑의 좌변이 있다면, 그 맵핑만 재귀적으로 다시 맵핑하게 해준다. 앞서 언급했던 jdd 동작을 실제로 의도대로 작동하도록 하는 예를 아래에 작성해보면 다음과 같다.

noremap <SID>(MoveDown) j
nmap <script> dd <SID>(MoveDown)dd

첫번째 명령은 “<SID>(MoveDown)”이라는 특수한 맵핑을 만든다. 이 맵은 스크립트 전용이다. 그리고 두 번째 명령에서는 <script> 옵션을 써서 “dd”라는 새 맵핑을 정의한다. 이 때 우변의 dd는 줄을 삭제하는 원래의 키로 작동하지만 “<SID>(MoveDown)” 부분만은 바로 윗줄에서 선언한 키 맵으로 변환된다.

즉 <script>의 의미는 우변에서 <SID>로 시작하는 스크립트 맵핑에 대해서만 재맵핑을 하겠다는 의미로 해석된다. 따라서 <script>가 붙은 :map 명령은 실제로는 :noremap 명령과 동일한 효과를 보인다고 볼 수 있으며, 예외적으로 <SID>로 시작하는 스크립트 내에서 정의한 다른 키맵만 재맵핑한다. 이를 통해서 사용자가 vimrc에서 임의로 지정해서 사용하고 있거나, 다른 스크립트에서 정의한 맵핑이 있더라도 키시퀀스에 대한 교란이 발생하지 않고 안전하게 작동하는 것을 보장할 수 있다.

이렇게 스크립트 전용 맵핑을 만드는 다른 방법으로는 <Plug> 맵을 사용하는 방법이 있다. <Plug>로 시작하는 맵핑은 vim 내부에서 사용자가 키보드로 입력할 수 없는 키시퀀스로 대체된다. 사용자가 입력할 수 없는 맵핑이 만들어지기 때문에 교란되지 않지만, <Plug>맵은 해당 스크립트 외부에 노출될 수 있어서 플러그인에서 사용하는 맵핑을 정의할 때 쓰인다. 다만 이는 실제로 발동하기 위해서는 사용자가 직접 해당 키맵을 호출하는 맵핑을 따로 만들어주어야 한다.

noremap <unique> <Plug>(remove_trailing_spaces)  :<c-u>call <SID>RemoveSpaces()<CR>
nmap ,s <Plug>(remove_trailing_spaces)

<unique> 키맵

이미 정의되어 있는 키맵에 대해서 같은 이름의 키맵을 다시 정의하려할 때, :map 명령은 :function이나 :command 명령과 달리 기존 키맵을 버리고 새 키맵을 덮어써버린다. 이를 방지하기 위해서 <unique> 옵션을 둘 수 있다. 유니크 키맵은 같은 이름의 키맵이 전역이든 버퍼-로컬이든 무관하게 존재한다면 맵핑 정의에 실패한다.


키맵 카운팅하기

키 맵의 우변이 될 수 있는 키 시퀀스에는 어떤 제약이 존재하지는 않는다. 따라서 숫자를 포함하는 키 시퀀스가 포함될 수 있다. 예를 들어 다음 키 맵을 보자.

:map , 3jdd

콤마를 누를 때 마다 3줄 아래를 지울 것이다. 만약 6줄 아래를 지우기 위해서 2,라고 입력하면 어떻게 동작할까? “,” 키가 “3j”로 대체되면서 “23j”를 입력한 효과가 발동되어 23개의 단어를 지나치게 될 것이다. 이처럼 반복횟수로 시작하는 키 시퀀스를 의도한대로 동작하게 하려면 키맵의 카운트를 유지할 수 있는 방법이 필요하다. 표현식 레지스터를 써서 이 문제를 해결할 수 있다. @='반복단위'<CR>을 주어 해당 동작을 반복할 수 있다.

:map , @='3j'<CR>dd

위와 같이 설정하면 “2,”라고 눌렀을 때 6dd 가 되어 여섯줄 아래를 삭제할 것이다.


이렇게 vim에서 키 맵을 정의할 때 사용되는 map 명령의 몇 가지 옵션과 그 작동 방식에 대해서 살펴보았다. 키맵을 작성할 때에는 1) 전역적으로 동작할 것인지, 버퍼에 한정하여 동작할 것인지와 2) 어떤 모드에서 동작할 것인지를 고려해야 하고, 3) 재맵핑을 허용할 것인지도 생각해야 한다. 재맵핑은 <Plug>/<SID> 맵 이름이 우변에 들어가는 경우를 제외하고는 절대 쓰지 않는다고 생각하고 거의 모든 경우에 :noremap을 쓴다고 생각하면 되겠다. 여기서 소개한 키 맵 옵션 외에 연산자와 결합하는 맵핑을 만드는 방법이 있는데, 이 부분은 다음 기회에 소개하도록 하겠다.