Vim이 안 꺼질 때부터 시작하는 Vi/Vim: 종료, 모드, 이동, 편집, 검색·치환
이 글의 핵심
Vim을 안전하게 종료하는 방법부터 노멀·입력·비주얼 모드, 자주 쓰는 이동과 편집 명령, 검색과 치환, 저장과 종료까지 서버에서 바로 쓰는 명령어를 정리합니다.
들어가며: “Vim을 켰는데 종료가 안 돼요!”
SSH로 서버에 접속하면 GUI 에디터가 없습니다. 이때 Vi/Vim은 거의 모든 리눅스/유닉스 시스템에 기본 설치되어 있는 필수 텍스트 에디터입니다.
하지만 처음 Vim을 열면:
- 텍스트를 입력할 수 없음
- 종료 방법을 모름
- 마우스가 작동하지 않음
- 명령어가 독특함
이 글에서 다루는 것:
- 종료하는 방법 (가장 많이 검색되는 질문)
- 모드 개념 (Normal, Insert, Visual, Command)
- 실전 명령어 (이동, 편집, 검색, 치환)
- 필수 설정 (.vimrc)
- 단축키 치트시트
긴급 탈출: Vim 종료하기
가장 많이 검색되는 질문
# Vim을 열었는데 종료가 안 될 때
# 1단계: ESC 키를 여러 번 누르기
ESC ESC ESC
# 2단계: 다음 중 하나 입력
:q # 저장하지 않고 종료 (변경사항 없을 때)
:q! # 강제 종료 (변경사항 버림)
:wq # 저장 후 종료
:x # 저장 후 종료 (변경사항 있을 때만 저장)
ZZ # :x와 동일 (더 빠름)
상황별 종료 방법
| 상황 | 명령어 | 설명 |
|---|---|---|
| 아무것도 안 바꿨음 | :q | 그냥 종료 |
| 수정했는데 저장하고 싶음 | :wq 또는 ZZ | 저장 후 종료 |
| 수정했는데 버리고 싶음 | :q! | 강제 종료 |
| 파일명 지정 저장 | :w filename | 다른 이름으로 저장 |
| 읽기 전용 파일 강제 저장 | :w! | 권한 있을 때만 가능 |
:q가 E37: No write since last change (add ! to override)로 거부되면 “수정한 내용이 있다”는 뜻이므로, 저장할지(:wq) 버릴지(:q!)를 정하면 됩니다. 창을 여러 개 열었거나 vim a.txt b.txt처럼 파일을 여러 개 열었다면 :q가 현재 창만 닫으므로, 한 번에 모두 끝내려면 :qa(모두 종료), :wqa(모두 저장 후 종료), :qa!(모두 버리고 종료)를 씁니다.
종료가 안 되는 원인이 Vim이 아닌 경우도 있습니다. 습관처럼 Ctrl+S를 누르면 터미널이 출력 정지(XOFF) 상태가 되어 어떤 키를 눌러도 화면이 멈춘 것처럼 보이는데, Vim이 멈춘 게 아니라 터미널이 출력을 막고 있는 것이므로 Ctrl+Q를 누르면 풀립니다. 또 실수로 Ctrl+Z를 누르면 Vim이 백그라운드로 정지되어 셸 프롬프트로 돌아오는데, 이때 파일은 아직 열린 상태이므로 fg로 다시 불러와 정상 종료해야 합니다. 그냥 새로 vim을 열면 다음 절의 스왑 파일 경고를 보게 됩니다.
”E325: ATTENTION” 스왑 파일 경고
Vim은 편집 중인 내용을 .파일명.swp 스왑 파일에 계속 기록해, SSH 연결이 끊기거나 Vim이 비정상 종료되어도 복구할 수 있게 합니다. 그래서 이전 세션이 제대로 끝나지 않았거나 다른 사람(또는 다른 터미널의 나)이 같은 파일을 열고 있으면 파일을 열 때 E325: ATTENTION 경고와 함께 [O]pen Read-Only, (E)dit anyway, (R)ecover, (D)elete it, (Q)uit, (A)bort 선택지가 나옵니다. 서버에서 설정 파일을 수정할 때 이 경고를 그냥 E로 넘기면 다른 쪽의 수정 내용을 덮어쓸 수 있으므로, 먼저 다른 세션에서 같은 파일을 열고 있지 않은지 확인하는 것이 안전합니다. 이전 세션이 끊긴 것이 확실하면 R로 복구한 뒤 내용을 확인하고 저장하고, 복구가 필요 없으면 D로 스왑 파일을 지우면 됩니다. 연결이 불안정한 서버에서 긴 편집을 할 때 이 기능 덕분에 작업을 날리지 않은 경험이 있어서, 저는 스왑 파일을 끄지 않고 대신 set directory=~/.vim/swap//로 모아 두는 쪽을 택합니다.
Vi vs Vim 차이
Vi (Visual Editor)
- 1976년 개발 (거의 50년 전!)
- 모든 유닉스/리눅스에 기본 설치
- 최소한의 기능
Vim (Vi IMproved)
- 1991년 개발 (Vi의 개선판)
- 추가 기능:
- 문법 강조 (Syntax Highlighting)
- 다중 취소/재실행
- 플러그인 시스템
- 분할 화면
- 자동완성
- 비주얼 모드
확인 방법
# Vi 버전 확인
vi --version
# Vim 설치 확인
vim --version
# 대부분의 시스템에서 vi는 vim의 심볼릭 링크
ls -l $(which vi)
# lrwxrwxrwx 1 root root 20 /usr/bin/vi -> /etc/alternatives/vi
모드 이해하기
Vim의 핵심: 모드
Vim은 모드 기반 에디터입니다. 같은 키가 모드에 따라 다르게 동작합니다.
graph LR
N[Normal 모드] -->|i, a, o| I[Insert 모드]
I -->|ESC| N
N -->|v, V, Ctrl+v| V[Visual 모드]
V -->|ESC| N
N -->|:| C[Command 모드]
C -->|Enter| N
Normal 모드 (기본)
- Vim을 열면 이 모드
- 명령어 실행 (이동, 삭제, 복사 등)
- 텍스트 입력 불가
- ESC로 언제든 돌아옴
# Normal 모드 명령어 예시
dd # 현재 줄 삭제
yy # 현재 줄 복사
p # 붙여넣기
u # 실행 취소
Insert 모드 (입력)
- 텍스트 입력 모드
- 일반 에디터처럼 타이핑 가능
- ESC로 Normal 모드로 복귀
# Insert 모드 진입 명령어
i # 커서 위치에서 입력
I # 줄 맨 앞에서 입력
a # 커서 다음 칸에서 입력
A # 줄 맨 뒤에서 입력
o # 아래 새 줄에서 입력
O # 위 새 줄에서 입력
Visual 모드 (선택)
- 텍스트 선택 모드
- 블록 지정 후 복사/삭제
v # 문자 단위 선택
V # 줄 단위 선택
Ctrl+v # 블록 단위 선택 (세로 선택)
Command 모드 (명령)
:로 시작- 저장, 종료, 검색, 치환 등
:w # 저장
:q # 종료
:wq # 저장 후 종료
:%s/old/new/g # 치환
기본 이동 명령어
방향키 대신 hjkl
# 기본 이동 (화살표 키도 작동하지만 hjkl이 더 빠름)
h # ← 왼쪽
j # ↓ 아래
k # ↑ 위
l # → 오른쪽
왜 hjkl?: Bill Joy가 Vi를 만들 때 쓰던 ADM-3A 터미널에는 별도의 화살표 키가 없었고, 대신 H·J·K·L 키 위에 화살표가 인쇄되어 있었습니다. 그 배치가 그대로 굳은 것입니다. 지금도 hjkl을 쓰는 이유는 손을 홈 row에서 떼지 않고 이동할 수 있고, 모든 이동 명령이 5j(5줄 아래), d3w(세 단어 삭제)처럼 횟수와 연산자와 조합되기 때문입니다. 화살표 키도 동작하지만 이 조합 문법에 익숙해지는 것이 Vim을 빠르게 쓰는 핵심입니다.
단어 단위 이동
w # 다음 단어 시작
W # 다음 단어 시작 (공백 기준)
e # 다음 단어 끝
E # 다음 단어 끝 (공백 기준)
b # 이전 단어 시작
B # 이전 단어 시작 (공백 기준)
줄 단위 이동
0 # 줄 맨 앞
^ # 줄 맨 앞 (공백 제외)
$ # 줄 맨 뒤
gg # 파일 맨 앞
G # 파일 맨 뒤
:n # n번째 줄로 이동 (예: :42)
42G # 42번째 줄로 이동
w와 W의 차이는 “단어”의 정의입니다. 소문자 w는 영숫자·밑줄 묶음과 구두점을 별개의 단어로 보므로 config.server.port에서 w를 누르면 .에서도 멈추고, 대문자 W는 공백으로만 나누므로 한 번에 다음 공백 뒤로 건너뜁니다. 경로나 URL처럼 기호가 많은 텍스트에서는 대문자 쪽이 훨씬 빠릅니다. 0과 ^의 차이도 들여쓰기된 코드에서 중요해서, 들여쓴 줄의 실제 코드 시작으로 가려면 ^를 씁니다.
화면 단위 이동
Ctrl+f # 한 화면 아래 (forward)
Ctrl+b # 한 화면 위 (backward)
Ctrl+d # 반 화면 아래 (down)
Ctrl+u # 반 화면 위 (up)
H # 화면 맨 위 (High)
M # 화면 중간 (Middle)
L # 화면 맨 아래 (Low)
텍스트 편집
삭제
x # 커서 위치 문자 삭제
X # 커서 앞 문자 삭제
dd # 현재 줄 삭제
D # 커서부터 줄 끝까지 삭제
dw # 단어 삭제
d$ # 커서부터 줄 끝까지 삭제
d0 # 커서부터 줄 앞까지 삭제
dG # 커서부터 파일 끝까지 삭제
dgg # 커서부터 파일 앞까지 삭제
변경
r # 커서 위치 문자 교체 (1글자)
R # 덮어쓰기 모드
cw # 단어 변경 (change word)
cc # 줄 변경
C # 커서부터 줄 끝까지 변경
s # 문자 삭제 후 입력 모드
S # 줄 삭제 후 입력 모드
Vim 편집 명령의 규칙은 연산자 + 이동(motion)입니다. d(삭제), c(변경), y(복사)는 연산자이고, 뒤에 붙는 w, $, G, gg 같은 이동 명령이 적용 범위를 정합니다. 그래서 dw, d$, dG를 따로 외울 필요 없이 이동 명령을 알면 조합이 생깁니다. 여기에 iw(단어 안쪽), i"(따옴표 안쪽), i((괄호 안쪽) 같은 텍스트 객체를 붙이면 ci"로 커서가 문자열 중간에 있어도 따옴표 안 내용 전체를 바꿀 수 있습니다. 설정 파일의 값을 바꿀 때 가장 자주 쓰는 조합입니다. 연산자를 두 번 쓰면(dd, cc, yy) 현재 줄 전체가 대상입니다.
취소/재실행
u # 실행 취소 (undo)
Ctrl+r # 재실행 (redo)
. # 마지막 명령 반복
검색과 치환
검색
/pattern # 앞으로 검색
?pattern # 뒤로 검색
n # 다음 검색 결과
N # 이전 검색 결과
* # 커서 위치 단어 검색
# # 커서 위치 단어 역방향 검색
예시:
/error # "error" 검색
n # 다음 error로 이동
N # 이전 error로 이동
치환
:s/old/new/ # 현재 줄에서 첫 번째 old를 new로
:s/old/new/g # 현재 줄에서 모든 old를 new로
:%s/old/new/g # 전체 파일에서 모든 old를 new로
:%s/old/new/gc # 전체 파일, 확인하며 치환
:5,10s/old/new/g # 5~10번째 줄에서 치환
실전 예시:
# Python에서 print 문을 logging으로 변경
:%s/print(/logging.info(/g
# 탭을 4개 공백으로 변경
:%s/\t/ /g
# 줄 끝 공백 제거
:%s/\s\+$//g
치환 패턴은 기본적으로 Vim 정규식이라 +, (, | 같은 기호가 이스케이프해야 특수 의미를 가집니다. 그래서 줄 끝 공백 제거에 \s\+처럼 백슬래시가 붙습니다. 패턴 앞에 \v(very magic)를 붙이면 :%s/\v\s+$//처럼 일반 정규식에 가깝게 쓸 수 있습니다. 경로처럼 /가 많은 문자열을 바꿀 때는 구분자를 바꿀 수 있어서 :%s#/usr/local#/opt#g처럼 쓰면 \/를 반복할 필요가 없습니다. 또 :%s/oldFunc/newFunc/g는 oldFuncHelper 같은 다른 이름의 일부도 바꿔 버리므로, 단어 전체만 바꾸려면 \<oldFunc\>처럼 단어 경계를 지정해야 합니다. 대소문자 구분은 .vimrc의 ignorecase/smartcase 설정을 따르며, 한 번만 바꾸려면 패턴 끝에 \C(구분)나 \c(무시)를 붙입니다.
복사, 붙여넣기, 삭제
복사 (Yank)
yy # 현재 줄 복사
Y # 현재 줄 복사 (yy와 동일)
yw # 단어 복사
y$ # 커서부터 줄 끝까지 복사
yG # 커서부터 파일 끝까지 복사
붙여넣기 (Paste)
p # 커서 뒤에 붙여넣기
P # 커서 앞에 붙여넣기
dd나 x로 지운 내용도 레지스터에 저장되므로, dd 후 p를 누르면 방금 지운 줄이 붙여넣어집니다. 이것이 Vim에서 “잘라내기”가 따로 없는 이유이자, yy로 복사해 둔 내용이 중간에 dd를 하는 순간 덮어써지는 흔한 당혹의 원인입니다. 복사한 내용은 "0 레지스터에도 따로 남아 있으므로 "0p로 붙여넣으면 이 문제를 피할 수 있습니다. 참고로 Neovim에서는 Y가 yy가 아니라 y$(커서부터 줄 끝까지)로 기본 매핑이 바뀌었습니다.
Visual 모드로 복사
# 1. v로 Visual 모드 진입
# 2. hjkl로 선택 범위 지정
# 3. y로 복사 (또는 d로 잘라내기)
# 4. p로 붙여넣기
예시:
# 5줄 복사하기
5yy # 현재 줄부터 5줄 복사
p # 붙여넣기
# 블록 복사
Ctrl+v # 블록 선택 모드
jjjj # 아래로 4줄 선택
lll # 오른쪽으로 3칸 선택
y # 복사
p # 붙여넣기
저장과 종료
저장
:w # 저장
:w filename # 다른 이름으로 저장
:w! # 강제 저장 (읽기 전용 파일)
:w >> file # 파일 끝에 추가
:5,10w file # 5~10번째 줄만 저장
종료
:q # 종료 (변경사항 없을 때)
:q! # 강제 종료 (변경사항 버림)
:wq # 저장 후 종료
:x # 저장 후 종료 (변경사항 있을 때만)
ZZ # :x와 동일 (더 빠름)
ZQ # :q!와 동일
분할 화면
수평/수직 분할
:split filename # 수평 분할
:sp filename # 수평 분할 (축약)
:vsplit filename # 수직 분할
:vsp filename # 수직 분할 (축약)
# 빈 창 분할
:split # 수평
:vsplit # 수직
창 간 이동
Ctrl+w h # 왼쪽 창
Ctrl+w j # 아래 창
Ctrl+w k # 위 창
Ctrl+w l # 오른쪽 창
Ctrl+w w # 다음 창
창 크기 조절
Ctrl+w = # 모든 창 크기 동일
Ctrl+w + # 현재 창 높이 증가
Ctrl+w - # 현재 창 높이 감소
Ctrl+w > # 현재 창 너비 증가
Ctrl+w < # 현재 창 너비 감소
필수 설정 (.vimrc)
.vimrc 파일 생성
# 홈 디렉토리에 .vimrc 생성
vim ~/.vimrc
추천 기본 설정
" 기본 설정
set nocompatible " Vi 호환 모드 끄기
syntax on " 문법 강조
set number " 줄 번호 표시
set relativenumber " 상대 줄 번호
set cursorline " 커서 줄 강조
set ruler " 커서 위치 표시
" 들여쓰기
set autoindent " 자동 들여쓰기
set smartindent " 스마트 들여쓰기
set tabstop=4 " 탭 크기
set shiftwidth=4 " 자동 들여쓰기 크기
set expandtab " 탭을 공백으로 변환
" 검색
set hlsearch " 검색 결과 강조
set incsearch " 점진적 검색
set ignorecase " 대소문자 무시
set smartcase " 대문자 포함 시 대소문자 구분
" UI
set showmatch " 괄호 매칭 표시
set wildmenu " 명령어 자동완성
set laststatus=2 " 상태바 항상 표시
set showcmd " 명령어 표시
" 편의 기능
set mouse=a " 마우스 사용
set clipboard=unnamed " 시스템 클립보드 사용
set encoding=utf-8 " UTF-8 인코딩
set backspace=indent,eol,start " 백스페이스 동작
" 파일 타입별 설정
filetype plugin indent on
" 색상 테마
colorscheme desert " 색상 테마 (기본 제공)
몇 가지 설정은 서버 환경에서 기대와 다르게 동작합니다. set clipboard=unnamed는 Vim이 +clipboard 기능으로 빌드되어 있어야 하는데, 서버의 기본 vim이나 vim-tiny는 대부분 -clipboard라서 설정해도 효과가 없습니다(vim --version | grep clipboard로 확인). 게다가 SSH로 접속한 원격 Vim의 “시스템 클립보드”는 서버 쪽 클립보드라서, 내 PC로 복사되지 않습니다. set mouse=a는 편리하지만 켜면 터미널 프로그램의 마우스 드래그 복사가 Vim에 가로채이므로, 원격에서 텍스트를 복사할 일이 많다면 끄거나 Shift를 누른 채 드래그해야 합니다. smartindent는 C 계열 기준의 오래된 규칙이라 filetype plugin indent on과 함께 쓰면 Python 등에서 오히려 들여쓰기가 어긋날 수 있어, 파일 타입별 들여쓰기에 맡기는 편이 낫습니다.
언어별 설정
" Python
autocmd FileType python setlocal tabstop=4 shiftwidth=4 expandtab
" JavaScript
autocmd FileType javascript setlocal tabstop=2 shiftwidth=2 expandtab
" Go
autocmd FileType go setlocal tabstop=4 shiftwidth=4 noexpandtab
" Makefile (탭 필수)
autocmd FileType make setlocal noexpandtab
치트시트
모드 전환
| 명령어 | 동작 |
|---|---|
ESC | Normal 모드로 |
i | Insert 모드 (커서 위치) |
a | Insert 모드 (커서 다음) |
o | Insert 모드 (아래 새 줄) |
v | Visual 모드 |
: | Command 모드 |
필수 명령어 Top 20
| 명령어 | 동작 | 사용 빈도 |
|---|---|---|
:wq | 저장 후 종료 | ⭐⭐⭐⭐⭐ |
:q! | 강제 종료 | ⭐⭐⭐⭐⭐ |
dd | 줄 삭제 | ⭐⭐⭐⭐⭐ |
yy | 줄 복사 | ⭐⭐⭐⭐ |
p | 붙여넣기 | ⭐⭐⭐⭐⭐ |
u | 실행 취소 | ⭐⭐⭐⭐⭐ |
/pattern | 검색 | ⭐⭐⭐⭐ |
:%s/old/new/g | 전체 치환 | ⭐⭐⭐⭐ |
gg | 파일 맨 앞 | ⭐⭐⭐ |
G | 파일 맨 뒤 | ⭐⭐⭐ |
0 | 줄 맨 앞 | ⭐⭐⭐ |
$ | 줄 맨 뒤 | ⭐⭐⭐ |
dw | 단어 삭제 | ⭐⭐⭐ |
cw | 단어 변경 | ⭐⭐⭐ |
A | 줄 끝에서 입력 | ⭐⭐⭐ |
o | 아래 새 줄 | ⭐⭐⭐ |
Ctrl+f | 한 화면 아래 | ⭐⭐ |
Ctrl+b | 한 화면 위 | ⭐⭐ |
. | 마지막 명령 반복 | ⭐⭐⭐ |
* | 단어 검색 | ⭐⭐ |
실전 시나리오
시나리오 1: 설정 파일 수정
# 1. 파일 열기
vim /etc/nginx/nginx.conf
# 2. 특정 줄 찾기 (커서는 "listen" 단어의 첫 글자로 이동)
/listen
# 3. 포트 번호로 이동 후 변경 (listen 80; → listen 8080;)
w # 다음 단어(80)로 이동
cw8080<ESC> # 80을 8080으로 변경 (cw는 ;앞에서 멈춤)
# 4. 저장 후 종료
:wq
/listen으로 검색하면 커서는 줄의 맨 앞이 아니라 일치한 단어의 첫 글자에 놓입니다. 그 상태에서 바로 cw를 누르면 포트가 아니라 listen이라는 단어가 바뀌므로, w로 한 단어 옮긴 뒤 바꿔야 합니다. nginx.conf처럼 root 소유 파일을 일반 사용자로 열었다가 수정 후에 E45: 'readonly' option is set 또는 E212: Can't open file for writing을 만나는 일도 흔합니다. :w!는 읽기 전용 표시를 무시할 뿐 파일 권한을 넘지 못하므로, 이때는 :w !sudo tee % > /dev/null로 저장하거나, 애초에 sudoedit /etc/nginx/nginx.conf로 여는 것이 안전합니다. 수정한 뒤에는 nginx -t로 설정 문법을 검사하고 나서 reload하는 습관이 중요합니다.
시나리오 2: 로그 파일 분석
# 1. 로그 파일 열기
vim /var/log/app.log
# 2. 에러 검색
/ERROR
# 3. 다음 에러로 이동
n
# 4. 에러 줄 복사
yy
# 5. 종료 (저장 안 함)
:q
수백 MB짜리 로그를 Vim으로 열면 파일 전체를 메모리에 읽고 스왑 파일까지 만들기 때문에 열리는 데 오래 걸리고 메모리를 크게 씁니다. 읽기만 할 거라면 view(읽기 전용 Vim)나 less를 쓰고, 에러 줄만 보려면 grep ERROR /var/log/app.log | less처럼 먼저 걸러 내는 편이 서버에 부담이 적습니다. 현재 기록 중인 로그를 Vim으로 열어 두면 새로 추가되는 줄이 보이지 않는다는 점도 기억해 두세요. 실시간으로 봐야 하면 tail -f나 less +F가 맞습니다.
시나리오 3: 코드 리팩토링
# 1. 파일 열기
vim app.py
# 2. 함수명 변경 (oldFunc → newFunc)
:%s/oldFunc/newFunc/g
# 3. 확인
:noh # 검색 강조 제거
# 4. 저장
:w
붙여넣기 들여쓰기가 계단처럼 밀릴 때
외부에서 복사한 코드를 Insert 모드에서 터미널 붙여넣기(우클릭, Ctrl+Shift+V)로 넣으면 줄마다 들여쓰기가 계단처럼 누적되는 경우가 있습니다. 터미널은 붙여넣은 텍스트를 사람이 한 글자씩 타이핑한 것처럼 Vim에 보내고, Vim은 줄바꿈마다 autoindent를 적용하기 때문입니다. Vim 8 이상은 bracketed paste를 지원해 대부분 자동으로 처리되지만, 오래된 Vim이나 일부 터미널에서는 여전히 발생하므로 붙여넣기 전에 :set paste, 끝난 뒤 :set nopaste를 쓰면 됩니다. 클립보드 레지스터를 지원하도록 빌드된 Vim이라면(vim --version에 +clipboard) Normal 모드에서 "+p로 붙여넣는 방법도 이 문제를 피합니다.
다음 단계
더 익히려면
- 플러그인 매니저 설치: vim-plug, Vundle
- 인기 플러그인:
- NERDTree (파일 탐색기)
- fzf.vim (파일 검색)
- vim-airline (상태바)
- YouCompleteMe (자동완성)
- Neovim 고려: Vim의 현대적 포크
대안 에디터
- Nano: 더 쉬운 터미널 에디터
- Emacs: Vim의 라이벌
- VSCode + Vim 플러그인: GUI + Vim 키바인딩
자주 묻는 질문 (FAQ)
Q. 파일 전체를 치환할 때 실수를 줄이려면 어떻게 하나요?
A. :%s/old/new/g는 파일 전체의 모든 일치 항목을 한 번에 바꾸므로, 확신이 없다면 끝에 c 플래그를 붙인 :%s/old/new/gc로 하나씩 확인하며 치환하는 것이 안전합니다. 특정 구간만 바꿔야 한다면 :5,10s/old/new/g처럼 줄 범위를 지정합니다. 치환 전에 /old로 먼저 검색해 어떤 부분이 걸리는지 확인해 두고, 결과가 잘못됐다면 Normal 모드에서 u로 되돌릴 수 있습니다.