본문으로 건너뛰기

"git" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

모든 태그 보기

agnoster와 Starship

· 약 7분

어느 날 Enter가 느려졌다

터미널에서 Enter를 눌렀는데 다음 프롬프트가 뜰 때까지 300ms 넘게 비는 느낌이 왔어요. 큰 레포일 때는 그러려니 했지만 작은 디렉토리에서도 똑같이 굼떴고, "디렉토리 크기와 무관하게 느리다" — 이 사실이 첫 번째 단서였어요.

이 글은 그 지연의 정체를 찾아가서, 얕은 수정으로 안 됐고, 결국 프롬프트 테마를 갈아엎고 Starship으로 넘어온 여정의 기록입니다.

첫 시도: status를 빠르게 한다

처음에는 단순하게 접근했습니다. "Enter 뒤 지연 = git status가 느림"이라고 가정하고, 속도를 높이는 옵션부터 모두 켰습니다.

# ~/.zshrc
DISABLE_UNTRACKED_FILES_DIRTY="true"
# git 전역
git config --global core.fsmonitor true
git config --global core.untrackedCache true
git config --global feature.manyFiles true

fsmonitor는 파일 시스템 이벤트를 상주 데몬이 추적해 git status의 내부 스캔 비용을 사실상 0에 가깝게 만듭니다. 실제로 git status --porcelain 자체의 시간은 크게 짧아졌습니다.

레포.git 크기fsmonitor 적용 후 status
small-repo작음즉시
mid-repo-A30 MB0.025 s
mid-repo-B51 MB0.041 s
big-repo246 MB0.042 s

그런데 Enter 지연은 거의 그대로였습니다. git status가 10배 빨라졌는데도 체감이 달라지지 않았으니, 제가 엉뚱한 부분을 최적화했다는 뜻이었습니다.

측정을 다시 할 때 만난 세 가지 함정

문제를 다시 풀어헤치려면 먼저 제대로 측정해야 했습니다. 여기서 쉽게 빠지는 함정이 세 개 있었습니다.

함정 1. zsh -i -c exit는 per-Enter 지연이 아니다

흔히 쓰는 "쉘 벤치마크"인 time zsh -i -c exit는 전혀 다른 대상을 측정합니다. 쉘이 시작할 때 한 번만 드는 비용입니다. compinit, plugin 로딩 같은 일회성 작업을 포함합니다. 매번 Enter를 칠 때 드는 비용과는 다른 층이라, 이 값을 아무리 줄여도 프롬프트 재평가 비용은 그대로입니다.

함정 2. precmd를 직접 호출해도 안 된다

일반적인 zsh 테마는 precmd_functions에 훅을 걸어 매 프롬프트 직전에 준비 작업을 합니다. 그러나 agnoster는 다릅니다.

PROMPT='%{%f%b%k%}$(build_prompt) '

PROMPT 변수 안에 명령 치환 $(build_prompt)를 끼워 둔 구조라, 프롬프트 문자열이 expansion될 때 비로소 git 호출이 일어납니다. 그래서 precmd를 수동으로 실행해 재면 거의 0ms가 찍힙니다. 실제 Enter 경로와 다른 곳에서 측정하는 셈입니다.

함정 3. 반복마다 새 zsh를 띄우면 측정이 오염된다

# 나쁜 예
for i in {1..10}; do
time zsh -ic 'print -P "$PROMPT" >/dev/null'
done

이렇게 측정하면 매 반복이 zsh 시작 비용(~200ms)에 묻힙니다. 같은 zsh 프로세스 안에서 루프를 돌려야 순수 프롬프트 렌더 비용만 남습니다.

올바른 one-liner

결국 큰 레포로 cd 한 상태에서 다음과 같이 측정해야 했습니다.

zsh -ic '
print -P "$PROMPT" >/dev/null # 웜업
{ time (for i in {1..10}; do print -P "$PROMPT" >/dev/null; done) } 2>&1
'

print -P "$PROMPT"는 PROMPT 문자열을 강제로 expand시키기 때문에 내부의 $(build_prompt)가 실제로 실행되고, 진짜 비용이 드러납니다.

진짜 범인: agnoster의 7× git spawn

