본문으로 건너뛰기

Claude Code 햅틱 알림

· 약 4분

문제: Claude Code는 기다려주지 않는다​

Claude Code를 쓰다 보면 이런 상황이 반복돼요.

  1. Claude에게 작업을 시키고 브라우저로 전환
  2. 한참 뒤에 돌아와 보니 "권한 승인 대기 중"에서 멈춰 있습니다
  3. 또는 이미 끝났는데 모르고 한참을 기다립니다

터미널을 계속 쳐다보고 있을 수도 없고, 다른 일을 하자니 Claude가 언제 저를 부르는지 알 수가 없습니다.

그런데 손에 쥐고 있는 마우스가 진동으로 알려준다면?

claude-code-logitech-haptic-plugin​

claude-code-logitech-haptic-plugin은 Claude Code의 이벤트를 Logitech MX Master 4의 햅틱 모터와 연결해주는 플러그인입니다.

작동 원리는 단순합니다.

Claude Code 이벤트 → Hook → 쉘 스크립트 → HTTP API → HapticWebPlugin → MX Master 4 진동

Claude Code는 hooks 시스템을 통해 특정 이벤트 발생 시 외부 스크립트를 실행할 수 있습니다. 이 플러그인은 그 hook에 햅틱 API 호출을 연결한 것입니다.

Claude Code 이벤트가 마우스 진동으로 전달되는 흐름

어떤 상황에서 진동하는가​

세 가지 이벤트에 각각 다른 진동 패턴이 매핑되어 있습니다.

상황진동 패턴느낌
권한 승인 요청knock톡톡 — 노크하듯 두 번
60초 이상 입력 대기ringing르르르 — 전화 오듯 울림
작업 완료completed뚝 — 깔끔한 완료 펄스

손바닥에 닿아 있는 마우스에서 미세한 진동이 오기 때문에, 다른 모니터를 보거나 문서를 읽고 있어도 바로 알 수 있습니다. 소리 알림과 달리 주변 사람을 방해하지도 않습니다.

MX Master 4 사용자에게 추천하는 이유​

MX Master 4는 엄지 쪽에 햅틱 피드백 패널(Haptic Sense Panel)이 들어간 모델입니다. 이 햅틱 모터를 외부 앱 알림에도 쓰게 해 주는 것이 HapticWebPlugin이고, 이 플러그인은 그것을 Claude Code와 연결합니다.

이미 MX Master 4를 쓰고 있다면 추가 하드웨어 없이 소프트웨어만으로 완성됩니다.

설치​

1단계: HapticWebPlugin 설치​

Logi Options+에서 플러그인을 설치합니다.

  1. Logi Options+ 열기 → MX Master 4 선택
  2. HAPTIC FEEDBACK 탭 → 설정(⚙️) 클릭
  3. INSTALL AND UNINSTALL PLUGINS 버튼
  4. 레포의 plugin/HapticWeb.lplug4 파일을 더블클릭
  5. Continue로 설치 완료

설치 확인: haptics.jmw.nz/playground에서 진동 패턴을 테스트합니다.

2단계: Claude Code 플러그인 설치​

git clone https://github.com/ChefJodlak/claude-code-logitech-haptic-plugin.git
cd claude-code-logitech-haptic-plugin
./install.sh

설치 스크립트가 다음 작업을 자동으로 수행합니다.

  • OS 감지
  • HapticWebPlugin 연결 확인
  • ~/.claude/hooks/ 디렉토리에 스크립트 설치
  • ~/.claude/settings.json에 hook 설정 추가
  • 테스트 진동 실행

3단계: Claude Code 재시작​

Hook 설정은 시작 시 로드되므로, 실행 중인 Claude Code를 재시작하면 바로 적용됩니다.

진동 패턴 커스터마이징​

기본 매핑이 마음에 들지 않으면 ~/.claude/hooks/haptic-trigger.sh에서 변경합니다.

WAVEFORM_PERMISSION="knock" # 권한 요청
WAVEFORM_IDLE="ringing" # 입력 대기
WAVEFORM_COMPLETE="completed" # 작업 완료

선택 가능한 진동 패턴은 15가지입니다.

분류패턴
정밀 피드백sharp_collision, damp_collision, subtle_collision, damp_state_change
진행/완료sharp_state_change, completed, firework, happy_alert, wave
긴급 알림angry_alert, mad, square
수신 알림knock, ringing, jingle

작업 완료 시 firework(축하 폭죽 패턴)을 쓰면 기분이 좋아질 수도 있습니다.

진동 강도 조절​

Logi Options+ → MX Master 4 → Haptic Feedback → Strength 슬라이더로 조절합니다. Subtle, Low, Medium, High 4단계가 있습니다. API로는 강도 제어가 안 되고 Logi Options+에서만 가능합니다.

동작 확인 및 트러블슈팅​

# HapticWebPlugin 동작 확인
curl -s https://local.jmw.nz:41443/

# 수동으로 진동 테스트
curl -X POST -d "" https://local.jmw.nz:41443/haptic/completed

# hook 스크립트 수동 테스트
echo '{"hook_event_name": "Stop"}' | ~/.claude/hooks/haptic-trigger.sh

진동이 안 된다면 다음 사항을 확인합니다.

  • Logi Options+가 실행 중인지 확인
  • MX Master 4가 연결되어 있는지 확인
  • ~/.claude/hooks/haptic-trigger.sh에 실행 권한이 있는지 확인 (chmod +x)

제거​

cd claude-code-logitech-haptic-plugin
./uninstall.sh

마치며​

이 플러그인의 가치는 기술적 복잡성이 아니라 컨텍스트 스위칭 비용을 줄여준다는 점에 있습니다. Claude Code에게 작업을 시키고 다른 일을 할 수 있게 되면, AI 코딩의 실질적인 생산성이 달라집니다.

MX Master 4 사용자라면 한 번 설치해볼 만해요. 설치에 5분도 안 걸리고, 되돌리기도 쉬워요.

GitHub: ChefJodlak/claude-code-logitech-haptic-plugin

Obsidian CLI 입문

· 약 5분

이 글은 Obsidian CLI 시리즈의 첫 번째 글이에요.

  1. Obsidian CLI 입문: 터미널에서 노트를 다루는 새로운 방법 ← 현재 글
  2. Obsidian CLI 비교: 공식 CLI vs URI vs 직접 파일 조작
  3. Obsidian CLI 자동화: 스크립트와 워크플로우 실전 가이드

문제: 노트 하나 쓰려고 GUI를 열어야 하는가​

터미널에서 코드를 쓰다가 떠오른 생각을 메모하고 싶을 때가 있습니다.

  1. ⌘+Tab으로 Obsidian 창을 찾습니다
  2. 데일리 노트를 엽니다
  3. 적당한 위치에 커서를 놓고 입력합니다
  4. 다시 ⌘+Tab으로 터미널로 돌아옵니다

한 줄 메모에 4단계. 이런 상황이 반복되면 점점 메모를 안 하게 됩니다.

자동화도 마찬가지입니다. 매일 데일리 노트를 만들고, 커밋 로그를 기록하고, 태그를 정리하고 싶은데, GUI에서는 이걸 스크립트로 만들 수가 없습니다.

2026년 2월, Obsidian v1.12에서 공식 CLI가 추가됐습니다.

Obsidian CLI란​

Obsidian CLI는 Obsidian v1.12.0에서 Early Access로 출시되고, v1.12.4부터 모든 사용자에게 무료로 공개된 공식 커맨드라인 인터페이스입니다.

100개 이상의 명령어를 제공하며, 데일리 노트 작성부터 검색, 태그 관리, 노트 생성, 파일 비교까지 Obsidian의 핵심 기능을 터미널에서 사용할 수 있습니다.

동작 구조는 다음과 같습니다:

터미널 명령어 → CLI → 로컬 소켓 → Obsidian 프로세스 → 볼트 조작

Obsidian CLI가 실행 중인 앱을 거쳐 볼트를 조작하는 경로

CLI가 Obsidian 내부 API를 통해 작동하기 때문에, 파일을 이동하면 위키링크가 자동으로 업데이트되고, 속성을 변경하면 인덱스에 즉시 반영됩니다. 단, Obsidian 데스크톱 앱이 실행 중이어야 합니다 — 헤드리스 도구가 아닙니다.

활성화​

1. CLI 활성화​

Obsidian을 열고 Settings → General → Command line interface를 켭니다.

화면의 안내에 따라 시스템 PATH에 CLI를 추가하고, 터미널을 재시작합니다.

2. 설치 확인​

obsidian version

정상이면 버전 번호가 출력됩니다.

3. 볼트 확인​

# 등록된 볼트 목록
obsidian vaults

터미널의 현재 디렉터리가 볼트 폴더면 그 볼트를, 아니면 현재 활성 볼트를 기본으로 사용합니다. 다른 볼트를 지정하려면 명령 앞에 vault= 파라미터를 붙입니다:

# 특정 볼트를 대상으로 실행 (vault=는 첫 번째 파라미터여야 함)
obsidian vault="MyVault" daily

데일리 노트​

