본문으로 건너뛰기

Neovim 키맵 인벤토리와 트러블슈팅

이 문서는 2023년에 작성한 개인 메모, 즉 "까먹지 않으려고 적어둔 단축키 표"를 확장한 것이다. 원본 메모는 다섯 개의 매핑과 L: Leader Key라는 한 줄짜리 범례가 전부였고, 각 매핑이 Neovim 기본 기능인지 플러그인이 등록한 것인지 구분이 없었다. 이 문서는 그 메모의 의도를 보존하되, 각 매핑의 소유자를 명확히 표시하고, 현재 세션에 실제로 적용된 매핑을 스스로 검증하는 절차와, 매핑이 동작하지 않을 때의 진단 순서를 함께 정리하는 것을 목표로 한다.

범위와 전제

  • 아래 단축키 표는 과거 로컬 설정의 스냅샷이다. Neovim 배포판(LazyVim, NvChad, AstroNvim 등)이나 개인 init.lua 구성에 따라 같은 키가 전혀 다른 동작에 연결되어 있을 수 있으므로, 이 표를 그대로 자기 환경에 적용되는 사실로 간주하면 안 된다.
  • Neovim은 버전이 올라가며 기본 매핑이 추가된다. 예를 들어 현재 stable은 <C-W>d로 커서 위치의 진단(diagnostic)을 floating window에 표시하는 기본 매핑을 제공한다. 표의 동작 일부는 최신 버전에서는 로컬 매핑 없이 내장 기능으로 사용할 수 있다는 뜻이다.
  • 검증 명령은 Lua API가 안정화된 Neovim 0.8 이상을 기준으로 한다. 출력이 이 문서와 다르다면 문서가 아니라 :verbose 계열 출력을 기준으로 삼는다.

키 표기법

원본 메모는 리더 키를 L로 표기했는데 두 가지 문제가 있었다. 첫째, L은 Neovim 내장 Normal 모드 명령(커서를 화면 마지막 줄로 이동, :h L)과 충돌한다. 둘째, <Leader>의 실제 키는 기본값이 백슬래시(\)이고 g:mapleader로 변경할 수 있으므로, 문자 L이 실제 입력 키인지 리더인지 구분할 수 없었다. 이 문서는 Vim/Neovim 표준 표기를 사용한다.

  • <Leader> — 리더 키 (:h mapleader, 기본값 \)
  • <C-x> — Ctrl 조합, <S-x> — Shift 조합, <C-Space> — Ctrl+Space
  • <CR> — Enter, <Cmd> — 명령행을 열지 않고 Ex 명령을 실행하는 표기

로컬 설정 스냅샷

아래 표는 2023년 메모의 매핑을 표준 표기로 옮긴 것이다. 전부 로컬 설정 스냅샷이며, 어느 설정 파일이나 플러그인이 등록했는지는 반드시 :verbose로 확인해야 한다. Neovim 내장 기능으로 단정하지 않는다.

Mode의도한 동작추정 소유자분류
Normal<Leader>f커서 위치 diagnostic을 floating window로 표시로컬 LSP 설정에서 vim.diagnostic.open_float()를 감싼 매핑로컬 스냅샷 — 소유자 확인 필요
Normal<Leader><Leader>t파일 트리 토글NvimTree/neo-tree 계열 플러그인로컬 스냅샷 — 소유자 확인 필요
Normal<Leader><Leader>sLeap 정방향 탐색 (글자 단위 점프)leap.nvim로컬 스냅샷 — 소유자 확인 필요
Normal<Leader>SLeap 역방향 탐색leap.nvim로컬 스냅샷 — 소유자 확인 필요
Insert<C-Space>자동완성 팝업 수동 호출nvim-cmp의 complete() 바인딩로컬 스냅샷 — 소유자 확인 필요

원본 메모의 역방향 Leap 표기는 L S로, <Leader>S인지 <Leader><Leader>S인지 모호했다. 위 표는 <Leader>S로 해석했으나 실제 설정은 :verbose nmap <Leader>S로 확인한다.

