Claude Code 를 본격적으로 쓰기 시작하면서 CLI 기반 에디터의 활용도가 부쩍 높아졌다. GUI 에디터에서 터미널을 띄우던 흐름이, 터미널 안에서 모든 것을 띄우는 흐름으로 뒤집혔다.

이 글은 위 화면을 만드는 dotfiles 의 구성이다. 같은 환경을 다른 머신에 그대로 옮기는 방법, 즉 git clone 한 줄과 bash setup.sh 한 줄로 끝나는 부트스트랩에 대한 글이기도 하다.

Claude Code 의 세부 설정 — skills, agents, hooks, MCP 같은 것들 — 은 별도 글로 정리할 예정이라 이번에는 짧게만 짚는다.

스택 구성

터미널 에뮬레이터는 alacritty. 그 창 안에서 화면을 셋으로 나누는 것이 tmux. 각 패인에서 실행되는 셸이 zsh. 좌상 패인의 에디터가 LazyVim 기반의 nvim, 우 30% 패인에서 Claude Code 가 AI 페어로 동작한다.

도구역할
alacritty터미널 에뮬레이터
tmux세션/창/패인 멀티플렉서
zsh
nvim (LazyVim)에디터 (좌상 패인)
Claude CodeAI 페어 (우 30% 패인)

이 글은 그 순서대로 — alacritty, tmux, zsh, nvim, Claude Code — 각 도구가 어떤 역할을 맡고 왜 그렇게 선택했는지 본다.

alacritty

alacritty는 Rust 로 작성된 크로스플랫폼 GPU 가속 터미널 에뮬레이터다. 자체 기능을 최소화하고, 화면 분할이나 세션 관리는 다른 도구에 위임하는 설계를 따른다.

터미널 에뮬레이터로 alacritty 를 골랐다. 이유는 셋이다.

  • GPU rendering — OpenGL 기반 렌더링으로 입력 지연이 작다.
  • config-as-code — 단 하나의 alacritty.toml 에 모든 설정이 모인다. 어디 저장됐는지 찾아 헤맬 일이 없다.
  • 단순함 — alacritty 는 탭, 분할, 세션 같은 기능을 의도적으로 포함하지 않는다. 이 자리는 tmux 가 채운다.

마지막 항목이 핵심이다. 분할과 세션을 alacritty 에 맡기지 않고 tmux 에 위임하면 같은 추상이 macOS 와 Linux 양쪽에서 동일하게 동작한다. 아래 계층이 단순할수록 위 계층의 이식성이 높아진다고 봤다.

공식 기본 설정은 거의 비어 있다. 색상도 폰트도 창 장식도 사용자가 직접 채워 넣기 전까지 alacritty 는 가장 순수한 “터미널” 에 가깝다. 이 빈 상태가 config-as-code 의 출발점이 된다.

내가 적용한 커스터마이징은 단순하다. 창 장식을 꺼서 macOS 의 타이틀 바를 없앴고, 여백을 빼서 픽셀 군더더기를 없앴고, 폰트는 nvim 의 devicons 가 렌더되도록 nerd font 계열로 두었고, 색상은 catppuccin mocha 를 toml 에 직접 적었다. 외부 yml/include 없이 한 파일에서 끝난다.

키바인딩은 Cmd 키 조합을 ESC 시퀀스로 변환한다.

[keyboard]
bindings = [
  { chars = "\u001Bh", key = "H", mods = "Command" },
  { chars = "\u001Bl", key = "L", mods = "Command" },
  { chars = "\u001Bw", key = "W", mods = "Command" },
]

macOS 만의 문제가 하나 있다. Cmd+H 가 OS 메뉴 레벨에서 “Hide Application” 으로 가로채진다. 이 키는 alacritty 의 키바인딩을 통해 ESC+h (즉 vim 의 M-h) 로 변환되어 nvim 에 전달돼야 하는데, AppKit 이 먼저 소비하면 그 변환이 일어나지 않는다. 그래서 setup-macos.sh 가 마지막에 한 줄을 추가한다.

