Shell 실행이 느려졌을 때 원인 찾고 개선하는 방법
iTerm을 열 때마다 커서가 뜨기까지 2~3초씩 걸리는 경험을 해본 적이 있을 것이다. 처음엔 괜찮았는데 이것저것 설치하다 보면 어느 순간부터 체감될 정도로 느려진다. 이건 대부분 쉘 설정 파일(.zshrc, .zprofile 등)에서 무거운 초기화 스크립트를 매번 실행하기 때문이다.
느려지는 원인
zsh 기준으로, 터미널을 열면 다음 순서로 설정 파일을 읽는다.
.zshenv— 모든 zsh 세션에서 가장 먼저 실행.zprofile— 로그인 쉘에서 실행.zshrc— 인터랙티브 쉘에서 실행.zlogin— 로그인 쉘에서.zshrc다음에 실행
이 파일들에 있는 초기화 코드가 하나하나 실행되면서 시간이 누적된다. 특히 다음과 같은 것들이 대표적인 주범이다.
conda init — Python 인터프리터를 매번 새로 띄워서 shell hook 코드를 생성한다. conda init zsh를 실행하면 .zshrc에 자동으로 추가되는 블록인데, 이것 하나로 1초 이상 잡아먹는 경우가 흔하다.
NVM (Node Version Manager) — nvm.sh 스크립트를 source 하는 과정이 무겁다. 0.5~0.7초 정도 걸리는데, .zshenv와 .zshrc 양쪽에 중복으로 들어가 있는 경우도 있어서 두 배로 느려지기도 한다.
oh-my-zsh + 플러그인 — oh-my-zsh 자체는 그렇게 무겁지 않지만, 플러그인을 여러 개 활성화하면 합산 시간이 늘어난다. 특히 Git 관련 플러그인이나 gitstatus 같은 것들이 느릴 수 있다.
rbenv, pyenv 등 버전 매니저 — eval "$(rbenv init -)" 같은 코드가 셸 시작마다 실행된다.
brew shellenv — Homebrew 경로를 설정하는 코드인데, 0.2~0.3초 정도 소요된다.
Cloud SDK (gcloud, aws-cli 등) — 자동완성 스크립트가 생각보다 무겁다.
시작 시간 측정하기
느린 건 알겠는데 정확히 뭐가 느린지 모르겠다면 측정부터 해야 한다.
전체 시작 시간 측정
/usr/bin/time zsh -i -c exit
이렇게 하면 zsh 인터랙티브 셸을 실행하고 바로 종료하면서 걸린 시간을 보여준다. 설정 파일 없이 실행하려면 이렇게 한다.
/usr/bin/time zsh --no-rcs -i -c exit
두 값을 비교하면 설정 파일이 얼마나 영향을 주는지 바로 알 수 있다. 설정 파일 없이는 보통 0.01초 이내로 끝난다.
컴포넌트별 시간 측정
어떤 설정이 느린지 개별적으로 측정하려면 각 초기화 코드를 따로 time으로 감싸서 확인한다.
# NVM 로딩 시간 측정
/usr/bin/time zsh -c 'source "$NVM_DIR/nvm.sh"' 2>&1
# conda init 시간 측정
/usr/bin/time zsh -c 'eval "$(/opt/anaconda3/bin/conda shell.zsh hook)"' 2>&1
# brew shellenv 시간 측정
/usr/bin/time zsh -c 'eval "$(/opt/homebrew/bin/brew shellenv)"' 2>&1
# oh-my-zsh 로딩 시간 측정
/usr/bin/time zsh -c 'source $ZSH/oh-my-zsh.sh' 2>&1
zsh 프로파일링
더 정밀하게 보고 싶다면 .zshrc 최상단과 최하단에 프로파일링 코드를 넣는 방법도 있다.
# .zshrc 최상단에 추가
zmodload zsh/zprof
# .zshrc 최하단에 추가
zprof
이렇게 하면 새 터미널을 열 때 어떤 함수가 얼마나 걸렸는지 상세한 프로파일링 결과가 출력된다. 확인이 끝나면 이 코드는 지워주면 된다.
개선 방법
1. 중복 로딩 제거
가장 먼저 확인할 것은 같은 코드가 여러 파일에 중복으로 들어가 있는지다. 특히 NVM이나 conda 같은 초기화 코드가 .zshenv와 .zshrc 양쪽에 있는 경우가 많다. .zshenv는 모든 zsh 세션(비인터랙티브 포함)에서 실행되므로, 인터랙티브 셸에서만 필요한 코드는 .zshrc에만 두면 된다.
# .zshenv에서 NVM 관련 코드가 있다면 삭제
# .zshrc에만 유지
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
2. Lazy Loading 적용
가장 효과가 큰 방법이다. 매번 실행할 필요 없는 초기화 코드를, 해당 명령어를 처음 사용할 때만 실행하도록 바꾸는 것이다.
NVM Lazy Loading
# 기존 방식 (매번 0.5초 이상 소요)
# export NVM_DIR="$HOME/.nvm"
# [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
# lazy loading 방식
export NVM_DIR="$HOME/.nvm"
nvm() {
unfunction nvm node npm npx
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm "$@"
}
node() {
unfunction nvm node npm npx
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
node "$@"
}
npm() {
unfunction nvm node npm npx
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
npm "$@"
}
npx() {
unfunction nvm node npm npx
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
npx "$@"
}
nvm, node, npm, npx 중 아무거나 처음 실행하면 그때 nvm.sh를 로드하고, 이후에는 정상적으로 동작한다. 처음 한 번만 약간의 딜레이가 있고 이후에는 아무 차이 없다.
Conda Lazy Loading
# 기존 방식 (매번 1초 이상 소요)
# __conda_setup="$('/opt/anaconda3/bin/conda' 'shell.zsh' 'hook' 2> /dev/null)"
# ...
# lazy loading 방식
conda() {
unfunction conda
__conda_setup="$('/opt/anaconda3/bin/conda' 'shell.zsh' 'hook' 2> /dev/null)"
if [ $? -eq 0 ]; then
eval "$__conda_setup"
else
if [ -f "/opt/anaconda3/etc/profile.d/conda.sh" ]; then
. "/opt/anaconda3/etc/profile.d/conda.sh"
else
export PATH="/opt/anaconda3/bin:$PATH"
fi
fi
unset __conda_setup
conda "$@"
}
conda 명령어를 처음 실행할 때만 초기화가 되므로, 셸 시작 시간에서 1초 이상이 빠진다.
rbenv Lazy Loading
# 기존 방식
# eval "$(rbenv init - zsh)"
# lazy loading 방식
rbenv() {
unfunction rbenv
eval "$(command rbenv init - zsh)"
rbenv "$@"
}
3. oh-my-zsh 플러그인 정리
.zshrc에서 plugins=(...)에 들어가 있는 플러그인 목록을 확인하고, 실제로 안 쓰는 플러그인은 제거한다.
# 많이 넣으면 느려진다
plugins=(git nvm docker kubectl aws terraform golang rust python)
# 실제로 쓰는 것만 남기기
plugins=(git docker)
oh-my-zsh 자체가 아예 필요 없다면 제거하고 직접 필요한 설정만 가져다 쓰는 것도 방법이다. zsh-syntax-highlighting과 zsh-autosuggestions 두 개만 있어도 충분한 경우가 많다.
4. 자동완성 스크립트 경량화
gcloud, aws-cli 등의 자동완성 스크립트가 무거운 경우, 필요할 때만 로드하거나 아예 빼는 것도 고려할 만하다.
# gcloud completion이 0.5초 이상 걸리는 경우
# 자동완성 없이 PATH만 추가
export PATH="$HOME/google-cloud-sdk/bin:$PATH"
# 자동완성이 필요하면 별도 함수로 분리
gcloud-init() {
source "$HOME/google-cloud-sdk/completion.zsh.inc"
}
적용 전후 비교
실제로 lazy loading을 적용한 결과를 보면 효과가 확실하다.
적용 전: 2.23 real 1.17 user 0.88 sys
적용 후: 0.55 real 0.31 user 0.22 sys
conda와 NVM만 lazy loading으로 바꿔도 1.5초 이상 줄어든다. 터미널을 자주 여는 사람이라면 이 차이가 꽤 크게 느껴진다.
정리
셸이 느린 이유는 대부분 .zshrc에 있다. 측정해보면 conda, NVM, rbenv 같은 버전 매니저와 Cloud SDK의 초기화 스크립트가 대부분의 시간을 차지한다.
- 중복 로딩부터 제거한다
- 자주 안 쓰는 도구는 lazy loading으로 전환한다
- oh-my-zsh 플러그인은 필요한 것만 남긴다
Mac 개발환경 세팅하기 글에서 Homebrew, iTerm2, Zsh 기본 설치 방법을 정리해뒀으니 참고하면 된다.