내장 대안을 알아두면 로컬 매핑이 사라진 환경에서도 작업 흐름이 끊기지 않는다.

  • 진단 float — 현재 stable의 기본 매핑 <C-W>d (:h CTRL-W_d-default)
  • 파일 탐색 — 내장 Netrw: :Explore, :Lexplore
  • 자동완성 — 내장 insert completion: <C-N>/<C-P> (키워드), <C-X><C-O> (omni)
  • s/S — 내장으로는 substitute 계열(scl, Scc)이다. leap.nvim은 이 키를 덮어쓰는 방식으로 동작하므로, leap이 로드되지 않은 환경에서는 원래 동작으로 돌아간다.

실제 매핑을 확인하는 공식 절차

"설정 파일에 적어둔 매핑"과 "현재 세션에 적용된 매핑"은 다를 수 있다. 플러그인이 나중에 같은 키를 덮어쓰거나, 버퍼 로컬 매핑이 글로벌 매핑을 가릴 수 있기 때문이다. 다음 명령들이 현재 세션의 상태를 보여주는 1차 소스다.

명령출력
:nmap현재 버퍼 기준 Normal 모드 매핑 전체 (글로벌 + 버퍼 로컬)
:imapInsert 모드 매핑 전체
:verbose nmap <lhs>해당 lhs의 매핑과 Last set from 파일:라인 — 소유자 특정
:verbose imap <lhs>Insert 모드에 대한 동일한 검사
:nmap <buffer>버퍼 로컬 Normal 매핑만 필터
:lua =vim.api.nvim_get_keymap('n')글로벌 Normal 매핑을 Lua 테이블로 반환 (덤프/비교용)
:lua =vim.api.nvim_buf_get_keymap(0, 'n')현재 버퍼(0)의 버퍼 로컬 매핑을 Lua 테이블로 반환

소유자를 특정하는 표준 순서는 이렇다. 문제가 재현되는 버퍼에서 :verbose nmap <lhs>를 실행하면, 출력의 Last set from이 소유 파일과 라인을 가리킨다. 출력이 비어 있으면 해당 모드에 매핑이 없다는 뜻이므로, 그 동작은 매핑이 아니라 내장 명령이거나 다른 모드의 매핑이다. nvim_get_keymap()은 글로벌만, nvim_buf_get_keymap()은 버퍼 로컬만 반환하므로 둘을 비교하면 버퍼 로컬 오버라이드 여부를 스크립트로 확인할 수 있다.

매핑 충돌(섀도잉) 진단

같은 lhs를 두 번 매핑하면 나중에 실행된 쪽이 이긴다. 플러그인 매니저의 지연 로딩(lazy loading) 때문에 "내 설정이 분명히 먼저 로드되는데 동작이 다르다"는 상황이 흔하다. 내 매핑이 초기화 단계에서 등록된 뒤, 어떤 플러그인이 로드되면서 같은 lhs를 다시 등록하면 플러그인이 최종 소유자가 된다. 버퍼 로컬 매핑은 해당 버퍼 안에서 글로벌 매핑을 가린다. LSP가 attach될 때 on_attach 콜백이 버퍼 로컬 매핑을 심는 구성이라면, LSP가 붙은 버퍼에서만 동작이 달라지는 식이다.

진단 순서:

  1. 문제 버퍼에서 :verbose nmap <lhs> — 현재 소유자와 Last set from 확인
  2. :nmap <buffer> — 버퍼 로컬 매핑 존재 여부 확인
  3. Last set from이 내 설정이면 로드 순서 문제이고, 플러그인 디렉터리면 해당 플러그인의 기본 매핑 옵션을 끄거나 내 매핑을 플러그인 로드 이후로 미룬다
  4. vim.keymap.setunique = true등록 시점에 이미 존재하는 lhs와의 충돌을 에러로 만든다. 이후 lazy-loaded plugin이 덮어쓰는 경우까지 막지는 않으므로, 그 충돌은 플러그인의 기본 매핑을 끄거나 load hook 이후에 소유자를 다시 확인해 관리한다

터미널에서 <C-Space>가 동작하지 않을 때