CLI의 킬러 피처입니다. 터미널에서 데일리 노트를 열고, 내용을 추가하고, 태스크를 확인할 수 있습니다.

기본 사용​

# 오늘의 데일리 노트 열기 (없으면 자동 생성)
obsidian daily

# 데일리 노트 내용을 터미널에 출력
obsidian daily:read

내용 추가​

# 데일리 노트에 텍스트 추가
obsidian daily:append content="## 오후 회의 메모
- 배포 일정 확인
- 버그 #234 논의"

태스크 확인​

# 오늘 데일리 노트의 태스크 목록
obsidian tasks daily

실전 예제: git 커밋 로그를 데일리 노트에 기록​

터미널에서 하루 작업을 마치고, 오늘의 커밋을 데일리 노트에 자동으로 기록할 수 있습니다:

obsidian daily:append content="## Git 커밋 요약
$(git log --oneline --since='08:00' --until='now')"

이 한 줄이면 됩니다. GUI를 열 필요도, 복사-붙여넣기를 할 필요도 없습니다.

검색과 읽기​

볼트 검색​

# 키워드로 볼트 전체 검색
obsidian search query="프로젝트 일정"

# 특정 경로 안에서만 검색
obsidian search query="배포" path="Work/"

검색 옵션:

옵션설명예시
query키워드 검색obsidian search query="TODO"
path경로 제한obsidian search query="회의" path="Daily/"

노트 읽기​

# 노트 내용을 터미널에 출력
obsidian read path="Projects/blog-migration.md"

# 활성 파일 읽기
obsidian read

터미널에서 노트를 읽을 수 있다는 건, grep이나 fzf와 조합할 수 있다는 뜻입니다.

파일 비교​

# 노트의 변경 이력 확인
obsidian diff path="Projects/blog-migration.md"

노트 생성​

# 새 노트 생성
obsidian create name="Meeting/2026-04-14-standup"

# 템플릿을 적용해서 생성
obsidian create name="Projects/new-feature" template="Templates/project"

# 내용과 함께 생성
obsidian create name="TIL/obsidian-cli" content="# Obsidian CLI
오늘 배운 것: 터미널에서 노트를 다루는 방법"

GUI에서 템플릿을 쓰는 것과 동일하게, template 옵션으로 지정한 템플릿이 적용됩니다. Obsidian 내부 API를 통해 생성되므로 플러그인(Templater 등)도 정상 작동합니다.

태그 관리​

# 볼트의 모든 태그와 사용 횟수
obsidian tags counts

출력 예시:

태그노트 수
#project23
#til45
#meeting12
#blog8

특정 태그가 달린 노트를 찾으려면:

# 태그 정보와 해당 노트 목록 조회
obsidian tag name=project verbose

태그 사용 현황을 한눈에 파악하고, 정리가 필요한 태그를 빠르게 찾을 수 있습니다.

주요 명령어 정리​

명령어설명
obsidian daily오늘의 데일리 노트 열기/생성
obsidian daily:append content="..."데일리 노트에 내용 추가
obsidian daily:read데일리 노트 내용 출력
obsidian tasks daily데일리 노트의 태스크 목록
obsidian search query="..."볼트 전체 검색
obsidian read path="..."노트 내용을 터미널에 출력
obsidian diff path="..."노트 변경 이력 확인
obsidian create name="..."새 노트 생성
obsidian create ... template=...템플릿으로 노트 생성
obsidian tags counts태그별 사용 횟수
obsidian tag name=... verbose태그가 달린 노트 목록
obsidian vaults볼트 목록
obsidian vault="..." <명령>특정 볼트 지정

마치며​

Obsidian CLI는 Obsidian을 터미널 시민으로 만들어줍니다. 생각이 떠오른 순간에서 기록까지의 거리가 짧아집니다.

설정에 1분, 첫 명령어 실행에 10초예요. 이미 Obsidian을 쓰고 있다면 한번 켜보세요.

다음 글에서는 공식 CLI 외에 URI scheme과 직접 파일 조작까지 포함해서, Obsidian을 터미널에서 제어하는 세 가지 방법을 비교해요.

참고 자료​

나의 tmux 설정

· 약 7분

tmux란?​

tmux(Terminal Multiplexer)는 하나의 터미널 안에서 여러 셸 세션을 관리할 수 있는 도구예요.

왜 쓰는가?

하나의 터미널을 여러 패널로 나눠 동시에 작업하고, SSH 연결이 끊어져도 세션이 유지됩니다. 서버에서 tmux를 띄워두면 언제든 다시 붙을 수 있고(detach/attach), 복잡한 윈도우/패널 레이아웃도 스크립트로 한 번에 구성합니다.

설치​

# macOS
brew install tmux

# Rocky Linux / CentOS
dnf install -y tmux

# Ubuntu / Debian
apt install -y tmux

기본 개념​

tmux
├── Session (세션)
│ ├── Window 1 (윈도우 = 탭)
│ │ ├── Pane 1 (패널 = 분할 영역)
│ │ └── Pane 2
│ └── Window 2
│ └── Pane 1
└── Session 2
└── ...

tmux 세션 안에 윈도우와 패널이 중첩되는 구조

Session은 독립적인 작업 단위입니다. 프로젝트별로 세션을 만들면 좋습니다. Window는 세션 안의 탭으로, 화면 전체를 차지합니다. Pane은 윈도우를 분할한 패널입니다. 여러 셸을 동시에 볼 수 있습니다.

필수 단축키​

tmux의 모든 단축키는 Prefix 키를 먼저 누른 뒤 명령키를 누릅니다.

저는 Prefix를 Ctrl+Space로 설정했어요 (기본값 Ctrl+b도 유지).

세션​

키동작
tmux new -s 이름새 세션 생성
Prefix d세션에서 분리 (detach)
tmux attach -t 이름세션에 다시 붙기
tmux ls세션 목록
tmux kill-session -t 이름세션 삭제

윈도우​

키동작
Prefix c새 윈도우
Prefix n / Prefix p다음/이전 윈도우
Prefix 숫자해당 번호 윈도우로 이동
Prefix ,윈도우 이름 변경
Prefix &윈도우 닫기

패널​

키동작
Prefix %세로 분할
Prefix "가로 분할
Prefix h/j/k/l패널 이동 (내 설정)
Prefix x패널 닫기
Prefix z패널 풀스크린 토글 (zoom)
Prefix Space레이아웃 순환

Prefix z는 유용합니다. 패널 하나를 전체화면으로 확대했다가 다시 원래 크기로 되돌립니다.

저의 tmux.conf 전체 설정​

Prefix 키​

# Ctrl+Space를 메인 Prefix로, Ctrl+b도 유지
set -g prefix C-Space
set -g prefix2 C-b
bind C-Space send-prefix

기본 Ctrl+b는 손이 불편해요. Ctrl+Space가 훨씬 자연스럽습니다.

기본 옵션​

# 히스토리 50000줄 유지
set-option -g history-limit 50000

# 분할 시 현재 경로 유지
bind % split-window -h -c "#{pane_current_path}"
bind '"' split-window -v -c "#{pane_current_path}"

# 트루컬러 지원
set -g default-terminal "tmux-256color"
set -ag terminal-overrides ",xterm-256color:RGB"

# 윈도우 이름 자동 변경 끄기
set -g automatic-rename off

# 마우스 지원
set -g mouse on

# 터미널 escape sequence 통과 (이미지 미리보기 등)
set -g allow-passthrough on

패널 테두리 커스터마이징​

# 패널 상단에 인덱스와 명령어 표시
set -g pane-border-format " #{pane_index} #{pane_current_command} "
set -g pane-border-status top
set -g pane-border-lines double

# 활성 패널 강조 (파란색)
set -g pane-active-border-style "fg=#89b4fa"
set -g pane-border-style "fg=#585b70"

# 활성/비활성 윈도우 배경색 차이
set -g window-active-style "bg=#1e1e2e"
set -g window-style "bg=#11111b"

비활성 패널의 배경을 약간 어둡게 하면 현재 작업 중인 패널이 바로 눈에 들어옵니다.

vi 복사 모드​

setw -g mode-keys vi

# v로 선택 시작, y로 복사 (시스템 클립보드에)
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi y send-keys -X copy-pipe-and-cancel "pbcopy"
bind-key -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel "pbcopy"

# 마우스 드래그로 선택 시 클립보드에 복사
bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-no-clear "pbcopy"

