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

치트시트

모드 전환

명령어동작
ESCNormal 모드로
iInsert 모드 (커서 위치)
aInsert 모드 (커서 다음)
oInsert 모드 (아래 새 줄)
vVisual 모드
: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로 붙여넣는 방법도 이 문제를 피합니다.


다음 단계

더 익히려면

  1. 플러그인 매니저 설치: vim-plug, Vundle
  2. 인기 플러그인:
    • NERDTree (파일 탐색기)
    • fzf.vim (파일 검색)
    • vim-airline (상태바)
    • YouCompleteMe (자동완성)
  3. 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로 되돌릴 수 있습니다.


같이 보면 좋은 글