느린 터미널에 쓰기엔 인생은 너무 짧다

3 days ago 4
  • 터미널 중심 작업에서는 새 탭, 입력, 자동완성 지연이 하루에 수백 번 누적되므로, 약 30ms에 시작되는 대화형 셸 같은 즉각적 반응성이 핵심 기준임
  • 가장 큰 개선은 oh-my-zsh, prezto, 플러그인 매니저를 빼고 실제로 쓰는 세 플러그인만 .zshrc에서 직접 source하는 최소 구성
  • 비용이 큰 compinit, nvm, kubectl 자동완성은 각각 24시간 캐시와 지연 로딩으로 셸 시작 시점의 작업을 줄이는 대상임
  • 동기식 git status 프롬프트는 Enter마다 지연을 만들 수 있어, pure처럼 즉시 렌더링하고 Git 정보를 비동기로 채우는 비동기 방식이 핵심임
  • 성능 점검은 time, hyperfine, zprof, 실행 추적으로 시작 시간·프롬프트 지연·입력 지연을 확인하고, 자주 쓰는 것만 남기는 의도적 선택이 결론임

터미널을 빠르게 유지해야 하는 이유

  • 대부분의 작업이 Git, kubectl, tmux, 서버 SSH처럼 터미널 안에서 하루 종일 이루어지는 환경임
  • 새 탭 열기, 문자 입력, Tab 자동완성의 지연은 하루에 수백 번 체감되는 누적 비용이며, “천 번의 작은 상처로 인한 죽음”에 가까운 문제임
  • 셸 시작 시간은 약 30ms 수준이며, 자동완성, 구문 강조, 자동 제안, fzf, direnv를 갖춘 대화형 셸 기준임
$ for i in {1..5}; do /usr/bin/time zsh -i -c exit; done 0.03 real 0.02 user 0.01 sys 0.03 real 0.02 user 0.01 sys ...
  • 30fps의 단일 프레임보다 짧은 시간에 완전한 대화형 셸이 로드되며, 새 탭도 즉시 열리는 구성임
  • 큰 최적화 프로젝트가 아니라, 셸을 작고 빠르게 유지해 온 습관의 결과이며 관련 설정은 dotfiles 저장소에 존재

프레임워크 없음

  • 가장 큰 개선점은 oh-my-zsh, prezto, 플러그인 매니저를 쓰지 않는 구성임
  • oh-my-zsh의 수백 개 플러그인과 테마 중 약 5%만 쓰면서 나머지 95% 비용을 셸을 열 때마다 시간과 컴퓨팅 자원으로 치르는 구조를 피하는 방식임
  • 플러그인 매니저도 시작 시점에 자체 오버헤드를 추가하는 요소임
  • 실제 사용 플러그인은 세 개이며, 설치 스크립트가 한 번 git clone한 뒤 .zshrc에서 직접 source하는 구성임
source ~/.zsh/fzf-tab/fzf-tab.plugin.zsh source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh source ~/.zsh/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
  • 시작 시점에 의존성 해결을 수행하는 플러그인 매니저가 없고, 이미 디스크에 있는 파일을 source하는 작업은 사실상 비용이 거의 없는 방식임

자동완성 캐싱

  • compinit은 일반적인 .zshrc에서 비용이 큰 작업 중 하나이며, 기본 동작은 셸을 열 때마다 모든 자동완성 파일에 보안 감사를 수행하는 방식임
  • 해결책은 캐시 파일인 .zcompdump가 24시간보다 오래됐을 때만 전체 실행을 하고, 그 외에는 -C로 검사를 건너뛰는 구성임
