느린 터미널에 쓰기엔 인생은 너무 짧다
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 저장소에 공개되어 있음
-
Homepage
-
Tech blog
- 느린 터미널에 쓰기엔 인생은 너무 짧다