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

6 days ago 9
  • 하루 종일 사용하는 터미널의 속도는 작업 효율을 좌우하며, 새 탭 열기·타이핑·자동완성의 미세한 지연이 하루에 수백 번 누적되면 비효율적
  • 완전히 로드된 인터랙티브 셸이 자동완성·구문 강조·자동제안·fzf·direnv를 포함하고도 약 30밀리초만에 시작하고, 새 탭은 즉각적으로 열리도록 개선
  • 가장 큰 비결은 oh-my-zsh나 prezto 같은 프레임워크와 플러그인 매니저를 쓰지 않는 것으로, 플러그인 3개만 직접 git clone해 .zshrc에서 source함
  • compinit 캐싱, 지연 로딩(lazy-loading), 비동기 프롬프트, GPU 가속 터미널 등으로 시작·프롬프트·입력 지연을 모두 최소화함
  • 대부분의 최적화는 무언가를 추가하는 것이 아니라 불필요한 것을 덜어내는 것이며, 실제로 자주 쓰는 것만 의도적으로 추가하는 절제가 핵심임

빠른 터미널이 필요한 이유

  • 거의 모든 작업이 터미널 안에서 이루어지며, Git·kubectl·tmux·서버 ssh 접속 등을 하루 종일 사용함
  • 그만큼 자주 쓰는 도구는 빨라야 하며, 새 탭 열기·문자 입력·탭 자동완성에서의 지연을 하루에 수백 번 체감함
  • 이런 미세한 지연이 누적되는 상황은 천 번의 칼질로 인한 죽음(death by a thousand cuts) 과 같음

셸 시작 속도 측정 결과

  • 업데이트 결과, 셸이 약 30밀리초만에 시작하며, 측정 명령으로 for i in {1..5}; do /usr/bin/time zsh -i -c exit; done 사용
  • 자동완성, 구문 강조, 자동제안, fzf, direnv가 모두 포함된 완전한 인터랙티브 셸이 30fps 단일 프레임보다 짧은 시간에 로드됨
  • 큰 최적화 프로젝트가 있었던 것이 아니라, 수년간 셸을 최소화하고 빠르게 유지한 습관의 결과임
  • 모든 설정은 dotfiles 저장소에 공개되어 있음

프레임워크 없음

  • 가장 큰 이점은 존재하지 않는 것에서 나오며, oh-my-zsh·prezto·플러그인 매니저를 쓰지 않음
  • oh-my-zsh의 수백 개 플러그인과 테마 중 약 5%만 사용하면서, 나머지 95%에 대한 시간과 컴퓨팅 자원 비용을 셸을 열 때마다 지불하게 됨
  • 플러그인 매니저는 그 위에 추가 오버헤드를 더함
  • 정확히 3개의 플러그인만 사용하며, 설치 스크립트가 한 번 git clone한 뒤 .zshrc에서 source함
    • fzf-tab, zsh-autosuggestions, zsh-syntax-highlighting
    • 시작 시 의존성 해석을 수행하는 플러그인 매니저가 없으며, 이미 디스크에 있는 파일을 source하는 것은 사실상 비용이 없음

자동완성 캐싱

  • compinit은 일반적인 .zshrc에서 가장 비용이 큰 작업 중 하나로, 기본적으로 셸을 열 때마다 모든 자동완성 파일에 대한 보안 감사를 수행함
  • 해결책은 캐시(.zcompdump)가 24시간보다 오래된 경우에만 전체 실행을 하고, 그 외에는 -C로 검사를 건너뛰는 것
    • glob 한정자 #qNmh-24는 "존재하며 최근 24시간 이내에 수정됨"을 의미함
    • 하루에 한 번만 전체 compinit을 실행하고, 나머지 시간에는 캐시된 읽기를 사용함

지연 로딩 (Lazy-loading)

  • nvm은 가장 악명 높은 셸 시작 속도 저하 원인으로, 시작시점에 즉시 source하면 0.5초가 쉽게 추가될 수 있음
  • 모든 셸에서 nvm이 필요한 것이 아니라 nvm을 입력할 때만 필요하므로, 첫 사용 시 자기 자신을 대체하는 함수로 감쌈
    • 첫 nvm 호출이 스텁을 삭제하고 실제 nvm을 source한 뒤(--no-use로 node 버전 해석도 막음) 인자를 전달함
  • kubectl 자동완성도 같은 방식으로, kubectl 바이너리를 호출해 자동완성 스크립트를 생성하므로 실제로 처음 실행한 뒤에만 로드함
  • eval "$(tool init zsh)"를 .zshrc에 넣으라고 안내하는 모든 도구는 시작 시 프로세스를 fork하고 출력을 평가하므로 지연 로딩 후보가 됨
  • direnv와 fzf는 빠르고 자주 사용하므로 즉시 로딩 상태로 유지하며, 실제로 자주 쓰는 것이 무엇인지 엄격하게 판단해야 함

논블로킹 프롬프트

  • git status를 동기적으로 실행하는 프롬프트는 어느 정도 큰 저장소에서 지연이 생기며, 이는 Enter를 누를 때마다 체감되어 느린 시작보다 더 나쁠 수 있음
  • pure를 사용하며, 프롬프트를 즉시 렌더링하고 git 정보는 준비되면 비동기적으로 채움
  • zsh 내장 vcs_info로 교체를 잠깐 시도했으나, pure의 비동기 동작이 더 나았음
  • 직접 프롬프트에서 비동기 git status를 구현할 수도 있으나, pure가 해당 용도에 맞게 잘 감싸 줌

터미널 에뮬레이터 자체

  • 셸 시작은 절반의 이야기일 뿐이며, 에뮬레이터 자체가 입력 지연을 추가함
  • GPU 가속 네이티브 터미널인 Ghostty를 사용하며, 설정은 단 7줄임
  • 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, 맨 아래에 zprof를 넣으면 시간이 어디에 쓰였는지 정렬된 표를 출력함
    • 상위 항목은 보통 compinit, nvm.sh source, eval "$(...)"이며, 가장 위 항목부터 고치고 재실행을 반복함
    • 완료 후 두 줄을 제거함
  • zprof로 부족하면 타임스탬프로 전체 시작을 추적: zsh -ixc exit 2>&1 | ts -i '%.s' | sort -rn | head -20
    • 또는 PS4='+%D{%s.%6.}: '를 설정하고 zsh -ixc exit 2> startup.log 실행 후 줄 간 큰 점프를 확인함
  • 시작은 빠른데 프롬프트 redraw가 느릴 수 있으며, 가장 큰 git 저장소로 cd한 뒤 Enter를 눌러 다음 프롬프트가 나타나기 전 지연이 있으면 프롬프트가 동기 작업을 하는 것임
    • 비동기 프롬프트로 전환하거나 Git 기능을 제거하는 선택지가 있음

마무리

  • 대부분의 최적화는 무언가를 덜어내는 것에 관한 것으로, 의도적으로 행동하고 실제로 사용할 것만 추가하는 것이 핵심
  • 이렇게 하면 하루에 여는 수십 개의 세션이 모두 즉시 열리고, 터미널은 기다려야 하는 애플리케이션이 아니라 머리의 확장처럼 느껴지는 도구가 됨
  • 하루 종일 사용하는 도구에 대해 이 속도는 타협 불가능함
  • 위의 모든 설정은 dotfiles 저장소에 공개되어 있음
Read Entire Article