Prefix [로 복사 모드에 진입하면 Vim처럼 v로 선택, y로 복사합니다. pbcopy로 macOS 클립보드에 바로 들어갑니다.

마우스 더블클릭으로 단어 선택​

# 단어 구분자에서 -./?를 제외 (URL, 경로를 한 덩어리로 선택)
set -g word-separators " =()[]{};,!@#$%^&*+|<>\"'"

# 더블클릭으로 단어 선택
bind -T copy-mode-vi DoubleClick1Pane select-pane \; send-keys -X select-word
bind -n DoubleClick1Pane select-pane \; copy-mode -M \; send-keys -X select-word

마우스 스크롤 속도​

# 기본 3줄 → 5줄씩 스크롤
bind-key -T copy-mode-vi WheelUpPane send-keys -X -N 5 scroll-up
bind-key -T copy-mode-vi WheelDownPane send-keys -X -N 5 scroll-down

패널 이동 (hjkl)​

bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R
bind L last-window # 마지막 윈도우로 이동
bind r source-file ~/.tmux.conf \; display "Reloaded!"

Neovim과 동일한 hjkl 방향키를 사용합니다. Prefix r로 설정을 다시 불러옵니다.

yazi 파일 탐색기 연동​

# Prefix + Tab: 오른쪽 50% 분할에서 yazi 실행
bind Tab split-window -h -l 50% -c "#{pane_current_path}" "yazi"

yazi는 터미널 파일 관리자입니다. Prefix Tab으로 현재 디렉토리에서 바로 엽니다.

플러그인​

TPM (Tmux Plugin Manager)​

# 설치
git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm

tmux 실행 후 Prefix + I(대문자)로 플러그인을 설치합니다.

사용 중인 플러그인​

set -g @plugin 'tmux-plugins/tpm' # 플러그인 매니저
set -g @plugin 'catppuccin/tmux' # 테마
set -g @plugin 'tmux-plugins/tmux-cpu' # CPU 사용률
set -g @plugin 'tmux-plugins/tmux-battery' # 배터리 표시
set -g @plugin 'tmux-plugins/tmux-resurrect' # 세션 저장/복원
set -g @plugin 'tmux-plugins/tmux-continuum' # 자동 저장/복원
플러그인역할
catppuccin/tmuxCatppuccin Mocha 테마
tmux-cpu상태바에 CPU/메모리 사용률 표시
tmux-battery상태바에 배터리 잔량 표시
tmux-resurrectPrefix Ctrl-s로 세션 저장, Prefix Ctrl-r로 복원
tmux-continuum15분마다 자동 저장, tmux 시작 시 자동 복원

tmux-resurrect + continuum​

set -g @continuum-restore 'on' # tmux 시작 시 자동 복원
set -g @continuum-save-interval '15' # 15분마다 자동 저장

이 조합 덕분에 시스템을 재시작해도 tmux 세션이 그대로 살아납니다. 윈도우 레이아웃, 패널 구성, 작업 디렉토리까지 복원됩니다.

Catppuccin 테마 + 상태바​

set -g @catppuccin_flavor 'mocha'
set -g @catppuccin_window_status_style 'rounded'

상태바 구성​

왼쪽: 세션 이름

set -g status-left "#{E:@catppuccin_status_session}"

오른쪽: 디렉토리 → Git 브랜치 → CPU → 메모리 → 배터리 → 시간

set -g status-right "#{E:@catppuccin_status_directory}"
set -ag status-right " #(cd #{pane_current_path}; git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '-') "
set -ag status-right " #{cpu_percentage} "
set -ag status-right " #{ram_percentage} "
set -ag status-right " #{battery_icon} #{battery_percentage} "
set -ag status-right "#{E:@catppuccin_status_date_time}"

상태바에 Git 브랜치가 표시되어서 어떤 브랜치에서 작업 중인지 바로 알 수 있습니다.

실전 활용: 다중 서버 접속 자동화​

tmux의 진짜 힘은 스크립트로 레이아웃을 자동 구성할 수 있다는 점입니다.

예를 들어 운영 서버 여러 대에 동시에 접속하는 스크립트를 만들 수 있습니다:

#!/bin/bash
SESSION="my-servers"

# 이미 세션이 있으면 재사용
tmux has-session -t $SESSION 2>/dev/null && {
tmux attach -t $SESSION
exit 0
}

# 윈도우 1: 전체 서버 모니터링 (tiled 레이아웃)
tmux new-session -d -s $SESSION -n "all" "ssh server1"
tmux split-window -t $SESSION:all "ssh server2"
tmux split-window -t $SESSION:all "ssh server3"
tmux split-window -t $SESSION:all "ssh server4"
tmux select-layout -t $SESSION:all tiled

# 윈도우 2: DB 서버만
tmux new-window -t $SESSION -n "db" "ssh db-primary"
tmux split-window -t $SESSION:db -h "ssh db-replica"

tmux select-window -t $SESSION:all
tmux attach -t $SESSION

이 스크립트를 실행하면:

┌─────────────┬─────────────┐
│ server1 │ server2 │
├─────────────┼─────────────┤
│ server3 │ server4 │
└─────────────┴─────────────┘

이런 레이아웃이 한 번에 만들어집니다. 저는 이 방식으로 환경별(beta/prod) x 리전별 서버 접속 스크립트를 만들어서 사용하고 있어요.

synchronize-panes​

모든 패널에 동일한 명령을 동시에 입력할 수도 있습니다:

# 동기화 모드 토글
Prefix :setw synchronize-panes on

이 상태에서 타이핑하면 모든 패널에 같은 명령이 입력됩니다. 여러 서버에 동시에 같은 작업을 할 때 유용합니다.

설정 적용 방법​

1. TPM 설치​

git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm

2. tmux.conf 저장​

위 설정 내용을 ~/.tmux.conf에 저장합니다.

3. 플러그인 설치​

tmux # tmux 실행
# Prefix + I (Shift+i) # 플러그인 설치

4. 설정 리로드​

# tmux 안에서
Prefix r

# 또는 터미널에서
tmux source-file ~/.tmux.conf

5. yazi 설치 (선택)​

# macOS
brew install yazi

# 기타: https://yazi-rs.github.io/docs/installation

정리​

기능설정
PrefixCtrl+Space (+ Ctrl+b)
패널 이동Prefix h/j/k/l
복사 모드vi 스타일 (v 선택, y 복사)
테마Catppuccin Mocha
상태바세션, 디렉토리, Git, CPU, RAM, 배터리, 시간
세션 복원resurrect + continuum (자동)
파일 탐색Prefix Tab (yazi)
설정 리로드Prefix r

tmux는 터미널에서 작업하는 시간이 길수록 효과가 큽니다. 특히 원격 서버 작업이 많다면 세션 유지와 다중 접속 자동화만으로도 생산성이 크게 올라갑니다.

부록: tmux 단축키 & 플러그인 학습 페이지​

별도로 정리한 인터랙티브 학습 페이지도 있어요. 단축키를 카테고리별로 정리해두었으니 참고하면 좋습니다.

→ tmux 단축키 & 플러그인 학습

참고 자료​

나의 Neovim 설정 전체 공개

· 약 5분

이 글은 Neovim 시리즈의 네 번째 글이에요.

  1. Neovim 입문: Vim을 넘어서는 첫걸음
  2. Neovim 중급: 생산성을 높이는 기능들
  3. Neovim 고급: 플러그인과 LSP로 IDE처럼 쓰기
  4. 나의 Neovim 설정 전체 공개 ← 현재 글
  5. LazyVim distro로 갈아타기

설정 철학​

설정은 Lua 기반으로 VimScript 대신 Lua로 모두 작성하고, 모듈화를 위해 기능별로 파일을 분리해서 관리합니다. 최소주의에 따라 꼭 필요한 플러그인만 23개로 유지하며, 일관된 키매핑을 위해 커스텀 keyMapper 유틸리티로 통일했습니다.

디렉토리 구조​

~/.config/nvim/
├── init.lua # 진입점 (1줄)
├── lazy-lock.json # 플러그인 버전 고정
└── lua/
├── config/
│ ├── init.lua # config 모듈 진입점
│ ├── globals.lua # 전역 변수 (leader 키 등)
│ ├── options.lua # Neovim 옵션
│ └── keymaps.lua # 글로벌 키매핑
├── plugins/
│ ├── alpha.lua # 시작 화면
│ ├── comment.lua # 주석 토글
│ ├── conform.lua # 코드 포매팅
│ ├── indent-blankline.lua # 들여쓰기 가이드
│ ├── kanagawa.lua # 컬러스킴
│ ├── lsp.lua # LSP 설정
│ ├── lualine.lua # 상태줄
│ ├── neo-tree.lua # 파일 탐색기
│ ├── nvim-autopairs.lua # 자동 괄호
│ ├── nvim-cmp.lua # 자동완성
│ ├── nvim-treesitter.lua # 구문 하이라이팅
│ ├── nvim-ufo.lua # 코드 폴딩
│ ├── render-markdown.lua # 마크다운 렌더링
│ ├── telescope.lua # 퍼지 파인더
│ └── vim-floaterm.lua # 플로팅 터미널
└── utils/
└── keyMapper.lua # 키매핑 헬퍼

핵심은 init.lua가 단 1줄이라는 점입니다.

require("config")

config/init.lua에서 globals, options, keymaps, lazy.nvim 순서로 로드합니다.

Neovim 설정 로드 순서

핵심 옵션​

-- lua/config/options.lua
opt = vim.opt

-- 2칸 탭
opt.tabstop = 2
opt.shiftwidth = 2
opt.softtabstop = 2
opt.expandtab = true
opt.smartindent = true
opt.wrap = false

-- 검색
opt.incsearch = true
opt.ignorecase = true
opt.smartcase = true -- 대문자가 포함되면 대소문자 구분

-- 줄 번호
opt.number = true
opt.relativenumber = true -- 상대 줄 번호 (이동에 유용)

-- 기타
opt.termguicolors = true
opt.signcolumn = "yes"
opt.scrolloff = 10 -- 커서 위아래 10줄 여유
opt.mouse:append("a")

-- 클립보드
opt.clipboard = "unnamedplus" -- y/p가 시스템 클립보드와 자동 동기화

-- 마크다운 전용 설정
vim.api.nvim_create_autocmd("FileType", {
pattern = "markdown",
callback = function()
vim.opt_local.wrap = true -- 줄 바꿈 활성화
vim.opt_local.linebreak = true -- 단어 단위로 줄 바꿈
vim.opt_local.conceallevel = 2 -- 문법 마커 숨기기
end,
})

relativenumber는 처음에는 어색하지만, 5j, 12k 같은 상대 이동을 할 때 줄 수를 바로 알 수 있어서 매우 편합니다.

시스템 클립보드 연동​

기본 Neovim에서 y는 내부 무명 레지스터에만 저장되고 macOS 시스템 클립보드와는 분리돼 있습니다. 시스템 클립보드로 복사하려면 매번 "+y처럼 접두사를 붙여야 하는데, 한글 IME 상태나 tmux 안에서 "(Shift+') 입력이 종종 씹혀서 번거롭습니다.

opt.clipboard = "unnamedplus" 한 줄로 해결됩니다. 비주얼 모드에서 y만 눌러도 macOS 클립보드에 바로 들어가고, 다른 앱에서 Cmd+C 한 내용도 p로 바로 붙여넣어집니다.

Neovim 시스템 클립보드 동기화 흐름

LazyVim 배포판에서는 기본값이지만, lazy.nvim만 직접 쓰는 수동 구성에서는 이 한 줄을 빼먹기 쉽습니다.

값 선택​

값의미
unnamed* 레지스터 (Linux X11의 primary selection)
unnamedplus+ 레지스터 (시스템 클립보드) — 권장

macOS에서는 *와 +가 실질적으로 같지만, 크로스 플랫폼 호환성 측면에서 unnamedplus가 표준입니다. LazyVim 기본값도 이것.

확인​

:set clipboard?

→ clipboard=unnamedplus가 출력되면 OK.

:checkhealth provider

→ clipboard provider 섹션에 에러가 없어야 합니다.

안 될 때​

  • which pbcopy 로 /usr/bin/pbcopy 확인 (macOS 기본 탑재).
  • Neovim 빌드에 클립보드 지원이 있는지: :echo has('clipboard') → 1.
  • tmux 안에서만 안 되는 경우, ~/.tmux.conf 에 set -g set-clipboard on. 다만 macOS 로컬 tmux + pbcopy 조합은 대부분 이 설정 없이도 동작합니다.

keyMapper 유틸리티​

모든 키매핑에 일관되게 noremap과 silent를 적용하기 위해 만든 헬퍼입니다.

-- lua/utils/keyMapper.lua
local keyMapper = function(from, to, mode, opts)
local options = { noremap = true, silent = true }
mode = mode or "n"

if opts then
options = vim.tbl_extend("force", options, opts)
end

vim.keymap.set(mode, from, to, options)
end

return { mapKey = keyMapper }

사용법:

local mapKey = require("utils.keyMapper").mapKey

mapKey("<leader>e", ":Neotree toggle<cr>") -- Normal 모드 (기본)
mapKey("<", "<gv", "v") -- Visual 모드 지정

전체 키매핑​

글로벌 키매핑​

키동작모드
SpaceLeader 키-
<leader>eNeo-tree 파일 탐색기 토글N
<leader>h검색 하이라이트 제거N
Ctrl-h/j/k/l분할 창 이동N
< / >들여쓰기 유지하며 인덴트V
Ctrl-;플로팅 터미널 토글N

Telescope 키매핑​

키동작
<leader>ff파일 이름 검색
<leader>fg파일 내용 검색 (grep)
<leader>fb버퍼 목록
<leader>fh도움말 검색

LSP 키매핑​

키동작
K호버 문서
gd정의로 이동
<leader>ca코드 액션

플러그인 전체 목록 (lazy.nvim 제외 23개)​

핵심​

플러그인역할
lazy.nvim플러그인 매니저
telescope.nvim퍼지 파인더 (파일/텍스트/버퍼 검색)
neo-tree.nvim사이드바 파일 탐색기
nvim-cmp자동완성 엔진
nvim-lspconfigLSP 클라이언트 설정
mason.nvim언어 서버/포매터 설치 관리
nvim-treesitter구문 파싱 & 하이라이팅
conform.nvim저장 시 자동 포매팅

자동완성 소스​

플러그인소스
cmp-nvim-lspLSP 자동완성
cmp-buffer버퍼 텍스트
cmp-path파일 경로
cmp_luasnip스니펫
LuaSnip스니펫 엔진
friendly-snippetsVS Code 스니펫 모음

UI & 외관​

플러그인역할
kanagawa.nvim컬러스킴 (dragon 테마)
lualine.nvim하단 상태줄
alpha-nvim시작 화면 대시보드
nvim-web-devicons파일 아이콘
indent-blankline.nvim들여쓰기 시각 가이드

편집 보조​

플러그인역할
Comment.nvimgcc로 주석 토글
nvim-autopairs괄호/따옴표 자동 닫기
nvim-ufoLSP 기반 코드 폴딩
vim-floaterm플로팅 터미널
render-markdown.nvim마크다운 실시간 렌더링

테마: Kanagawa Dragon​

Kanagawa의 Dragon 변형을 사용합니다. 일본 전통 색상에서 영감을 받은 다크 테마로, 눈의 피로가 적습니다.

커스터마이징 항목:

overrides = function(colors)
local theme = colors.theme
return {
-- 플로팅 윈도우 배경 투명화
NormalFloat = { bg = "none" },
FloatBorder = { bg = "none" },
FloatTitle = { bg = "none" },

-- Telescope UI 커스터마이징
TelescopePromptNormal = { bg = theme.ui.bg_p1 },
TelescopeResultsNormal = { fg = theme.ui.fg_dim, bg = theme.ui.bg_m1 },
TelescopePreviewNormal = { bg = theme.ui.bg_dim },

-- 자동완성 팝업
Pmenu = { fg = theme.ui.shade0, bg = theme.ui.bg_p1 },
PmenuSel = { fg = "NONE", bg = theme.ui.bg_p2 },
}
end,
theme = "dragon",

상태줄(lualine)은 Gruvbox 테마를 사용해서 본문과 미묘하게 다른 톤을 줍니다.

마크다운 작성 환경​

블로그를 Neovim으로 작성하기 때문에 마크다운 환경을 신경 썼습니다.

마크다운 환경에서는 render-markdown.nvim으로 헤딩, 코드 블록, 테이블, 체크박스를 시각적으로 렌더링하고, 줄 바꿈 활성화를 위해 마크다운 파일에서만 wrap = true로 설정합니다. conceallevel 2로 **bold** 같은 마커를 숨기고 bold 형태로 표시합니다.

LSP 구성​

Mason으로 3개 언어 서버를 관리합니다.

서버언어포매터
lua_lsLuastylua
ts_lsTypeScript/JavaScriptprettierd
goplsGogofmt (내장)

새 언어를 추가하려면:

  1. :Mason에서 언어 서버 설치
  2. lsp.lua의 ensure_installed에 추가
  3. conform.lua에 포매터 추가 (필요 시)

정리​

이 설정은 계속 발전 중이에요. Neovim의 장점은 제 워크플로우에 맞게 모든 것을 조정할 수 있다는 점이에요. 처음에는 남의 설정을 복사하더라도, 하나씩 이해하면서 자기 것으로 만들어가는 과정이 중요해요.

Neovim 고급 활용

· 약 5분

이 글은 Neovim 시리즈의 세 번째 글이에요.

  1. Neovim 입문: Vim을 넘어서는 첫걸음
  2. Neovim 중급: 생산성을 높이는 기능들
  3. Neovim 고급: 플러그인과 LSP로 IDE처럼 쓰기 ← 현재 글
  4. 나의 Neovim 설정 전체 공개

플러그인 매니저: lazy.nvim​

Neovim 플러그인 매니저 중 가장 인기 있는 것이 lazy.nvim입니다. 지연 로딩(lazy loading)을 기본으로 지원해서 시작 속도가 빠릅니다.

설치 (부트스트랩)​

~/.config/nvim/init.lua에 아래 내용을 추가하면 lazy.nvim이 없을 때 자동으로 설치됩니다.

local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.uv.fs_stat(lazypath) then -- Neovim 0.9 이하는 vim.loop
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")

마지막 줄의 "plugins"는 ~/.config/nvim/lua/plugins/ 디렉토리를 의미합니다. 이 디렉토리에 파일을 추가하면 자동으로 플러그인이 로드됩니다.

플러그인 추가 방법​

lua/plugins/ 안에 파일을 만들고 테이블을 반환하면 됩니다.

-- lua/plugins/example.lua
return {
"작성자/플러그인이름",
config = function()
require("플러그인").setup({
-- 옵션
})
end
}

플러그인 관리: :Lazy

LSP (Language Server Protocol)​

LSP는 에디터와 언어 서버 사이의 표준 프로토콜입니다. 코드 자동완성, 정의로 이동, 에러 표시, 리팩토링 등 IDE 기능의 핵심입니다.

구조​

Neovim ←→ nvim-lspconfig ←→ Language Server
(lua_ls, gopls, ts_ls, ...)

Neovim LSP 도구 연결 구조

Mason으로 언어 서버 설치​

Mason은 언어 서버, 포매터, 린터를 Neovim 안에서 설치/관리하는 도구입니다.

-- lua/plugins/lsp.lua
return {
{
"mason-org/mason.nvim",
config = function()
require("mason").setup()
end
},
{
"mason-org/mason-lspconfig.nvim",
config = function()
require("mason-lspconfig").setup({
ensure_installed = { "lua_ls", "ts_ls", "gopls" }
})
end
},
{
"neovim/nvim-lspconfig",
config = function()
vim.lsp.enable({ "lua_ls", "ts_ls", "gopls" })

-- LSP 키매핑
vim.keymap.set("n", "K", vim.lsp.buf.hover)
vim.keymap.set("n", "gd", vim.lsp.buf.definition)
vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action)
end
}
}
키동작
K커서 위 심볼의 문서 표시
gd정의로 이동
<leader>ca코드 액션 (자동 수정 등)

Mason UI: :Mason (설치된 서버 확인/추가/삭제)

자동완성: nvim-cmp​

nvim-cmp는 Neovim의 자동완성 엔진입니다. 여러 소스(LSP, 버퍼, 파일 경로, 스니펫)에서 후보를 가져옵니다.

-- lua/plugins/nvim-cmp.lua
return {
"hrsh7th/nvim-cmp",
dependencies = {
"hrsh7th/cmp-nvim-lsp", -- LSP 소스
"hrsh7th/cmp-buffer", -- 버퍼 텍스트 소스
"hrsh7th/cmp-path", -- 파일 경로 소스
"L3MON4D3/LuaSnip", -- 스니펫 엔진
"saadparwaiz1/cmp_luasnip", -- 스니펫 소스
"rafamadriz/friendly-snippets", -- 스니펫 모음
},
config = function()
local cmp = require("cmp")
local luasnip = require("luasnip")

require("luasnip.loaders.from_vscode").lazy_load()

cmp.setup({
snippet = {
expand = function(args)
luasnip.lsp_expand(args.body)
end,
},
mapping = cmp.mapping.preset.insert({
["<C-Space>"] = cmp.mapping.complete(),
["<CR>"] = cmp.mapping.confirm({ select = true }),
["<Tab>"] = cmp.mapping.select_next_item(),
["<S-Tab>"] = cmp.mapping.select_prev_item(),
}),
sources = cmp.config.sources({
{ name = "nvim_lsp" },
{ name = "luasnip" },
{ name = "buffer" },
{ name = "path" },
}),
})
end
}

자동완성 키:

키동작
Ctrl-Space수동으로 완성 목록 열기
Tab다음 후보
Shift-Tab이전 후보
Enter선택 확정

Treesitter: 구문 하이라이팅​

Treesitter는 코드를 파싱해서 정확한 구문 하이라이팅을 제공합니다. 정규식 기반의 기존 하이라이팅보다 훨씬 정확합니다.

-- lua/plugins/nvim-treesitter.lua
return {
"nvim-treesitter/nvim-treesitter",
branch = "master", -- configs 모듈은 master 브랜치 API. main 브랜치는 호환되지 않는 재작성판
build = ":TSUpdate",
config = function()
require("nvim-treesitter.configs").setup({
ensure_installed = {
"lua", "go", "javascript", "html",
"markdown", "markdown_inline",
},
highlight = { enable = true },
indent = { enable = true },
})
end
}

새 언어 추가: :TSInstall python

Telescope: 퍼지 파인더​

Telescope는 파일, 텍스트, 버퍼, Git 등 모든 것을 검색할 수 있는 퍼지 파인더입니다.

-- lua/plugins/telescope.lua
return {
"nvim-telescope/telescope.nvim",
tag = "0.1.5", -- 작성 당시 태그. 최신 태그는 저장소에서 확인
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)
vim.keymap.set("n", "<leader>fb", builtin.buffers)
vim.keymap.set("n", "<leader>fh", builtin.help_tags)
end
}
키동작
<leader>ff파일 이름 검색
<leader>fg파일 내용 검색 (grep)
<leader>fb열린 버퍼 목록
<leader>fh도움말 검색