defaults write org.alacritty NSUserKeyEquivalents -dict-add "Hide Alacritty" ""

이 한 줄로 alacritty 의 nvim 통합이 macOS 에서도 동작한다. 이 결정은 설정 파일로 해결되지 않아 설치 스크립트에 들어갔다.

tmux

tmux는 터미널 멀티플렉서다. 하나의 터미널 안에서 여러 세션, 창, 패인을 관리하고, 세션을 detach 해도 프로세스가 유지된다.

alacritty 위에서 화면을 나누는 일은 tmux 가 한다. 그래서 alacritty 에는 탭이 없고, 대신 tmux 세션 / 창 / 패인이 그 자리를 채운다.

설정은 기본값에서 크게 벗어나지 않는다. prefix 는 C-b 그대로 유지한다 (C-a 는 readline 의 line-start 와 충돌해 셸에서 방해가 된다). copy mode 는 vim 키로 동작하게 하고, nvim 과의 ESC 지연은 제거했다. 마지막 항목은 작은 설정이지만 nvim 사용자에게 효과가 크다.

tmux 는 ESC 키를 prefix 나 meta 시퀀스의 시작으로 해석하기 위해 대기 시간을 둔다. set -gs escape-time 0 으로 이를 제거하면 nvim 에서 모드 전환이 즉시 반영된다.

분할 키바인딩에 두 가지 결정이 글의 흐름과 직결된다. 우측에 Claude Code 패인을 띄우는 단축키, 그리고 그 배치를 한 키로 정리하는 단축키다. 뒤에 설명할 nc 함수는 이 분할을 함수 한 번으로 자동으로 구성하는 상위 도구에 해당한다.

bind i split-window -fh -p 30 -c "#{pane_current_path}" "claude"
bind o split-window -v -l 15% -c "#{pane_current_path}"

prefix i 는 우측에 Claude Code 패인을, prefix o 는 하단에 셸 패인을 생성한다. nc 함수가 자동화하는 분할을 수동으로도 실행할 수 있다.

또 하나 중요한 결정은 vim 과 tmux 의 패인 이동을 같은 키로 통합한 것이다. C-h/j/k/l 을 prefix 없이 누르면, nvim 안에서는 nvim 의 좌측 창으로, 셸 패인에서는 tmux 의 좌측 패인으로 이동한다. 현재 pane 의 프로세스가 vim 계열인지를 tmux 쪽에서 검사해 자동으로 분기한다. 도구 경계가 사라진다. 손가락이 prefix 키를 거치지 않고 세 패인 사이를 이동할 수 있다.

is_vim="ps -o state= -o comm= -t '#{pane_tty}' \
    | grep -iqE '^[^TXZ ]+ +(\\S+\\/)?g?(view|n?vim?x?)(diff)?$'"
bind-key -n 'C-h' if-shell "$is_vim" 'send-keys C-h' 'select-pane -L'
bind-key -n 'C-j' if-shell "$is_vim" 'send-keys C-j' 'select-pane -D'
bind-key -n 'C-k' if-shell "$is_vim" 'send-keys C-k' 'select-pane -U'
bind-key -n 'C-l' if-shell "$is_vim" 'send-keys C-l' 'select-pane -R'

is_vim 이 패인의 프로세스를 검사한다. vim 계열이면 키 입력을 nvim 에 전달하고, 아니면 tmux pane 이동을 실행한다.

zsh

zsh는 Bash 호환 셸로, 강력한 자동완성과 확장된 글로빙, 플러그인 생태계가 특징이다. macOS Catalina 이후 기본 셸이기도 하다.

zsh 는 두 가지로 구성된다. PATH/환경 을 잡는 .zshrc함수/alias 를 모아둔 aliases.zsh. ZDOTDIR 로 ~/.config/zsh 를 가리킨 뒤, .zshrc 가 그 디렉토리의 *.zsh 를 모두 source 한다.

ZDOTDIR=$HOME/.config/zsh