<C-Space>는 전통적인 터미널에서 NUL(0x00) 바이트로 전송되며 <C-@>와 구분되지 않는 경우가 많다. 터미널 에뮬레이터가 이 바이트를 아예 보내지 않거나, 운영체제가 먼저 가로채는 경우도 있다. 대표적으로 macOS는 "이전 입력 소스 선택" 단축키가 기본으로 Ctrl+Space에 할당되어 있어, 시스템 설정의 키보드 단축키에서 이를 해제하지 않으면 Neovim까지 키가 도달하지 않는다. tmux 안에서 사용하는 경우에도 extended keys 설정이 없으면 수정자 조합이 손실될 수 있다.

전송 경로를 확인하는 가장 빠른 방법은 Neovim 밖에서 바이트를 직접 보는 것이다.

  1. 셸에서 cat -v를 실행하고 Ctrl+Space를 누른다. ^@가 보이면 NUL이 전송되는 것이고, 아무것도 보이지 않으면 터미널이나 OS가 삼키는 것이다.
  2. Neovim 안에서는 Insert 모드에서 <C-V>를 누른 뒤 <C-Space>를 눌러 실제로 삽입되는 문자를 확인한다.
  3. tmux 사용자는 tmux 바깥에서 같은 테스트를 반복해 tmux가 경로상의 변수인지 분리한다.

전송이 불가능한 환경이라면 그 lhs를 고집할 이유가 없다. 내장 <C-N>/<C-P><C-X><C-O>로 자동완성을 대체하거나, 전송이 보장되는 다른 lhs를 선택하는 편이 낫다. 터미널 키 입력 전반의 동작은 :h tui를 참조한다.

Lua로 매핑 등록하기

새 매핑은 Ex 명령 :map 계열보다 vim.keymap.set으로 등록하는 것이 권장된다. :map과 달리 noremap = true가 기본값이라 재귀 매핑 사고를 피할 수 있고, desc로 문서화 문자열을 함께 저장할 수 있기 때문이다. desc:map 출력과 which-key 같은 탐색 UI에 그대로 표시된다.

-- 예시: 로컬 스냅샷 표의 의도를 재현하는 등록 코드
local map = vim.keymap.set

-- Normal: 커서 아래 diagnostic을 floating window로
map('n', '<Leader>f', function()
vim.diagnostic.open_float({ border = 'rounded' })
end, { desc = 'diagnostic float 열기', silent = true })

-- Insert: 자동완성 팝업 수동 호출 (nvim-cmp 설치 전제)
map('i', '<C-Space>', function()
require('cmp').complete()
end, { desc = '자동완성 팝업 열기' })

-- Normal: 파일 트리 토글 — unique로 충돌을 설정 시점에 감지
map('n', '<Leader><Leader>t', '<Cmd>NvimTreeToggle<CR>', {
desc = '파일 트리 토글',
unique = true,
})

스냅샷 표의 "의도한 동작" 열을 실제 desc와 일치시켜 두면, 문서와 설정이 어긋났는지를 :map 출력만으로 바로 알 수 있다. 위 nvim-cmp 예시는 해당 플러그인이 설치된 환경에서만 유효하며, 내장 completion만 쓰는 환경이라면 <C-Space> 매핑 자체가 불필요하다.

유지보수 체크리스트

  • 새 매핑을 추가할 때 desc를 반드시 달고, 추가 전 :verbose nmap <lhs>로 충돌 여부 확인
  • 가능하면 unique = true로 등록 시점의 중복을 에러로 승격하고, lazy load 뒤에도 소유자 재확인
  • Neovim 본체나 플러그인 업그레이드 후 스냅샷 표의 각 lhs를 :verbose로 재검증 (새 기본 매핑이 생기거나 플러그인 기본값이 바뀔 수 있음)
  • 설정에서 제거한 매핑은 이 문서의 표에서도 같은 시점에 제거
  • 터미널 에뮬레이터를 바꾸면 <C-Space> 등 제어키 전송을 cat -v로 재확인
  • 내장 기본 매핑으로 대체 가능한 로컬 매핑은 정리 (예: 진단 float은 <C-W>d)

관련 문서

공식 레퍼런스