live_grep를 사용하려면 ripgrep이 설치되어 있어야 합니다: brew install ripgrep

파일 탐색기: Neo-tree​

-- lua/plugins/neo-tree.lua
return {
"nvim-neo-tree/neo-tree.nvim",
branch = "v3.x",
dependencies = {
"nvim-lua/plenary.nvim",
"nvim-tree/nvim-web-devicons",
"MunifTanjim/nui.nvim",
},
keys = {
{ "<leader>e", "<cmd>Neotree toggle<cr>", desc = "Neo-tree" },
},
}

<leader>e로 사이드바 파일 탐색기를 토글할 수 있습니다. VS Code의 Explorer와 비슷한 역할입니다.

코드 포매팅: conform.nvim​

-- lua/plugins/conform.lua
return {
"stevearc/conform.nvim",
config = function()
require("conform").setup({
formatters_by_ft = {
lua = { "stylua" },
javascript = { "prettierd", "prettier", stop_after_first = true },
typescript = { "prettierd", "prettier", stop_after_first = true },
},
format_on_save = {
timeout_ms = 500,
lsp_format = "fallback",
},
})
end
}

파일 저장 시 자동으로 포매팅됩니다. 포매터는 Mason으로 설치할 수 있습니다: :Mason에서 stylua, prettierd를 검색합니다.