측정을 제대로 돌리자 범인이 또렷이 보였습니다. ~/.oh-my-zsh/themes/agnoster.zsh-themeprompt_git()은 매 프롬프트 렌더마다 git 서브프로세스를 일곱 번 띄웁니다.

1. git config --get oh-my-zsh.hide-status
2. git rev-parse --is-inside-work-tree
3. git rev-parse --git-dir
4. git status --porcelain (parse_git_dirty)
5. git symbolic-ref HEAD
6. git log --oneline @{upstream}.. (ahead)
7. git log --oneline ..@{upstream} (behind)

그리고 macOS의 기본 git 바이너리는 Apple wrapper입니다.

/Library/Developer/CommandLineTools/usr/libexec/git-core/git

호출 한 번당 fork → exec → wrapper → core 체인에 고정적으로 40~50ms가 듭니다. 이 오버헤드는 git이 아무리 빨라도 줄어들지 않습니다.

산수가 맞아떨어집니다.

7 × 50ms ≈ 350ms

그래서 .git이 수십 MB든 수백 MB든 바닥값은 비슷하게 나옵니다. 이게 Enter 지연의 정체였습니다.

측정 결과 (10회 평균)

상태big-repo (246MB .git)small-repo
agnoster 원본 (prompt_git 포함)~427 ms~335 ms
prompt_git를 빈 함수로 대체~14.7 ms~13.6 ms

프롬프트 지연의 95% 이상이 prompt_git의 spawn 오버헤드였습니다. git status 자체는 fsmonitor 덕분에 이미 충분히 빨랐습니다. 제가 status 내부를 최적화하느라 헛돈 까닭은 줄일 수 있는 구간만 보고 줄이려 했기 때문입니다. spawn 횟수는 그대로 7이었으니 체감이 달라지지 않은 게 당연했습니다.

세 가지 선택지

구조적 한계가 확인되자 길이 명확해졌습니다.

A. agnoster의 git 세그먼트를 끈다

git config --global oh-my-zsh.hide-status 1

혹은 zshrc 마지막에:

prompt_git() { : }

프롬프트 렌더 비용이 15ms로 떨어집니다. 대신 브랜치/dirty 표시가 사라집니다. 저는 브랜치를 프롬프트에서 보는 편이 좋아서 이 선택지는 제외했습니다.

B. powerlevel10k로 간다

gitstatusd라는 상주 데몬이 변경을 감시하며 결과를 파이프로 돌려줍니다. git 바이너리를 띄우지 않으므로 spawn은 0회입니다. instant prompt를 사용해 초기 표시도 빠릅니다.

괜찮은 옵션이지만, 설정을 직접 다듬기엔 .p10k.zsh 포맷이 다소 난잡해 보였습니다.

C. Starship으로 간다

Rust로 작성한 단일 바이너리입니다. zsh 프롬프트 훅에서 starship 바이너리를 한 번만 fork하고, 그 안에서 libgit2를 라이브러리로 링크해 호출합니다. 외부 git 프로세스는 띄우지 않습니다. 즉 spawn 1회로 끝납니다.

  • 설정은 ~/.config/starship.toml 한 파일 (TOML)
  • git diff로 리뷰/롤백이 쉬움
  • 프리셋 갤러리가 존재해 시작점 제공

풍부한 정보와 속도, 설정 가독성을 모두 고려해 Starship으로 결정했습니다.

Starship 전환 작업

설치

brew install starship
# 1.25.0

~/.zshrc 두 군데 수정

agnoster를 끄고:

- ZSH_THEME="agnoster"
+ # ZSH_THEME="agnoster" # 비활성, Starship 사용
+ ZSH_THEME=""

파일 끝에 초기화 추가:

# Starship prompt. agnoster 대신. 없으면 graceful skip.
if command -v starship >/dev/null 2>&1; then
eval "$(starship init zsh)"
fi

바이너리가 없으면 조용히 건너뛰도록 command -v 체크를 넣었습니다. 새 머신 셋업할 때 zshrc를 그대로 복사해도 안전합니다.

프리셋 적용

starship preset pastel-powerline -o ~/.config/starship.toml