autoload -Uz compinit if [[ -n ~/.zcompdump(#qNmh-24) ]]; then compinit -C else compinit fi
  • 글롭 한정자 #qNmh-24는 “존재하며 최근 24시간 안에 수정됨”이라는 의미임
  • 전체 compinit은 하루 한 번만 실행하고, 나머지 시간에는 캐시 읽기를 사용하는 방식임

지연 로딩

  • nvm은 셸 시작을 느리게 만드는 대표적인 요소이며, 시작 시점에 즉시 source하면 쉽게 0.5초를 추가할 수 있는 대상임
  • 모든 셸에서 nvm이 필요한 것이 아니라 nvm을 입력할 때 필요하므로, 첫 사용 시 자기 자신을 실제 함수로 교체하는 래퍼 함수 사용임
export NVM_DIR="$HOME/.nvm" nvm() { unset -f nvm [ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh" --no-use [ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \. "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" nvm "$@" }
  • 첫 nvm 호출은 스텁 함수를 삭제하고, --no-use로 실제 스크립트를 로드해 Node 버전 해석도 피한 뒤 인자를 전달하는 흐름임
  • kubectl 자동완성도 kubectl 바이너리를 실행해 완성 스크립트를 생성하므로, 실제로 처음 kubectl을 실행한 뒤에만 로드하는 방식임
kubectl() { command kubectl "$@" local ret=$? if [[ -z $KUBECTL_COMPLETE ]]; then source <(command kubectl completion zsh) KUBECTL_COMPLETE=1 fi return $ret }
  • .zshrc에 eval "$(tool init zsh)"를 넣으라고 하는 도구는 지연 로딩 후보이며, 이런 구성은 시작 시점에 프로세스를 포크하고 출력을 평가하기 때문임
  • direnv와 fzf는 빠르고 계속 쓰기 때문에 즉시 로드하는 구성임
  • 실제로 자주 쓰는 것에 엄격해야 하는 접근임

블로킹 없는 프롬프트

  • 프롬프트가 git status를 동기식으로 실행하면 어느 정도 큰 저장소에서 지연이 발생하는 구조임
  • 이런 지연은 Enter를 누를 때마다 느껴지므로 느린 시작보다 더 나쁜 문제가 될 수 있음
  • pure는 프롬프트를 즉시 렌더링하고 Git 정보는 준비되는 대로 비동기로 채우는 방식임
  • zsh 내장 vcs_info로 교체를 시도했지만, pure의 비동기 동작이 해당 사용 사례에 더 나은 구성임
  • 자체 프롬프트에서도 비동기 git status 구현이 가능하지만, pure가 해당 사용 사례에 알맞게 감싼 형태임

터미널 자체

  • 셸 시작 시간은 전체 이야기의 절반이며, 터미널 에뮬레이터도 입력 지연을 추가하는 요소임
  • Ghostty는 GPU 가속과 네이티브 구성을 갖춘 터미널이며, 설정 파일은 일곱 줄 규모임
  • tmux new -A -s main 별칭인 t와 함께 쓰면 새 터미널 창이 기존 세션으로 바로 돌아가는 구성임

셸 성능 직접 측정

  • 살펴볼 지연은 시작 시간, 프롬프트 지연, 입력 지연의 세 종류임
  • 첫 실행은 콜드 캐시 때문에 항상 더 느리므로, 다음 명령을 여러 번 실행하는 방식임
time zsh -i -c exit
  • 시작 시간이 100ms 미만이면 괜찮고, 50ms 미만이면 훌륭한 수준이며, 500ms 이상이면 개선할 작업이 있는 상태임
  • 제대로 된 통계를 보려면 hyperfine 사용임
hyperfine --warmup 3 'zsh -i -c exit'
  • Zsh에는 프로파일러가 포함돼 있으며, .zshrc 맨 위에 다음 줄을 추가하는 방식임
zmodload zsh/zprof
  • .zshrc 맨 아래에는 다음 줄을 추가하는 방식임
zprof
  • 새 셸을 열면 시간이 어디에 쓰였는지 정렬된 표를 확인할 수 있음
  • 상위 항목은 보통 compinit, nvm.sh 로드, eval "$(...)" 중 하나임
  • 가장 위 항목을 고치고 다시 실행하는 반복 방식이며, 작업이 끝나면 두 줄 삭제 필요
  • zprof가 충분히 세밀하지 않으면 타임스탬프로 전체 시작 과정을 추적하는 방식임
zsh -ixc exit 2>&1 | ts -i '%.s' | sort -rn | head -20
  • set -x는 명령을 실행하기 전에 각 추적 줄을 출력하므로, ts -i는 명령의 실행 시간을 그 다음 줄에 붙이는 구조임
  • 원래 추적에서 큰 숫자 바로 위에 있는 명령이 느린 명령임
  • 다른 방법은 PS4='+%D{%s.%6.}: '를 설정하고 zsh -ixc exit 2> startup.log를 실행한 뒤 줄 사이의 큰 점프를 찾는 방식임
  • 시작은 빠르지만 프롬프트 다시 그리기가 느릴 수도 있으므로, 가장 큰 Git 저장소로 cd한 뒤 Enter를 눌러 다음 프롬프트가 나타나기 전 지연을 확인하는 방식임
  • 지연이 있으면 프롬프트가 동기식 작업을 수행해 느려지는 상태이며, 비동기 프롬프트로 바꾸거나 Git 기능을 제거하는 선택지 존재

마무리

  • 대부분의 최적화는 무엇을 추가하느냐가 아니라 무엇을 빼느냐의 문제임
  • 실제로 사용할 것만 의도적으로 추가하는 접근이 핵심임
  • 하루에 여는 수십 개 세션이 모두 즉시 열리는 상태이며, 터미널은 기다려야 하는 애플리케이션이 아니라 머리의 확장처럼 느껴지는 도구가 되는 구성임
  • 하루 종일 쓰는 도구에서는 이런 반응성이 타협 불가 조건임
  • 관련 설정은 dotfiles 저장소에 존재
Read Entire Article