추천 플러그인 조합​

최소한의 IDE 환경을 위한 추천 조합:

카테고리플러그인역할
플러그인 관리lazy.nvim플러그인 매니저
LSPmason + lspconfig언어 서버
자동완성nvim-cmp코드 완성
구문treesitter하이라이팅
검색telescope퍼지 파인더
파일 탐색neo-tree파일 트리
포매팅conform.nvim자동 포매팅
테마kanagawa / tokyonight / catppuccin컬러스킴

다음 글​

이 플러그인들을 실제로 어떻게 조합하고 커스터마이징하는지, 저의 전체 Neovim 설정을 공개할게요.

→ 나의 Neovim 설정 전체 공개

참고 자료​

SSH ProxyJump 접속

· 약 4분

문제 상황​

클라우드 환경에서 흔히 볼 수 있는 구성이 있어요. 보안을 위해 내부 서버(DB, 앱 서버 등)는 외부에서 직접 접속할 수 없고, Bastion 서버(점프 서버)를 통해서만 접근할 수 있는 구조예요.

[내 PC] → [Bastion 서버] → [내부 서버]
(공인 IP) (사설 IP)

ProxyJump로 Bastion을 통과해 내부 서버에 연결하는 경로

이런 구조에서 내부 서버에 접속하려면 보통 두 단계를 거칩니다.

# 1단계: Bastion 서버에 접속
ssh user@bastion-host

# 2단계: Bastion에서 내부 서버에 접속
ssh user@internal-host

매번 두 번 SSH로 접속하는 건 번거롭고, 파일 전송(scp)은 더 복잡해집니다. SSH ProxyJump를 사용하면 이 과정을 한 줄로 줄일 수 있습니다.

ProxyJump란?​

OpenSSH 7.3(2016년)부터 추가된 기능으로, SSH 접속 시 중간 서버를 점프 호스트로 지정할 수 있습니다. 중간 서버에 셸을 열지 않고, TCP 포워딩만 수행합니다.

주요 특징은 다음과 같습니다.

  • Bastion 서버에 로그인하지 않고 통과만 합니다.
  • 최종 목적지까지 암호화가 유지됩니다(Bastion에서 트래픽을 볼 수 없습니다).
  • 파일 전송(scp, rsync)도 동일하게 동작합니다.

기본 사용법: -J 플래그​

ssh -J user@bastion-host user@internal-host

이 한 줄로 Bastion을 거쳐 내부 서버에 바로 접속합니다.

예시​

# Bastion(203.0.113.10)을 거쳐 내부 DB 서버(10.0.1.50)에 접속
ssh -J admin@203.0.113.10 dbadmin@10.0.1.50

포트가 다른 경우​

# Bastion이 2222 포트를 사용하는 경우
ssh -J admin@203.0.113.10:2222 dbadmin@10.0.1.50

SSH Config로 영구 설정​

매번 -J 플래그를 타이핑하는 건 여전히 번거롭습니다. ~/.ssh/config에 설정해두면 ssh internal-db만으로 접속됩니다.

# Bastion 서버
Host bastion
HostName 203.0.113.10
User admin
IdentityFile ~/.ssh/id_ed25519

# 내부 DB 서버 (Bastion 경유)
Host internal-db
HostName 10.0.1.50
User dbadmin
ProxyJump bastion
IdentityFile ~/.ssh/id_ed25519