for _zsh_conf in $ZDOTDIR/*.zsh(N); do
  source "$_zsh_conf"
done

이 패턴 덕분에 alias / 함수 / 플러그인 설정을 파일로 분리해서 추가할 수 있다. 새 함수가 생기면 새 .zsh 파일을 만들고 끝이다. .zshrc 자체는 거의 안 건드린다.

nc 함수의 정의는 다음과 같다.

function nc() {
  if [[ -z "$TMUX" ]]; then
    echo "Not inside a tmux session. Run from within tmux."
    return 1
  fi

  local target="${1:-$PWD}"
  local dir
  if [[ -d "$target" ]]; then
    dir="$(realpath "$target")"
  else
    dir="$(realpath "$(dirname "$target")")"
  fi

  local nvim_pane
  nvim_pane="$(tmux display-message -p '#{pane_id}')"

  tmux split-window -h -c "$dir" -l 30% "claude; exec $SHELL"

  tmux select-pane -L
  tmux split-window -v -c "$dir" -l 15%

  tmux select-pane -t "$nvim_pane"
  nvim "$@"
}

함수를 단계별로 살펴보자.

첫 번째 가드는 tmux 안인지 확인한다. tmux 밖에서 nc 를 호출하면 분할 자체가 의미가 없으므로 즉시 종료한다. 메시지를 한 줄 출력하고 1 을 반환한다.

다음은 작업 디렉토리 결정이다. 인자가 없으면 $PWD, 디렉토리면 그대로, 파일이면 그 부모 디렉토리를 사용한다. realpath 로 절대 경로화한다. 이 dir 이 세 패인 모두의 cwd 로 지정된다. nvim 으로 파일을 열고 옆 패인에서 git status 를 입력하면 같은 repo 가 보인다.

다음은 현재 패인의 ID 저장이다. 분할이 끝난 뒤 nvim 으로 포커스를 되돌리려면 원래 패인의 id 가 필요한데, 분할 도중에 id 가 바뀔 수 있으므로 미리 저장한다.

이후 분할을 두 번 수행한다.

첫 번째 분할은 tmux split-window -h -c "$dir" -l 30% "claude; exec $SHELL" 로, 우측에 30% 폭의 패인을 만들고 claude 를 실행한다. exec $SHELL 을 붙이면 claude 종료 후 패인이 즉시 사라지지 않고 셸로 전환된다.

두 번째 분할은 tmux select-pane -L 으로 좌측 (원래 nvim 자리) 으로 돌아간 뒤, tmux split-window -v -c "$dir" -l 15% 로 위/아래로 나눈다. 아래 15% 가 작은 터미널 패인이 된다.

마지막으로 nvim 을 실행한다. 저장해둔 nvim_pane 으로 포커스를 이동한 뒤 (이제 좌상 패인이다) nvim "$@" 으로 에디터를 연다. 인자가 파일이면 해당 파일이 열리고, 디렉토리면 LazyVim 의 대시보드가 표시된다.

결과적으로 아래와 같은 배치가 nc 한 번으로 구성된다.

┌───────────────────────────┬──────────────┐
│                           │              │
│         nvim              │   claude     │
│       (LazyVim)           │   (30%)      │
│                           │              │
├───────────────────────────┤              │
│   shell (15%)             │              │
└───────────────────────────┴──────────────┘

이 함수를 호출하는 alias 가 몇 개 있다.

alias zrc="nc ~/.config/zsh/"
alias nvimrc="nc ~/.config/nvim/"
alias alc="nc ~/.config/alacritty/"
alias tlc="nc ~/.tmux.conf"

zrc 한 번이면 zsh 설정 디렉토리를 nvim 으로 열면서 옆에 Claude Code 가 떠 있는 상태가 된다. 설정 파일을 고치다가 바로 옆 pane 에서 검토를 받을 수 있다.

다른 alias 도 몇 가지 있다. f 는 fzf 로 파일을 골라 nvim 으로 여는 단축키, g 는 lazygit, ?? 는 fabric-ai, ? 는 w3m 검색이다. 그 중 nc 가 이 글의 중심인 이유는, 함수 한 줄이 다섯 도구의 자리를 동시에 정의하기 때문이다.

nvim

Neovim은 Vim 의 리팩토링 포크다. 비동기 플러그인, 내장 LSP 클라이언트, Lua 기반 설정이 추가되었다. LazyVim은 그 위에 합리적 기본값과 모듈식 extras 시스템을 제공하는 설정 프레임워크다.

에디터는 LazyVim 베이스의 nvim 이다. 처음부터 init.lua 를 짜는 대신 LazyVim 의 합리적 기본값을 받는 쪽을 골랐다. LSP, treesitter, finder, mason 기반 LSP 설치가 이미 묶여 있어서 직접 짜면 며칠이 걸린다. 그리고 언어별 extras 가 lazyvim.json 의 줄 단위로 켜고 꺼지기 때문에, 새 언어가 필요해지면 한 줄 추가 + :Lazy sync 한 번으로 LSP / treesitter / 포매터까지 한꺼번에 설치된다. 현재 14개 언어와 코딩/에디터/포매팅/테스트 extras 를 포함해 32개가 활성화되어 있다.

커스터마이징은 두 디렉토리로 나뉜다. lua/config/* 는 LazyVim 의 기본값을 재정의하는 위치 (keymap, option, autocmd), lua/plugins/* 는 새 플러그인이나 extras 의 추가 옵션을 배치하는 위치다. 언어별 설정은 plugins/language/<lang>.lua 한 파일로 분리했다. go 를 더 쓰지 않게 되면 그 파일만 지우면 된다.

lua/plugins/language/
├── go.lua
├── html.lua
├── java.lua
├── markdown.lua
└── typescript.lua

nvim 자체에 대해서는 더 깊이 들어가지 않는다. 키맵, LSP 설정, 디버거 통합, snacks.nvim picker, harpoon2 워크플로우 등 각각이 별도 글 분량이다. 이 글의 범위는 “LazyVim 베이스에 모듈식으로 구성하는 패턴” 까지다.

Claude Code

우측 30% 패인의 위치는 Claude Code 가 채운다. brew cask 한 줄 (cask "claude-code") 로 설치되고, nc 함수가 그 자리를 구성한다. 화면 안에서의 역할은 단순하다. 좌측에서 코드를 편집하는 동안 우측에서 같은 디렉토리의 컨텍스트로 함께 작업하는 페어다.

dotfiles 의 claude/ 모듈은 이보다 더 많은 것을 포함한다. ~/.claude/ 아래에 settings, agents, hooks, rules, skills 가 stow 되며, 이 파일들이 Claude Code 의 동작을 세부적으로 조정한다. agents 는 작업 단위 위임, hooks 는 파일 저장 시점의 자동화, skills 는 재사용 가능한 워크플로우, rules 는 언어별/공통 코드 컨벤션을 담당한다.

다만 이번 글의 범위는 “다섯 도구가 한 화면에 모이는 패턴” 까지다. Claude Code 의 각 컴포넌트 (settings, agents, hooks, skills, MCP, output styles) 는 별도 글로 정리할 예정이다.

이 글에서 정리할 한 가지는 단순하다. Claude Code 는 nc 함수가 만든 우 30% 패인 안에서 실행된다. 그 이상도 그 이하도 아니다. 배치가 도구를 호출하고, 도구는 배치 위에서 동작한다.

한계와 트레이드오프

이 셋업이 안 맞는 경우가 몇 있다.

GUI 디버거에 의존하는 작업. 브라우저 devtools, 대형 IDE 의 시각 디버거가 일상 도구라면 터미널 중심 배치는 두 도구 사이를 자주 왕복하게 만든다. 이 구성은 코드 편집 + 셸 + AI 페어가 99% 를 차지한다는 가정 위에 서 있다.

협업 스크린쉐어. 동료에게 화면을 보여줄 때 nvim 키바인딩의 의도가 전달되지 않는 경우가 자주 있다. dd 가 한 줄을 삭제하는 모습을 처음 보는 사람은 낯설어한다. 페어 프로그래밍이 잦다면 GUI 에디터의 사회적 비용이 더 적다.

Linux 셋업과의 차이. 같은 dotfiles 가 Linux 에서도 동작하지만, 이 글이 다루지 않은 부분 (Hyprland 윈도우 매니저, Kime 입력기, Linux 전용 패키지) 은 별개다. macOS 에서는 alacritty 가 터미널 에뮬레이터 역할을 맡지만, Linux 에서는 Hyprland 윈도우 매니저가 그 역할을 일부 담당한다.

빠진 조각도 솔직히 짚는다. 입력기 (kime), 윈도우 매니저 (hypr), 키매핑 (karabiner) 은 이 글의 범위 밖이라 넣지 않았다. 한국어 개발자에게는 입력기 결정이 결국 영향을 주지만, 이건 분리된 글이 더 적합하다고 판단했다.

부트스트랩

부트스트랩은 두 단계로 나뉜다. OS 별 패키지 설치stow 기반 dotfiles 심링크.

진입점은 setup.sh 다. 이 스크립트는 uname -s 로 OS 를 분기한 다음 setup-macos.sh 또는 setup-linux.sh 를 source 한다. macOS 라면 다음 순서로 일이 일어난다.

  1. Xcode Command Line Tools — 없으면 설치, 있으면 통과.
  2. Homebrew — 없으면 공식 스크립트로 설치, 있으면 통과.
  3. brew bundleBrewfile 에 적힌 패키지를 일괄 설치한다. alacritty, tmux, neovim, zsh 플러그인 매니저 (zinit), fzf/fd/ripgrep/zoxide 같은 검색 도구, lazygit, gh, claude-code 가 함께 설치된다.
  4. alacritty defaults write — macOS 의 Cmd+H 가 AppKit 메뉴에 가로채지지 않도록 NSUserKeyEquivalents 를 비운다. 그래야 alacritty 가 Cmd+H 를 받아서 vim 의 M-h 로 변환할 수 있다.

OS 별 설치가 끝나면 setup.sh 본체로 돌아온다. 거기서 GNU stow 가 모듈별로 dotfiles 를 심링크한다.

COMMON_MODULES=(claude git lazygit nvim tmux obsidian zsh)
for m in $COMMON_MODULES; do
  stow --restow "$m"
done

각 모듈 디렉토리는 <module>/.config/<tool>/... 형태다. 예를 들어 nvim/.config/nvim/init.lua 는 stow 가 ~/.config/nvim/init.lua 로 심링크한다. XDG_CONFIG_HOME 규약을 그대로 따르므로 모듈 추가/제거가 디렉토리 단위로 깔끔하다.

macOS 에서는 여기에 alacritty, karabiner 가 추가로 stow 되고, 마지막으로 second-brain Obsidian vault 를 clone 해 obsidian-cli 의 기본 vault 로 등록한다. 이 두 단계가 끝나면 exec zsh 한 번으로 모든 설정이 적용된다.

마무리

시작 지점으로 돌아가면 nc 한 번에 셋으로 분할된 화면이 있고, 그 안에 다섯 도구가 각자의 역할을 맡는다. 그 한 번 뒤에는 bash setup.sh 한 줄로 재현되는 dotfiles 가 있고, dotfiles 는 alacritty / tmux / zsh / nvim / Claude Code 를 어떤 역할로 배치할지에 대한 결정의 묶음이다.

저장소는 다음과 같다.

git clone https://github.com/byunghak/.dotfiles.git ~/.dotfiles
cd ~/.dotfiles && bash setup.sh

같은 환경을 새 머신에 옮기는 데 두 줄이면 충분하다.

다음 글들에서는 Claude Code 의 세부 (settings, agents, hooks, skills, MCP) 를 하나씩 다룬다. 이 글이 layout 의 역할 배치에 대한 글이라면, 다음 글들은 그 역할 안에서 실행되는 도구의 결정을 다루는 글이 된다.