pastel-powerline의 정보 밀도가 가장 괜찮아 선택했습니다. 이대로 써도 되지만 두 가지가 걸렸습니다.

  1. 한 줄 프롬프트라 긴 디렉토리/브랜치명이 나오면 명령 입력 공간이 좁아집니다.
  2. 다크 터미널에서 일부 세그먼트의 글자가 묻힙니다.

튜닝 1: 두 줄 프롬프트

format 문자열 끝(마지막 """ 직전)에 줄바꿈과 character 모듈을 추가합니다.

$line_break\
$character

파일 끝에는 [character] 섹션을 추가합니다.

[character]
success_symbol = "[❯](bold #FCA17D)"
error_symbol = "[❯](bold red)"
vimcmd_symbol = "[❮](bold green)"

이제 첫 줄에는 정보(유저/경로/git/언어/시간)를 표시하고, 두 번째 줄에는 하나만 둡니다. 명령 입력이 세그먼트에 영향받지 않습니다.

튜닝 2: 다크 터미널 가독성

기본 pastel-powerline은 각 세그먼트 배경색만 지정하고 글자색은 터미널 기본을 씁니다. 어두운 터미널(제 환경은 거의 검정 배경)에서는 짙은 배경 세그먼트 안의 글자가 잘 보이지 않았습니다.

각 세그먼트에 명시적 글자색과 bold를 지정했습니다.

세그먼트배경글자색
username#9A348E (보라)#ffffff bold
directory#DA627D (분홍)#1a1a2e bold
git_branch / git_status#FCA17D (피치)#1a1a2e bold
언어 버전 (golang 등)#86BBD8 (연파랑)#1a1a2e bold
docker_context#06969A (청록)#ffffff bold
time#33658A#4A86C5#ffffff bold

time 세그먼트의 원래 배경 #33658A는 너무 어두워서 거의 터미널 배경과 합쳐져 보였습니다. 더 밝은 파랑 #4A86C5로 바꾸고, format 문자열 안의 배경/전경 치환자 두 군데도 같이 교체했습니다.

bg:#33658A) → bg:#4A86C5)
fg:#33658A) → fg:#4A86C5)

Nerd Font 글리프가 많은 파일이라 직접 편집하면 글자가 깨지기 쉽습니다. 수정은 Python 스크립트로 바이트 안전하게 적용했습니다.

결과

속도 (20회 평균, 같은 zsh 내 print -P "$PROMPT")

상태big-repo (246MB .git)small-repo
agnoster 원본427 ms335 ms
Starship (pastel-powerline)~70 ms~60 ms

약 6배입니다. 인간 지각 한계(~100ms) 안쪽이라 체감상 즉시 반응합니다. Enter가 걸리던 감각이 완전히 사라졌습니다.

시각

실제 화면입니다(일부 모자이크).

Starship 프롬프트 실제 렌더링

표시 요소:

  • 사용자명 (흰 bold, 보라 배경)
  • 디렉토리 (3계층 축약)
  • git 브랜치 + 상태 (stashed $ / untracked ? / modified ! / renamed / ahead/behind 등)
  • 감지된 언어 버전 (위 예시엔 Go v7.3.2, Java v25.0.1)
  • 시간 (♥ HH:MM)
  • 두 번째 줄: 입력 커서

이 과정에서 배운 것

  1. 느린 걸 고치려면 먼저 제대로 재야 합니다. 제가 잰 zsh -i -c exit는 Enter 지연이 아니라 쉘 시작 시간이었습니다. 엉뚱한 걸 최적화하느라 며칠 돌아갔습니다.

  2. spawn 횟수는 내부 로직 속도와 독립입니다. git이 1ms에 끝나도 7번 띄우면 350ms가 듭니다. 개선 가능한 구간이 보여도, 그 부분이 병목인지부터 확인해야 합니다.

  3. 도구의 구조를 모르면 설정도 고치기 어렵습니다. agnoster가 PROMPT 변수 안에 명령 치환을 넣는 구조라는 걸 몰랐다면 precmd 벤치마크만 반복하며 "왜 안 느리지?"라고 했을 겁니다.

  4. 근본 전환이 결국 싸게 먹힐 때가 있어요. 보조 옵션을 쌓아도 못 넘는 한계가 있을 때는 테마 자체를 갈아치우는 편이 빨라요. agnoster → Starship은 설정 파일 두 개 수정 + brew install 한 번이었어요.

참고 자료