이제 다음과 같이 접속합니다.

ssh internal-db

파일 전송도 자연스럽게 동작합니다.

# 내부 서버로 파일 복사
scp backup.sql internal-db:/tmp/

# 내부 서버에서 파일 가져오기
scp internal-db:/var/log/app.log ./

# rsync도 동일
rsync -avz ./deploy/ internal-db:/var/www/app/

다중 점프 (Multi-hop)​

Bastion이 여러 단계인 경우도 지원합니다.

[내 PC] → [Bastion 1] → [Bastion 2] → [최종 서버]

-J 플래그 방식​

ssh -J user@bastion1,user@bastion2 user@final-host

쉼표로 구분하면 순서대로 점프합니다.

SSH Config 방식​

Host bastion1
HostName 203.0.113.10
User admin

Host bastion2
HostName 10.0.1.1
User admin
ProxyJump bastion1

Host final-server
HostName 10.0.2.50
User deploy
ProxyJump bastion2

final-server → bastion2 → bastion1 → 인터넷 순서의 체인으로 연결됩니다.

ProxyJump vs ProxyCommand​

SSH Config에는 비슷한 역할을 하는 ProxyCommand가 있습니다. 차이점은 다음과 같습니다.

# ProxyJump (간단)
Host internal
ProxyJump bastion

# ProxyCommand (구버전 호환)
Host internal
ProxyCommand ssh -W %h:%p bastion
ProxyJumpProxyCommand
도입OpenSSH 7.3+OpenSSH 5.4+
문법간단복잡
다중 점프쉼표로 구분중첩 필요
커맨드라인-J 플래그-o ProxyCommand=...

OpenSSH 7.3 이상이면 ProxyJump를 쓰는 편이 낫습니다. 더 간단하고 다중 점프도 깔끔합니다.

OpenSSH 버전은 ssh -V로 확인합니다.

실전 팁​

1. Bastion 서버에 에이전트 포워딩​

ProxyJump를 쓰면 로컬의 SSH 키로 내부 서버에 직접 인증하므로 ForwardAgent가 필요 없습니다. Bastion 셸에 로그인한 뒤 그 안에서 다시 ssh로 접속할 때만 에이전트 포워딩을 활성화합니다.

Host bastion
HostName 203.0.113.10
User admin
ForwardAgent yes

보안 주의: ForwardAgent는 Bastion 서버의 root가 에이전트 소켓에 접근할 수 있으므로, 신뢰할 수 있는 서버에서만 사용해야 합니다.

2. 접속 끊김 방지 (KeepAlive)​

점프 호스트를 거치면 타임아웃이 발생하기 쉽습니다.

Host *
ServerAliveInterval 60
ServerAliveCountMax 3

60초마다 keepalive 패킷을 보내서 연결을 유지합니다.

3. 포트 포워딩과 함께 사용​

Bastion을 거쳐 내부 DB 포트를 로컬로 포워딩할 수도 있습니다.

# 내부 PostgreSQL(5432)을 로컬 15432로 포워딩
ssh -J bastion -L 15432:10.0.1.50:5432 admin@10.0.1.50 -N

이제 localhost:15432로 내부 DB에 접속합니다.

psql -h localhost -p 15432 -U postgres

SSH Config로 설정하면 더 깔끔합니다.

Host db-tunnel
HostName 10.0.1.50
User dbadmin
ProxyJump bastion
LocalForward 15432 localhost:5432
ssh -N db-tunnel
# 다른 터미널에서: psql -h localhost -p 15432 -U postgres

정리​

상황명령어
단일 점프ssh -J bastion internal
다중 점프ssh -J bastion1,bastion2 final
파일 전송scp -J bastion file internal:/path
포트 포워딩ssh -J bastion -L 15432:db:5432 db-host -N

SSH ProxyJump는 Bastion 서버 환경에서 꼭 알아둘 만한 기능이에요. SSH Config에 한 번 설정해두면 내부 서버도 직접 연결된 것처럼 쓸 수 있어요.

Neovim 중급 활용

· 약 4분

이 글은 Neovim 시리즈의 두 번째 글이에요.

  1. Neovim 입문: Vim을 넘어서는 첫걸음
  2. Neovim 중급: 생산성을 높이는 기능들 ← 현재 글
  3. Neovim 고급: 플러그인과 LSP로 IDE처럼 쓰기
  4. 나의 Neovim 설정 전체 공개

텍스트 오브젝트​

Vim의 진짜 힘은 동사 + 대상 조합입니다. 텍스트 오브젝트는 "대상"에 해당합니다.

문법​

{동사}{범위}{대상}

동사는 d(삭제), c(변경), y(복사), v(선택)이며, 범위는 i(inner, 안쪽), a(around, 바깥 포함)입니다. 대상은 w(단어), ", ', ), }, ], t(태그) 등입니다.

실전 예시​

커서가 단어 중간에 있을 때:

명령동작
ciw단어를 지우고 입력 모드 (Change Inner Word)
diw단어 삭제
yiw단어 복사

따옴표 안에 커서가 있을 때:

명령동작
ci"따옴표 안의 내용을 지우고 입력
di"따옴표 안의 내용 삭제
da"따옴표까지 포함해서 삭제

괄호 안에 커서가 있을 때:

명령동작
ci(괄호 안 내용을 변경
da{중괄호를 포함해서 삭제
vi[대괄호 안 내용을 선택

i vs a의 차이​

print("hello world")

커서가 hello 위에 있을 때:

  • di" → print("") (따옴표 안 내용만 삭제)
  • da" → print() (따옴표까지 삭제)

텍스트 오브젝트를 익히면 편집 속도가 비약적으로 올라갑니다.

버퍼, 윈도우, 탭​

Neovim 버퍼 윈도우 탭 관계

버퍼​

파일을 열면 버퍼에 로드됩니다. 여러 파일을 동시에 열어 둡니다.

:e file1.lua 파일 열기 (새 버퍼)
:ls 열린 버퍼 목록
:bn 다음 버퍼
:bp 이전 버퍼
:bd 버퍼 닫기
:b {숫자} 해당 번호 버퍼로 이동

윈도우 (분할)​

화면을 분할해 여러 버퍼를 동시에 봅니다.

:split 가로 분할 (또는 Ctrl-w s)
:vsplit 세로 분할 (또는 Ctrl-w v)
Ctrl-w h/j/k/l 분할 창 간 이동
Ctrl-w = 분할 창 크기 균등화
:close 현재 창 닫기

탭​

여러 윈도우 레이아웃을 탭으로 관리합니다.

:tabnew 새 탭
:tabn 다음 탭 (또는 gt)
:tabp 이전 탭 (또는 gT)
:tabclose 탭 닫기

실무 팁: 버퍼 + 윈도우 분할을 주로 쓰고, 탭은 맥락이 완전히 다른 작업을 분리할 때 쓰면 좋습니다.

매크로​

반복 작업을 녹화해서 재생하는 기능입니다.

기본 사용법​

  1. q{레지스터}: 녹화 시작 (예: qa)
  2. 원하는 작업 수행
  3. q: 녹화 중지
  4. @{레지스터}: 재생 (예: @a)
  5. @@: 마지막 매크로 재생
  6. {숫자}@a: N번 반복 재생

실전 예시​

모든 줄 끝에 세미콜론을 추가하고 싶다면:

qa a 레지스터에 녹화 시작
A; 줄 끝에 ; 추가
Esc Normal 모드로
j 다음 줄로
q 녹화 중지
99@a 99번 반복

레지스터​

Vim의 복사/붙여넣기는 레지스터라는 저장소를 사용합니다. 클립보드가 여러 개 있는 것과 같습니다.

주요 레지스터​

레지스터설명
""기본 레지스터 (마지막 yank/delete)
"0yank 전용 레지스터
"+시스템 클립보드
"a ~ "z이름 지정 레지스터
"_블랙홀 레지스터 (버리기)

사용법​

"ay a 레지스터에 복사
"ap a 레지스터에서 붙여넣기
"+y 시스템 클립보드에 복사
"+p 시스템 클립보드에서 붙여넣기
"_dd 줄을 삭제하되 레지스터에 저장하지 않음

레지스터 확인: :reg

팁: dd로 줄을 지우면 기본 레지스터가 덮어씌워집니다. 복사한 내용을 유지하려면 "0p(yank 레지스터)를 쓰거나, 삭제 시 "_dd(블랙홀)를 쓰면 됩니다.

Marks​

특정 위치를 북마크해두고 바로 이동할 수 있습니다.

ma 현재 위치를 a 마크로 저장
'a a 마크의 줄로 이동
`a a 마크의 정확한 위치로 이동
:marks 모든 마크 목록
  • 소문자(a-z): 파일 내 마크
  • 대문자(A-Z): 파일 간 글로벌 마크

비주얼 블록 모드​

Ctrl-v로 사각형 영역을 선택할 수 있습니다. 여러 줄을 동시에 편집할 때 유용합니다.

여러 줄 앞에 텍스트 추가​

1. Ctrl-v 블록 선택 시작
2. j/k로 줄 선택
3. I 줄 앞에 입력 모드
4. 텍스트 입력
5. Esc 모든 선택된 줄에 적용

여러 줄 뒤에 텍스트 추가​

1. Ctrl-v 블록 선택 시작
2. j/k로 줄 선택
3. $ 줄 끝까지 선택 확장
4. A 줄 뒤에 입력 모드
5. 텍스트 입력
6. Esc 적용

내장 터미널​

Neovim 안에서 :terminal로 터미널을 엽니다.

:terminal 터미널 열기

터미널에서 Normal 모드로 전환: Ctrl-\ Ctrl-n

저는 내장 터미널 대신 vim-floaterm 플러그인을 사용합니다. Ctrl-;로 플로팅 터미널을 토글할 수 있어 더 편합니다.

기본 설정 시작하기​

Neovim 설정 파일은 ~/.config/nvim/init.lua입니다. 간단한 설정부터 시작합니다.

-- 줄 번호 표시
vim.opt.number = true
vim.opt.relativenumber = true

-- 탭 설정 (2칸)
vim.opt.tabstop = 2
vim.opt.shiftwidth = 2
vim.opt.expandtab = true

-- 검색 설정
vim.opt.ignorecase = true
vim.opt.smartcase = true
vim.opt.incsearch = true

-- 스크롤 여유
vim.opt.scrolloff = 10

-- 마우스 지원
vim.opt.mouse:append("a")

이 정도만 설정해도 기본 Neovim보다 훨씬 쾌적해집니다.

다음 글​

기본기가 탄탄해졌다면, 플러그인과 LSP를 설정해서 Neovim을 IDE처럼 사용하는 방법을 알아봐요.

→ Neovim 고급: 플러그인과 LSP로 IDE처럼 쓰기

참고 자료​

Neovim 입문

· 약 4분

이 글은 Neovim 시리즈의 첫 번째 글이에요.

  1. Neovim 입문: Vim을 넘어서는 첫걸음 ← 현재 글
  2. Neovim 중급: 생산성을 높이는 기능들
  3. Neovim 고급: 플러그인과 LSP로 IDE처럼 쓰기
  4. 나의 Neovim 설정 전체 공개

Neovim이란?​

Neovim은 Vim의 모던 포크(fork)입니다. Vim의 철학(모달 편집, 키보드 중심)을 유지하면서 내부를 현대적으로 재구성했습니다.

Vim과 뭐가 다른가?

VimNeovim
설정 언어VimScriptLua (VimScript도 지원)
비동기 처리제한적내장
내장 LSP없음있음
내장 터미널기본적완전한 터미널 에뮬레이터
플러그인 생태계성숙빠르게 성장 중
설정 파일~/.vimrc~/.config/nvim/init.lua

한 줄로 요약하면 Vim의 편집 철학 + 현대적인 확장성입니다.

설치​

macOS​

brew install neovim

Rocky Linux / CentOS​

# EPEL에서 설치
dnf install -y epel-release
dnf install -y neovim

Ubuntu / Debian​

apt install -y neovim

설치 확인:

nvim --version

4가지 모드​

Neovim(Vim)이 다른 에디터와 결정적으로 다른 점은 모드가 있다는 것입니다.

Neovim 네 가지 모드 전환

Normal 모드​

Neovim을 열면 이 모드입니다. 글을 쓰는 게 아니라 글을 조작하는 모드입니다.

  • 커서 이동, 삭제, 복사, 붙여넣기 등
  • 다른 모드에서 Esc를 누르면 항상 Normal로 돌아옵니다

Insert 모드​

실제로 글을 타이핑하는 모드입니다.

키동작
i커서 앞에 입력
a커서 뒤에 입력
I줄 맨 앞에 입력
A줄 맨 뒤에 입력
o아래에 새 줄 만들고 입력
O위에 새 줄 만들고 입력

Visual 모드​

텍스트를 선택하는 모드입니다.

키동작
v문자 단위 선택
V줄 단위 선택
Ctrl-v블록(사각형) 선택

Command 모드​

:를 누르면 하단에 명령줄이 나타납니다.

:w 저장
:q 종료
:wq 저장 후 종료
:q! 저장하지 않고 종료

커서 이동​

Normal 모드에서의 이동이 Vim의 핵심입니다. 처음에는 어색하지만 손에 익으면 마우스보다 빠릅니다.

기본 이동​

k
h l
j
키방향
h← 왼쪽
j↓ 아래
k↑ 위
l→ 오른쪽

팁: j의 아래쪽 꼬리를 생각하면 기억하기 쉽습니다.

단어 단위 이동​

키동작
w다음 단어 시작
b이전 단어 시작
e단어 끝

줄 이동​

키동작
0줄 맨 앞
$줄 맨 끝
^줄의 첫 글자 (공백 제외)

화면/파일 이동​

키동작
gg파일 맨 위
G파일 맨 아래
Ctrl-d반 페이지 아래
Ctrl-u반 페이지 위
{숫자}G해당 줄로 이동 (예: 42G)

기본 편집​

삭제​

키동작
x커서 위 문자 삭제
dd줄 삭제
dw단어 삭제
d$ 또는 D커서부터 줄 끝까지 삭제

복사 & 붙여넣기​

키동작
yy줄 복사 (yank)
yw단어 복사
p커서 뒤에 붙여넣기
P커서 앞에 붙여넣기

실행 취소 & 다시 실행​

키동작
u실행 취소 (Undo)
Ctrl-r다시 실행 (Redo)

검색과 치환​

검색​

/검색어 아래 방향으로 검색
?검색어 위 방향으로 검색
n 다음 결과
N 이전 결과

치환​

:s/old/new/ 현재 줄에서 첫 번째만
:s/old/new/g 현재 줄에서 모두
:%s/old/new/g 파일 전체에서 모두
:%s/old/new/gc 파일 전체, 하나씩 확인하며

파일 열기​

# 터미널에서
nvim filename.txt

# Neovim 안에서
:e filename.txt
:e . 현재 디렉토리 탐색

숫자 + 명령 조합​

Vim의 강력한 문법: 숫자 + 동작으로 반복할 수 있습니다.

5j 5줄 아래로
3dd 3줄 삭제
4w 4단어 앞으로
10G 10번째 줄로
2yy 2줄 복사

vimtutor로 연습하기​

Neovim에는 대화형 튜토리얼이 내장되어 있습니다.

nvim +Tutor

30분 정도 따라 하면 기본 조작이 손에 익습니다. 처음 Neovim을 설치했다면 반드시 한 번은 해보세요.

다음 글​

기본 조작이 익숙해졌다면 텍스트 오브젝트, 매크로, 레지스터 등 생산성을 끌어올리는 기능을 알아봐요.

→ Neovim 중급: 생산성을 높이는 기능들

참고 자료​

커스텀 도메인과 SSL

· 약 4분
노트

이 글은 Hugo 시절(2026-04)의 기록입니다. 사이트는 2026-08에 Docusaurus로 통합했습니다(전환 글).

이 글은 Hugo + PaperMod 블로그 세팅 시리즈의 마지막 글이에요.

  1. Hugo + PaperMod로 기술 블로그 만들기
  2. NCP 서버에 Hugo 블로그 배포하기
  3. 커스텀 도메인 연결과 Let's Encrypt SSL 설정 ← 현재 글

진행 순서​

도메인과 SSL 설정에는 순서가 있습니다. 순서가 바뀌면 인증서 발급이 실패합니다.

도메인 구매 → DNS A 레코드 설정 → Nginx server_name 변경
→ hugo.toml baseURL 변경 → certbot SSL 발급 → 재배포

도메인 구매부터 SSL 발급과 재배포까지의 설정 순서

1. 도메인 구매​

도메인 구매처는 여러 곳이 있습니다.

업체특징
Cloudflare원가 판매, DNS 관리 편리
가비아국내 최대, 한글 지원
NCP서버와 같은 곳에서 관리

저는 Cloudflare를 선택했어요. DNS 관리 UI가 깔끔하고 추가 비용이 없어요.

.dev 도메인 참고사항​

.dev는 Google이 관리하는 TLD로, HSTS preload가 적용되어 있습니다.

  • 모든 .dev 도메인은 브라우저에서 HTTPS만 허용됩니다.
  • HTTP로는 접속 자체가 불가능합니다.
  • SSL 설정이 필수입니다.

기술 블로그에 어울리는 도메인이지만, SSL 없이는 쓸 수 없다는 점을 알고 있어야 합니다.

2. DNS 설정​

Cloudflare DNS에서 A 레코드를 추가합니다.

TypeNameContentProxy
A@서버IPDNS only
경고

Cloudflare Proxy를 반드시 꺼야 합니다(회색 구름). Cloudflare 프록시가 켜져 있으면 서버 IP가 Cloudflare로 대체되어 Let's Encrypt 인증서 발급이 복잡해집니다.

DNS 전파를 확인합니다.

dig +short dbalog.dev
# 서버IP가 나오면 성공

보통 Cloudflare는 수 분 내로 전파됩니다.

3. Hugo 설정 변경​

hugo.toml의 baseURL을 변경합니다.

# 변경 전
baseURL = "https://example.com/"
# 변경 후
baseURL = "https://dbalog.dev/"

이 값은 sitemap, RSS, 메타 태그 등 블로그 전체에서 사용되므로 반드시 변경해야 합니다.

4. Nginx server_name 변경​

deploy/nginx/blog.conf:

server_name dbalog.dev; # IP 또는 example.com에서 변경

서버에 반영:

scp deploy/nginx/blog.conf ncp-blog:/etc/nginx/conf.d/blog.conf
ssh ncp-blog "nginx -t && systemctl reload nginx"

5. Let's Encrypt SSL 인증서 발급​

certbot 설치​

Rocky Linux 9 기본 저장소에는 certbot이 없습니다. EPEL 저장소를 먼저 추가합니다.

ssh ncp-blog "dnf install -y epel-release"
ssh ncp-blog "dnf install -y certbot python3-certbot-nginx"

인증서 발급​

certbot의 --nginx 플러그인이 인증서 발급과 Nginx 설정 수정을 한번에 처리합니다.

ssh ncp-blog "certbot --nginx -d dbalog.dev \
--non-interactive \
--agree-tos \
--email your@email.com \
--redirect"
  • --nginx: Nginx 설정을 자동으로 수정
  • --redirect: HTTP→HTTPS 301 리다이렉트 자동 추가
  • --non-interactive: 대화형 프롬프트 없이 실행

성공하면 이런 메시지가 나옵니다.

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/dbalog.dev/fullchain.pem
Key is saved at: /etc/letsencrypt/live/dbalog.dev/privkey.pem
This certificate expires on 2026-07-09.

certbot이 Nginx에 추가한 설정​

listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/dbalog.dev/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dbalog.dev/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

그리고 HTTP→HTTPS 리다이렉트를 위한 별도 server 블록:

server {
if ($host = dbalog.dev) {
return 301 https://$host$request_uri;
}
listen 80;
server_name dbalog.dev;
return 404;
}

인증서 자동 갱신​

Let's Encrypt 인증서는 90일마다 만료됩니다. certbot이 systemd 타이머를 자동 등록하므로 수동 갱신이 필요 없습니다.

갱신 테스트:

ssh ncp-blog "certbot renew --dry-run"

6. 재배포​

baseURL이 변경되었으므로 빌드 후 재배포합니다.

./deploy.sh

확인​

# HTTPS 접속
curl -s -o /dev/null -w "%{http_code}" https://dbalog.dev/
# 200

# HTTP → HTTPS 리다이렉트
curl -s -o /dev/null -w "%{http_code}" http://dbalog.dev/
# 301

브라우저에서 https://dbalog.dev/에 접속하면 자물쇠 아이콘이 표시됩니다.

시리즈 마무리​

3편에 걸쳐 Hugo 블로그를 처음부터 세팅했어요.

  1. Hugo + PaperMod 설치 및 설정
  2. NCP 서버에 Nginx + rsync 배포
  3. 도메인 연결 + SSL 인증서

이제 남은 건 글을 쓰는 것뿐이고, 마크다운 파일을 만들고 ./deploy.sh를 실행하면 끝이에요.

참고 자료​

NCP Hugo 블로그 배포

· 약 4분
노트

이 글은 Hugo 시절(2026-04)의 기록입니다. 사이트는 2026-08에 Docusaurus로 통합했습니다(전환 글).

이 글은 Hugo + PaperMod 블로그 세팅 시리즈의 두 번째 글이에요.

  1. Hugo + PaperMod로 기술 블로그 만들기
  2. NCP 서버에 Hugo 블로그 배포하기 ← 현재 글
  3. 커스텀 도메인 연결과 Let's Encrypt SSL 설정

배포 구조​

[로컬 macOS] [NCP Rocky Linux]
content/*.md
↓ hugo --minify
public/ ──── rsync ────→ /var/www/blog/ ←── Nginx 서빙

Hugo가 생성한 정적 파일을 rsync로 서버에 전송하고, Nginx가 서빙하는 단순한 구조입니다. DB도 Node.js도 필요 없습니다.

Hugo 블로그 배포 흐름

1. SSH 키 인증 설정​

매번 비밀번호를 입력하는 건 번거롭고, 스크립트 자동화도 불가능합니다. SSH 키 인증을 먼저 세팅합니다.

키 생성​

ssh-keygen -t ed25519 -C "blog-deploy"
  • ed25519: RSA보다 짧고 안전한 알고리즘
  • passphrase: 비워도 되고, 입력하면 macOS Keychain이 기억해 줍니다

서버에 공개키 등록​

ssh-copy-id -i ~/.ssh/id_ed25519.pub root@서버IP

이때 마지막으로 서버 비밀번호를 입력합니다. 이후로는 비밀번호 없이 접속 가능합니다.

SSH config 설정 (선택)​

~/.ssh/config에 아래를 추가하면 ssh ncp-blog만으로 접속할 수 있습니다.

Host ncp-blog
HostName 서버IP
User root
IdentityFile ~/.ssh/id_ed25519

2. Nginx 설정 파일​

Hugo 정적 파일을 서빙하기 위한 Nginx 설정입니다.

server {
listen 80;
server_name 서버IP;

root /var/www/blog;
index index.html;

# gzip 압축
gzip on;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 1000;

# 정적 파일 캐싱 (30일)
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}

# Hugo 정적 사이트 서빙
location / {
try_files $uri $uri/ =404;
}

# 404 페이지
error_page 404 /404.html;
location = /404.html {
internal;
}

# 보안 헤더
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
# 레거시 헤더: 현대 브라우저는 무시한다
add_header X-XSS-Protection "1; mode=block" always;
}

핵심 설정:

  • try_files $uri $uri/ =404: 파일 → 디렉토리(index.html) → 404 순서로 탐색
  • gzip: 텍스트 기반 파일을 압축해서 전송 (대역폭 절약)
  • expires 30d: CSS/JS/이미지를 30일 캐싱 (재방문 시 빠른 로딩)

3. 서버 세팅​

SSH로 서버에 접속해서 실행합니다.

# Nginx 설치
dnf install -y nginx

# 블로그 디렉토리 생성
mkdir -p /var/www/blog

# Nginx 설정 파일 복사 (로컬에서)
scp deploy/nginx/blog.conf ncp-blog:/etc/nginx/conf.d/blog.conf

# 설정 검사 + 시작
ssh ncp-blog "nginx -t && systemctl enable nginx && systemctl start nginx"

systemctl enable은 서버가 재부팅되어도 Nginx가 자동 시작되게 합니다.

4. 배포 스크립트​

매번 명령어를 입력하는 것은 번거로우니 스크립트로 만듭니다.

deploy.sh:

#!/bin/bash
set -e

SERVER_USER="root"
SERVER_HOST="서버IP"
SERVER_PORT="22"
REMOTE_DIR="/var/www/blog"

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"

build() {
echo "=== Hugo 빌드 시작 ==="
hugo --minify
echo "=== 빌드 완료 ==="
}

deploy() {
echo "=== 서버 배포 시작 ==="
rsync -avz --delete \
-e "ssh -p ${SERVER_PORT}" \
public/ \
"${SERVER_USER}@${SERVER_HOST}:${REMOTE_DIR}/"
echo "=== 배포 완료 ==="
}

case "${1:-all}" in
build) build ;;
deploy) deploy ;;
all) build && deploy ;;
*) echo "사용법: $0 {build|deploy|all}" ; exit 1 ;;
esac

사용법:

chmod +x deploy.sh

./deploy.sh build # 빌드만
./deploy.sh deploy # 배포만
./deploy.sh # 빌드 + 배포

rsync의 --delete 옵션은 서버에서 로컬에 없는 파일을 삭제합니다. 글을 지웠을 때 서버에도 반영되게 하려면 필요합니다.

--delete 함정: 서버에 수동으로 올려둔 파일(임시 백업, 정적 다운로드 자료 등)도 같이 지워집니다. 빌드 산출물 외의 파일은 같은 디렉토리에 두지 마십시오. 보존이 필요하면 별도 디렉토리(/var/www/blog-extra/)로 분리하거나 --exclude='backups/' 패턴을 추가합니다.

5. NCP ACG (방화벽) 설정​

서버에서 curl localhost는 되는데 외부에서 접속되지 않는다면, NCP ACG 설정을 확인해야 합니다.

NCP 콘솔 → Server → ACG → 인바운드 규칙:

프로토콜포트허용 소스
TCP800.0.0.0/0
TCP4430.0.0.0/0

NCP는 OS 레벨 방화벽(firewalld)과 별개로 ACG라는 네트워크 방화벽을 사용합니다. 둘 다 확인해야 합니다.

배포 확인​

curl -s -o /dev/null -w "HTTP %{http_code}, %{time_total}s" http://서버IP/
# HTTP 200, 0.021s

다음 글​

IP 주소로 접속하는 블로그는 아무래도 불편해요. 다음 글에서는 커스텀 도메인을 연결하고 HTTPS를 설정할게요.

→ 커스텀 도메인 연결과 Let's Encrypt SSL 설정