2026년 Neovim 최소 셋업의 핵심은 딱 네 개다 — 플러그인 관리(lazy.nvim), LSP(nvim-lspconfig + Mason), 문법 파싱(nvim-treesitter), 파일·문자열 탐색(telescope). 이 넷이면 VS Code에서 하던 일의 90%는 커버된다.
문제는 이걸 어떻게 조립하느냐다. 대부분의 사람이 처음부터 500줄짜리 config를 복붙했다가 뭐가 뭔지 몰라서 포기한다. 나도 그랬다. 이 글은 각 조각이 왜 필요한지, 최소한의 lua로 어떻게 붙이는지, 그리고 러닝커브가 실제로 얼마나 아픈지를 솔직하게 다룬다.
먼저 전제 하나. 이건 "kickstart.nvim 클론" 글이 아니다. 남이 만든 배포판(LazyVim, NvChad, AstroNvim)을 쓰면 5분 만에 예쁜 화면이 뜨지만, 뭔가 고장 나면 그 500줄이 전부 남의 코드라 손을 못 댄다. 실무에서 이걸로 데인 적이 있다. LazyVim 업데이트 한 번에 키맵이 통째로 바뀌어서 반나절을 날렸다. 그래서 개인적으로는 최소 구성을 직접 짜서 무슨 일이 일어나는지 아는 쪽을 선호한다.
왜 IDE 대신 Neovim인가?
솔직히 말하면 대부분의 사람에게는 VS Code나 JetBrains가 정답이다. Neovim을 추천하는 경우는 명확하다 — SSH로 원격 서버를 자주 만지거나, 손을 홈 로우에서 안 떼고 싶거나, 에디터가 4GB씩 먹는 게 싫거나, 설정을 코드로 버전 관리하고 싶은 사람.
구체적인 차이를 보자. 아래는 일반적으로 관찰되는 특성이지 정밀 벤치마크는 아니다. 다만 방향성은 확실하다.
| 항목 | Neovim(최소셋업) | VS Code |
|---|---|---|
| 콜드 스타트 | 보통 50~150ms | 보통 1.5~4초 |
| 유휴 메모리 | 수십 MB대 | 수백 MB~1GB대 |
| 원격(SSH) 편집 | 그냥 됨(터미널) | Remote-SSH 확장 필요 |
| 설정 형식 | lua(코드) | JSON + GUI |
| 러닝커브 | 가파름 | 완만함 |
메모리와 시작 속도는 체감이 크다. 특히 하루에 에디터를 수십 번 열고 닫는 워크플로우(git 커밋 메시지, 설정 파일 잠깐 수정)라면 50ms와 3초의 차이는 누적되면 무시 못 한다. 반대로 디버거 UI, 확장 마켓의 완성도, 팀 온보딩 난이도는 VS Code가 압도적이다. 트레이드오프를 알고 고르면 된다.
먼저 알아야 할 것: init.lua 구조
Neovim 설정의 진입점은 ~/.config/nvim/init.lua 하나다. 여기서 lua로 옵션을 켜고, 플러그인 매니저를 부트스트랩하고, 나머지는 lua/ 밑으로 쪼갠다. 2026년 기준 vimscript(.vim)를 새로 배울 이유는 거의 없다. 전부 lua로 간다.
디렉터리 구조는 이 정도면 충분하다.
~/.config/nvim/
├── init.lua -- 진입점: 옵션 + lazy 부트스트랩
└── lua/
├── config/
│ └── options.lua -- 편집기 기본 옵션
└── plugins/
├── lsp.lua -- LSP + Mason
├── treesitter.lua
└── telescope.lua
처음엔 파일 하나에 다 몰아넣어도 된다. 나도 6개월은 init.lua 한 파일로 버텼다. 200줄 넘어가면서 스크롤이 지겨워질 때 쪼개면 그만이다. 미리 과하게 나누지 마라 — 폴더만 예쁘고 정작 뭐가 어디 있는지 못 찾는 게 더 나쁘다.
1단계: 기본 옵션 (플러그인 0개)
플러그인을 깔기 전에 순정 옵션부터 잡는 게 순서다. 이것만으로도 vim이 꽤 쓸 만해진다. vim.opt으로 설정한다.
-- lua/config/options.lua
local opt = vim.opt
opt.number = true -- 절대 줄번호
opt.relativenumber = true -- 상대 줄번호(j/k 이동에 유용)
opt.expandtab = true -- 탭을 스페이스로
opt.shiftwidth = 2
opt.tabstop = 2
opt.smartindent = true
opt.wrap = false
opt.ignorecase = true -- 검색 대소문자 무시
opt.smartcase = true -- 단, 대문자 넣으면 구분
opt.termguicolors = true -- 24bit 트루컬러(테마 정상 표시에 필수)
opt.signcolumn = "yes" -- LSP 아이콘 자리 항상 확보(화면 덜 튐)
opt.updatetime = 250 -- 진단 표시 반응 속도
opt.scrolloff = 8 -- 커서 위아래 여백 8줄 유지
opt.clipboard = "unnamedplus" -- 시스템 클립보드 연동
여기서 문서에 잘 안 나오는 팁 하나. signcolumn = "yes"를 안 켜면 LSP 에러 아이콘이 뜰 때마다 화면 왼쪽이 한 칸씩 밀려서 눈이 피곤하다. 기본값(auto)이 함정인 셈. 그리고 clipboard = "unnamedplus"는 리눅스에서 xclip이나 wl-clipboard가 깔려 있어야 동작한다. 이게 없으면 시스템 복붙이 조용히 안 돼서 한참 헤맨다.
리더 키는 플러그인 로드 전에 잡아야 키맵이 꼬이지 않는다. init.lua 맨 위에 둔다.
-- init.lua (맨 위)
vim.g.mapleader = " " -- 리더를 스페이스로
vim.g.maplocalleader = " "
require("config.options")
2단계: lazy.nvim — 플러그인 매니저
lazy.nvim은 2026년 현재 사실상 표준 플러그인 매니저다. 예전 packer.nvim은 유지보수가 사실상 멈췄으니 새로 시작한다면 고민할 것 없이 lazy다. 이름값대로 "게으른 로딩(lazy loading)"이 핵심 — 플러그인을 실제로 필요할 때까지 안 불러와서 시작이 빠르다.
부트스트랩 코드는 공식 문서 그대로 쓰면 된다. git이 설치돼 있으면 알아서 클론한다.
-- init.lua (options require 다음)
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
vim.fn.system({
"git", "clone", "--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable", lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
-- lua/plugins/ 폴더 안의 파일을 전부 스펙으로 읽는다
require("lazy").setup("plugins")
require("lazy").setup("plugins") 한 줄이 lua/plugins/ 밑의 모든 파일을 자동으로 긁어온다. 각 파일은 lua 테이블(스펙)을 return하면 된다. 이 구조가 lazy.nvim의 진짜 장점이다 — 플러그인 하나당 파일 하나, 관련 설정을 한곳에 모을 수 있다.
:Lazy로 대시보드를 열어 상태를 확인하고, :Lazy sync로 업데이트한다. 뭔가 이상하면 여기부터 보면 된다.3단계: Treesitter — 진짜 문법 하이라이트
nvim-treesitter는 정규식 기반의 낡은 하이라이트를 실제 구문 트리 기반으로 바꿔준다. 색만 예뻐지는 게 아니라, 함수/블록 단위 선택, 정확한 들여쓰기, 코드 접기의 기반이 된다. LSP보다 먼저 깔길 권한다 — 설정이 단순하고 효과가 즉각적이라 "오 뭔가 되네" 하는 첫 성취감을 준다.
-- lua/plugins/treesitter.lua
return {
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate", -- 파서 컴파일(C 컴파일러 필요)
config = function()
require("nvim-treesitter.configs").setup({
-- 자주 쓰는 언어만 파서 설치
ensure_installed = {
"lua", "vim", "vimdoc",
"javascript", "typescript", "tsx",
"python", "rust", "go",
"json", "yaml", "html", "css", "markdown",
},
auto_install = true, -- 새 파일타입 열면 파서 자동 설치
highlight = { enable = true },
indent = { enable = true },
})
end,
}
여기서 조심할 점. build = ":TSUpdate"는 파서를 C로 컴파일하기 때문에 시스템에 gcc나 clang이 있어야 한다. 없으면 Error: no C compiler found류의 메시지가 뜬다. 나는 새 서버에 셋업하다가 이걸로 막힌 적이 몇 번 있다. build-essential(데비안 계열)이나 Xcode CLT(맥)를 먼저 깔아두자.
4단계: LSP — 자동완성·정의 이동·진단의 핵심
LSP(Language Server Protocol)가 Neovim을 IDE급으로 끌어올리는 진짜 엔진이다. 자동완성, 정의로 점프, 참조 찾기, 실시간 에러 표시, 리네임 — 이 전부가 LSP에서 나온다. 문제는 조립 부품이 세 개라 초보자가 가장 많이 헷갈리는 지점이라는 것.
세 부품의 역할을 명확히 구분하자. 이거 모르고 config를 복붙하면 영원히 헤맨다.
| 부품 | 역할 |
|---|---|
mason.nvim | 언어 서버 바이너리를 설치·관리(패키지 매니저) |
mason-lspconfig | mason와 lspconfig를 연결하는 다리 |
nvim-lspconfig | 설치된 서버를 Neovim에 실제로 연결·설정 |
쉽게 말해 Mason이 서버를 "깔고", lspconfig가 그걸 "켠다". 아래는 세 개를 한 파일로 묶은 최소 설정이다.
-- lua/plugins/lsp.lua
return {
"neovim/nvim-lspconfig",
dependencies = {
{ "williamboman/mason.nvim", config = true },
"williamboman/mason-lspconfig.nvim",
},
config = function()
require("mason").setup()
require("mason-lspconfig").setup({
-- 이 서버들을 자동 설치
ensure_installed = { "lua_ls", "pyright", "ts_ls", "gopls" },
})
local lspconfig = require("lspconfig")
-- 각 서버를 기본 설정으로 켠다
lspconfig.lua_ls.setup({})
lspconfig.pyright.setup({})
lspconfig.ts_ls.setup({})
lspconfig.gopls.setup({})
-- LSP가 버퍼에 붙을 때 키맵 등록
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(ev)
local map = function(keys, fn)
vim.keymap.set("n", keys, fn, { buffer = ev.buf })
end
map("gd", vim.lsp.buf.definition) -- 정의로 이동
map("gr", vim.lsp.buf.references) -- 참조 찾기
map("K", vim.lsp.buf.hover) -- 문서 팝업
map("<leader>rn", vim.lsp.buf.rename) -- 리네임
map("<leader>ca", vim.lsp.buf.code_action)
end,
})
end,
}
tsserver는 이제 ts_ls다. 옛날 글 복붙하면 server not found 에러가 난다. 이런 이름 변경이 Neovim 생태계에서 가장 짜증나는 부분이다 — 6개월 전 블로그 글이 이미 낡아 있다.위 설정에는 자동완성 UI(nvim-cmp 또는 blink.cmp)가 빠져 있다. LSP 자체는 완성 후보를 제공하지만, 예쁜 팝업 메뉴로 띄우려면 별도 플러그인이 필요하다. 최소 셋업의 범위를 넘어서니 여기선 생략하지만, 다음 단계로 hrsh7th/nvim-cmp 또는 더 가벼운 saghen/blink.cmp를 붙이면 된다. 개인적으로 요즘은 blink.cmp가 설정이 간결해서 더 손이 간다.
5단계: Telescope — 파일·문자열 퍼지 검색
Telescope는 파일 찾기, 프로젝트 전체 문자열 검색(grep), 버퍼 전환을 퍼지(fuzzy)로 해주는 플러그인이다. VS Code의 Cmd+P와 전역 검색을 합친 것에 가깝다. 일단 손에 익으면 이거 없이는 못 산다. 체감상 가장 자주 쓰는 플러그인이다.
-- lua/plugins/telescope.lua
return {
"nvim-telescope/telescope.nvim",
branch = "0.1.x",
dependencies = { "nvim-lua/plenary.nvim" },
config = function()
local builtin = require("telescope.builtin")
vim.keymap.set("n", "<leader>ff", builtin.find_files) -- 파일 찾기
vim.keymap.set("n", "<leader>fg", builtin.live_grep) -- 전체 grep
vim.keymap.set("n", "<leader>fb", builtin.buffers) -- 열린 버퍼
vim.keymap.set("n", "<leader>fh", builtin.help_tags) -- 도움말 검색
end,
}
이제 노멀 모드에서 스페이스 + f + f를 누르면 파일 검색 창이 뜬다. live_grep은 프로젝트 안 모든 파일에서 실시간으로 문자열을 찾아주는데, 이게 진짜 물건이다. 단, 성능을 위해 ripgrep(rg 명령)이 시스템에 깔려 있어야 한다. 없으면 live_grep이 작동 안 하거나 느리다. find_files도 fd가 있으면 훨씬 빠르다.
# 외부 의존성 설치 (데비안/우분투 예시)
sudo apt install ripgrep fd-find build-essential
# 맥(homebrew)
brew install ripgrep fd
전체 조각을 붙이면
지금까지의 파일을 다 만들었다면 최종 init.lua는 이 정도로 짧다. 로직의 대부분은 lua/plugins/ 밑에 흩어져 있고, 진입점은 얇게 유지하는 게 핵심이다.
-- init.lua (전체)
vim.g.mapleader = " "
vim.g.maplocalleader = " "
require("config.options")
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
vim.fn.system({
"git", "clone", "--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable", lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
require("lazy").setup("plugins")
전부 합쳐 200줄이 안 된다. 배포판이 깔아주는 수천 줄과 비교하면 초라해 보이지만, 이 200줄은 전부 내가 이해하는 코드다. 뭔가 고장 나면 어디를 봐야 할지 안다. 그게 직접 짜는 이유의 전부다.
러닝커브의 현실 — 솔직하게
여기서 미화하지 않겠다. Neovim은 첫 2주가 진짜 고통스럽다. 모달 편집(노멀/인서트 모드 전환) 자체가 근육 기억을 새로 만드는 일이라, 처음엔 VS Code 쓸 때보다 생산성이 반토막 난다. 이건 각오해야 한다.
대략적인 적응 곡선은 이렇다. 개인차는 크지만 방향은 비슷하다.
| 시기 | 체감 |
|---|---|
| 1~3일 | 저장/종료도 버벅임, 후회의 시기 |
| 1~2주 | 기본 이동은 되지만 여전히 느림 |
| 1개월 | 예전 속도 회복, 일부 작업은 더 빠름 |
| 3개월+ | 모달 편집이 편해짐, 마우스가 답답해짐 |
버티는 요령 몇 가지. 첫째, :Tutor 명령을 꼭 한 번 완주해라(30분). 둘째, 처음부터 화살표 키를 쓰지 말고 hjkl로만 이동하는 걸 강제해라. 셋째, 마이그레이션은 한 번에 하지 말고 사이드 프로젝트나 설정 파일 편집처럼 부담 없는 작업부터 시작해라. 메인 업무를 첫날부터 Neovim으로 하면 십중팔구 3일 만에 VS Code로 돌아간다.
그리고 이건 개인 의견인데 — 모든 사람이 Neovim을 써야 하는 건 아니다. 편집 속도가 당신의 병목이 아니라면(대부분은 아니다) 얻는 게 크지 않을 수 있다. 재미와 커스터마이즈의 즐거움을 위해 쓰는 거라면 그건 그것대로 충분히 좋은 이유다. 나는 그쪽에 가깝다.
다음에 붙이면 좋은 것들
최소 셋업이 손에 익으면 이 순서로 확장하길 권한다. 한 번에 다 깔지 마라 — 하나 붙이고 며칠 써보고 다음 걸 붙이는 게 정신 건강에 좋다.
- 자동완성: blink.cmp 또는 nvim-cmp — LSP 후보를 팝업으로
- Git: gitsigns.nvim(변경 표시) + lazygit 연동
- 파일 트리: oil.nvim(개인적으로 nvim-tree보다 선호) 또는 neo-tree
- 상태줄: lualine.nvim
- 포매터/린터: conform.nvim + nvim-lint
- 키맵 힌트: which-key.nvim — 리더 키 누르면 가능한 동작 표시
여기까지 붙이면 사실상 IDE다. 하지만 시작은 이 글의 네 개면 충분하다. 욕심내서 20개 플러그인을 한꺼번에 깔면 뭐 하나 고장 났을 때 원인을 못 찾는다. 적게 시작해서 필요할 때 늘려라.
자주 묻는 질문
Neovim과 Vim은 뭐가 다른가요?
Neovim은 Vim의 포크로, lua 스크립팅과 내장 LSP를 기본 지원한다. 순정 Vim도 vim9script와 플러그인으로 비슷하게 만들 수 있지만, 2026년 현재 플러그인 생태계(lazy.nvim, Treesitter, LSP 등)는 사실상 Neovim 중심으로 돌아간다. 새로 시작한다면 Neovim이 정답이다.
배포판(LazyVim, NvChad)을 쓰면 안 되나요?
써도 된다. 빠르게 예쁜 환경을 원하면 오히려 추천한다. 다만 뭔가 고장 났을 때 내부를 이해 못 하면 손을 못 댄다는 단점이 있다. 학습 목적이라면 최소 구성을 직접 짜본 뒤 배포판을 쓰는 걸 권한다. 그러면 배포판 코드도 읽힌다.
Mason, lspconfig, mason-lspconfig가 왜 다 필요한가요?
역할이 다르다. Mason은 언어 서버 바이너리를 설치하는 패키지 매니저, lspconfig는 그 서버를 Neovim에 연결하는 설정 모음, mason-lspconfig는 둘을 자동으로 이어주는 다리다. 셋을 함께 쓰면 서버 설치부터 연결까지 자동화된다.
tsserver가 안 됩니다. 왜죠?
TypeScript 서버의 lspconfig 이름이 tsserver에서 ts_ls로 바뀌었다. 오래된 튜토리얼을 따라 했다면 이 부분에서 막힌다. ts_ls로 바꾸면 된다. 이처럼 이름 변경이 잦으니 에러가 나면 :checkhealth lsp와 :LspInfo로 상태를 먼저 확인하는 습관을 들이자.
Telescope의 live_grep이 작동하지 않아요.
거의 항상 ripgrep(rg)이 시스템에 설치되지 않은 게 원인이다. live_grep은 내부적으로 rg를 호출하기 때문에 이게 없으면 결과가 안 나온다. sudo apt install ripgrep이나 brew install ripgrep으로 설치하고 nvim을 다시 열어보라.
Neovim 셋업에 얼마나 걸리나요?
이 글의 최소 구성 기준 실제 config 작성은 30분~1시간이면 된다. 하지만 진짜 시간이 드는 건 모달 편집에 적응하는 과정으로, 예전 생산성을 회복하는 데 보통 2주에서 한 달이 걸린다. 설정보다 손에 익히는 게 훨씬 오래 걸린다는 걸 기억하자.
초보자도 IDE 대신 바로 Neovim으로 시작해도 되나요?
추천하지 않는다. 프로그래밍 자체가 처음이라면 언어와 에디터를 동시에 배우는 부담이 너무 크다. VS Code로 개발에 익숙해진 뒤, 편집 효율이나 원격 작업 같은 명확한 동기가 생겼을 때 넘어오는 편이 좋다. 도구는 목적이 아니라 수단이다.

댓글 0