macOS 개발 환경 설정: Homebrew, 셸, 런타임 버전 관리와 새 맥에서 자주 막히는 곳

이 글의 핵심

새 맥을 개발용으로 세팅할 때 순서는 Command Line Tools → Homebrew → 셸 → 런타임 버전 관리자 → Git·SSH → 도구 순서가 가장 덜 꼬입니다. 이 글은 그 순서대로 필요한 명령을 정리하면서, 인터넷의 오래된 세팅 글을 따라 하다 막히는 지점들(Homebrew Python에 pip install이 거부되는 PEP 668, 더 이상 없는 exa와 homebrew/cask-fonts 탭, grep·find를 다른 도구로 alias했을 때 깨지는 스크립트, 대소문자를 구분하지 않는 APFS와 Git core.ignorecase)을 함께 다룹니다. 마지막에는 Brewfile과 dotfiles로 다음 맥에서 같은 환경을 재현하는 방법을 정리합니다.

새 맥을 받으면 인터넷의 “맥 개발 환경 세팅” 글을 따라 명령을 복사하게 되는데, 이런 글은 빨리 낡습니다. 몇 년 전 글의 명령을 그대로 치면 탭이 없다거나, 패키지가 없어졌다거나, pip가 설치를 거부하는 에러를 연달아 만나게 됩니다. 이 글은 설치 순서를 정리하면서 2026년 기준으로 실제로 막히는 지점을 함께 적었습니다. 모든 도구를 다 깔 필요는 없고, 자기 작업에 필요한 부분만 골라 쓰면 됩니다.

설치 순서가 중요한 이유는 의존 관계 때문입니다. Homebrew는 Command Line Tools가 필요하고, 런타임 버전 관리자는 셸 설정 파일을 건드리며, Git과 SSH 설정은 그 뒤에 해야 한 번에 끝납니다.


1단계: Command Line Tools

clang, git, make 같은 기본 개발 도구 묶음입니다. Xcode 전체가 아니라 이것만 있어도 대부분의 개발이 됩니다.

xcode-select --install
xcode-select -p          # /Library/Developer/CommandLineTools
clang --version          # Apple clang version ...

gcc --version을 쳐도 Apple clang이 나옵니다. macOS의 gcc는 clang을 가리키는 이름일 뿐이고, 진짜 GCC가 필요하면 brew install gcc로 설치한 뒤 gcc-14처럼 버전이 붙은 이름으로 불러야 합니다. C++ 빌드 설정에서 “GCC로 빌드했다”고 생각했는데 실제로는 clang이었던 경우가 꽤 흔합니다.

macOS 대규모 업데이트 뒤 xcrun: error: invalid active developer path가 나오면 Command Line Tools가 업데이트 과정에서 무효화된 것이니 xcode-select --install을 다시 실행하면 됩니다.


2단계: Homebrew

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

설치가 끝나면 마지막에 PATH 설정 명령을 안내합니다. Apple Silicon 맥은 Homebrew가 /opt/homebrew에 설치되고, 이 경로는 기본 PATH에 없으므로 셸 설정에 추가해야 합니다.

echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"
brew --version

.zshrc가 아니라 .zprofile에 넣는 것이 Homebrew의 공식 안내입니다. .zprofile은 로그인 셸에서 한 번 읽히고 .zshrc는 대화형 셸마다 읽히는데, PATH는 한 번만 설정되면 되기 때문입니다.

위치가 Intel과 다른 이유: Intel 맥은 전통적인 /usr/local, Apple Silicon은 /opt/homebrew입니다. 두 아키텍처용 Homebrew가 한 맥에 공존할 수 있게(Rosetta로 Intel용 Homebrew를 따로 쓰는 경우) 경로를 나눈 것입니다. 그래서 Intel 맥 시절의 스크립트나 빌드 설정에 /usr/local/include, /usr/local/lib이 하드코딩돼 있으면 Apple Silicon에서 라이브러리를 못 찾습니다. 경로는 $(brew --prefix) 또는 $(brew --prefix openssl@3)처럼 물어서 쓰는 습관이 좋습니다.

brew install <패키지>            # CLI 도구 (formula)
brew install --cask <앱>         # GUI 앱 (cask)
brew update && brew upgrade      # Homebrew 자체와 설치된 패키지 업데이트
brew outdated                    # 업데이트 대상 확인
brew cleanup                     # 오래된 버전 정리
brew doctor                      # 설정 문제 진단
brew services start postgresql@17   # 백그라운드 서비스로 실행

/opt/homebrew에 권한 오류가 나서 sudo brew나 sudo chown -R을 쓰고 싶어질 때가 있는데, Homebrew는 sudo로 실행하는 것을 막고 있고 소유권을 바꾸면 이후 설치가 더 꼬입니다. 그럴 때는 먼저 brew doctor의 안내를 따르세요.


3단계: 터미널과 셸

macOS Catalina부터 기본 셸은 Zsh입니다. 기본 Terminal.app으로도 충분하지만, 분할 화면과 검색이 편한 iTerm2, 빠른 GPU 렌더링의 Ghostty·WezTerm 같은 선택지가 있습니다.

brew install --cask iterm2      # 또는 ghostty, wezterm

폰트

예전 글에는 brew tap homebrew/cask-fonts가 먼저 나오는데, 이 탭은 2024년에 폐지되고 폰트 cask가 메인 cask 저장소로 합쳐졌습니다. 지금 이 명령을 치면 탭이 없다는 에러가 나거나 경고가 뜨므로 그냥 빼고 설치하면 됩니다.

brew install --cask font-jetbrains-mono font-fira-code font-jetbrains-mono-nerd-font

프롬프트 테마가 아이콘을 쓴다면 “Nerd Font” 버전을 터미널 폰트로 지정해야 아이콘이 네모로 깨지지 않습니다.

Zsh 설정

플러그인 관리 프레임워크로 Oh My Zsh가 가장 흔하지만 필수는 아닙니다. 시작이 느려지는 게 싫다면 Homebrew로 플러그인만 설치해 직접 불러와도 됩니다.

brew install zsh-autosuggestions zsh-syntax-highlighting starship
# ~/.zshrc
source $(brew --prefix)/share/zsh-autosuggestions/zsh-autosuggestions.zsh
eval "$(starship init zsh)"
# syntax-highlighting은 다른 플러그인보다 마지막에 불러와야 함
source $(brew --prefix)/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

Oh My Zsh를 쓴다면 설치 후 ~/.zshrc의 plugins=(git ...)에 필요한 것만 넣으세요. 플러그인을 수십 개 켜 두면 새 탭을 열 때마다 눈에 띄게 느려집니다. 셸 시작 시간은 time zsh -i -c exit로 잴 수 있고, 0.3초를 넘기 시작하면 무엇이 느린지 찾아볼 때입니다(nvm 초기화가 흔한 원인입니다).

alias는 신중하게

# ~/.zshrc
alias ll='eza -lah --git'
alias gs='git status'
alias gl='git log --oneline --graph --all'
alias ports='lsof -i -P -n | grep LISTEN'
alias ....='cd ../../..'

인기 있는 세팅 글에는 alias grep='rg', alias find='fd', alias cat='bat' 같은 설정이 자주 나오는데, 저는 기존 명령 이름을 덮어쓰는 alias는 권하지 않습니다. rg와 fd는 grep·find와 옵션 체계가 달라서, 문서나 동료가 알려준 grep -r ... --include 같은 명령을 붙여 넣으면 엉뚱하게 동작합니다. 또 대화형 셸에서만 alias가 적용되기 때문에 “터미널에서는 되는데 스크립트에서는 결과가 다른” 혼란이 생깁니다. 새 도구는 원래 이름(rg, fd, bat)으로 쓰는 편이 오래 갑니다.


4단계: 현대적인 CLI 도구

brew install ripgrep fd fzf bat eza jq yq tree htop tldr git-delta gh

ls 대체로 유명했던 exa는 개발이 중단되어 Homebrew에서도 제거됐고, 포크인 eza가 이어받았습니다. 옛 글의 brew install exa가 실패하면 eza로 바꾸면 되고, 옵션은 거의 같습니다.

git-delta는 Git diff를 보기 좋게 바꿔 줍니다.

git config --global core.pager delta
git config --global interactive.diffFilter 'delta --color-only'
git config --global delta.navigate true

5단계: 런타임 버전 관리

언어 런타임은 Homebrew로 직접 설치하기보다 버전 관리자를 쓰는 편이 낫습니다. 프로젝트마다 요구 버전이 다르고, Homebrew는 brew upgrade 때 런타임을 새 버전으로 올려 버려서 기존 프로젝트가 갑자기 깨질 수 있기 때문입니다.

하나로 통합: mise

최근에는 Node·Python·Go·Ruby·Java 등을 하나의 도구로 관리하는 mise(예전 이름 rtx)를 쓰는 경우가 늘었습니다. 프로젝트 폴더에 .mise.toml이나 .tool-versions를 두면 그 폴더에 들어갈 때 자동으로 버전이 바뀝니다.

brew install mise
echo 'eval "$(mise activate zsh)"' >> ~/.zshrc
mise use --global node@22 [email protected] go@latest
mise use node@20          # 현재 프로젝트만 (.mise.toml 생성)

자세한 사용법은 mise 런타임 버전 관리에 정리했습니다.

언어별 전용 도구를 쓴다면

Node.js (nvm): nvm은 Homebrew 설치를 공식 지원하지 않으므로 공식 설치 스크립트를 씁니다(버전 번호는 nvm 저장소의 최신 릴리스로 바꾸세요). nvm은 셸 시작 시간을 늘리는 대표 원인이라, 속도가 중요하면 fnm이나 mise가 대안입니다.

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
nvm install --lts
nvm alias default lts/*
corepack enable            # pnpm·yarn을 프로젝트가 지정한 버전으로 사용

Python (pyenv 또는 uv):

brew install pyenv
cat >> ~/.zshrc <<'EOF'
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
EOF
pyenv install 3.13
pyenv global 3.13

예전 글의 eval "$(pyenv init --path)"는 지금 버전에서는 필요 없고, 위의 pyenv init - zsh 한 줄이 공식 안내입니다. pyenv는 Python을 소스에서 빌드하므로 처음 설치할 때 몇 분 걸립니다. 빌드 없이 빠르게 받고 가상 환경과 패키지까지 한 번에 관리하고 싶다면 uv가 좋은 선택입니다(brew install uv, uv python install 3.13, uv venv).

Go: brew install go면 충분하고, 버전을 여러 개 써야 할 때만 mise를 씁니다. Go 1.11 이후 모듈 모드에서는 GOPATH를 직접 설정할 필요가 없으며 기본값 ~/go를 쓰면 됩니다. go install로 설치한 도구를 쓰려면 ~/go/bin만 PATH에 추가합니다.

Rust: Homebrew의 rust 대신 공식 rustup을 씁니다. 툴체인 전환, 크로스 컴파일 타깃 추가가 rustup으로만 제대로 됩니다.

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Java: SDKMAN이나 mise로 설치하고, IDE가 요구하는 JDK 버전을 프로젝트별로 고정합니다.

Python 전역 pip가 막히는 이유 (PEP 668)

새 맥에서 가장 자주 받는 질문이 이것입니다.

error: externally-managed-environment
× This environment is externally managed
╰─> To install Python packages system-wide, try brew install xyz ...

Homebrew의 Python은 PEP 668에 따라 전역 pip install을 거부합니다. Homebrew가 관리하는 패키지 파일을 pip가 덮어써서, 나중에 brew upgrade 때 Python이나 그 Python에 의존하는 다른 Homebrew 패키지가 깨지는 일을 막기 위해서입니다. 오래된 세팅 글의 pip install virtualenv pipenv black requests numpy를 그대로 치면 바로 이 에러가 납니다.

  • 프로젝트 의존성: 프로젝트마다 가상 환경을 만듭니다. python3 -m venv .venv && source .venv/bin/activate 또는 uv venv.
  • 전역에서 쓰는 CLI 도구(black, ruff, httpie, poetry 등): pipx install ruff 또는 uv tool install ruff. 도구마다 독립된 가상 환경에 설치되어 서로 충돌하지 않습니다.
  • --break-system-packages로 강제할 수 있지만, 저는 이 옵션을 쓴 뒤 brew upgrade에서 Python 관련 패키지가 꼬여 재설치한 적이 있어서 쓰지 않습니다.

6단계: Git과 SSH

git config --global user.name "Your Name"
git config --global user.email "[email protected]"
git config --global init.defaultBranch main
git config --global pull.rebase true
git config --global push.autoSetupRemote true   # 새 브랜치 첫 push에 -u 불필요 (Git 2.37+)
git config --global core.editor "code --wait"

core.ignorecase는 건드리지 않기

macOS의 기본 파일 시스템(APFS)은 대소문자를 구분하지 않습니다. Readme.md와 README.md를 같은 파일로 봅니다. Git은 저장소를 만들 때 이를 감지해 core.ignorecase=true로 설정하는데, 일부 세팅 글은 “대소문자 구분을 켜자”며 git config --global core.ignorecase false를 권합니다. 이건 파일 시스템의 동작을 바꾸지 않고 Git의 판단만 바꾸는 것이라, 파일 이름의 대소문자만 바꾸면 Git이 두 파일이 있다고 착각해 커밋이 꼬일 수 있습니다. Git 공식 문서도 이 값을 수동으로 바꾸지 말라고 안내합니다. 파일 이름 대소문자를 바꿀 때는 git mv readme.md README.md를 쓰면 됩니다.

반대로 리눅스에서 대소문자만 다른 두 파일(Makefile, makefile)이 있는 저장소를 맥에서 클론하면 한쪽이 덮여 버립니다. 이런 저장소를 다뤄야 한다면 디스크 유틸리티에서 대소문자 구분 APFS 볼륨을 따로 만들어 그 안에서 작업합니다.

SSH 키

ssh-keygen -t ed25519 -C "[email protected]"
ssh-add --apple-use-keychain ~/.ssh/id_ed25519   # 암호를 키체인에 저장
pbcopy < ~/.ssh/id_ed25519.pub                    # GitHub → Settings → SSH keys에 등록
# ~/.ssh/config
Host github.com
  AddKeysToAgent yes
  UseKeychain yes
  IdentityFile ~/.ssh/id_ed25519

UseKeychain은 macOS 전용 옵션이라, 같은 ~/.ssh/config를 리눅스와 공유하면 리눅스 ssh가 “Bad configuration option”을 냅니다. 공유한다면 IgnoreUnknown UseKeychain을 맨 위에 넣어 둡니다. 연결 확인은 ssh -T [email protected]입니다.

커밋 서명은 GPG 대신 SSH 키로도 할 수 있어서(Git 2.34+) 키를 하나로 관리할 수 있습니다.

git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true

GitHub에 같은 공개키를 “Signing key”로 한 번 더 등록해야 “Verified”가 표시됩니다.


7단계: 앱과 서비스

# 에디터
brew install --cask visual-studio-code jetbrains-toolbox
# 컨테이너: Docker Desktop 또는 가벼운 대안
brew install --cask docker-desktop      # 예전 cask 이름은 docker
# brew install --cask orbstack          # 가벼운 대안 (개인 무료, 상업적 사용은 유료)
# brew install colima docker            # CLI만으로 쓰는 오픈소스 대안
# DB
brew install postgresql@17 redis
brew services start postgresql@17
# 창 관리·유틸리티
brew install --cask rectangle maccy stats

Docker는 이제 docker-compose(하이픈)가 아니라 docker compose 서브커맨드로 씁니다. Apple Silicon에서 amd64 전용 이미지를 돌리면 에뮬레이션이라 매우 느리므로, 가능하면 arm64(멀티 아키텍처) 이미지를 쓰고 필요할 때만 --platform linux/amd64를 줍니다.

모바일 개발: iOS는 App Store에서 Xcode를 설치하고 CocoaPods는 brew install cocoapods로 설치합니다(예전 글의 sudo gem install cocoapods는 시스템 Ruby를 건드리므로 피합니다). React Native의 전역 react-native-cli는 더 이상 쓰지 않고 npx @react-native-community/cli로 대체됐습니다.


8단계: macOS 설정

# Finder: 숨김 파일, 확장자, 경로 막대 표시
defaults write com.apple.finder AppleShowAllFiles -bool true
defaults write NSGlobalDomain AppleShowAllExtensions -bool true
defaults write com.apple.finder ShowPathbar -bool true
killall Finder

# 스크린샷 저장 위치
mkdir -p ~/Pictures/Screenshots
defaults write com.apple.screencapture location ~/Pictures/Screenshots

# 키 반복 속도 (적용하려면 로그아웃 필요)
defaults write NSGlobalDomain KeyRepeat -int 2
defaults write NSGlobalDomain InitialKeyRepeat -int 15

defaults는 앱의 설정 파일(plist)을 직접 고치는 명령입니다. 앱이 실행 중이면 메모리의 설정으로 다시 덮어쓸 수 있어서 killall로 재시작해야 반영되고, macOS 버전이 바뀌면 키 이름이 사라져 조용히 무시되는 경우도 있습니다. Finder에서는 ⌘⇧.로 숨김 파일 표시를 바로 토글할 수 있으니 명령을 외울 필요는 없습니다.

설정 앱 메뉴 이름은 macOS Ventura부터 “시스템 환경설정”이 “시스템 설정”으로 바뀌었습니다. FileVault(시스템 설정 → 개인정보 보호 및 보안)와 방화벽(시스템 설정 → 네트워크 → 방화벽)은 켜 두는 것을 권합니다.

비밀 값 관리

~/.zshrc에 export OPENAI_API_KEY=...를 직접 적는 방식은 dotfiles를 Git에 올리는 순간 키가 유출됩니다. 저장소에 올리지 않는 별도 파일(~/.secrets)에 두고 .zshrc에서 불러오거나, macOS 키체인(security find-generic-password -w -s openai)이나 1Password CLI(op read)로 필요할 때 꺼내 쓰는 쪽이 안전합니다.


9단계: 다음 맥에서 재현하기

Brewfile

brew bundle dump --file=~/dotfiles/Brewfile --force   # 현재 설치 목록 저장
brew bundle --file=~/dotfiles/Brewfile                # 새 맥에서 설치
brew bundle cleanup --file=~/dotfiles/Brewfile        # 목록에 없는 패키지 확인 (--force로 삭제)
# Brewfile
brew "git"
brew "ripgrep"
brew "eza"
brew "mise"
cask "visual-studio-code"
cask "iterm2"
cask "font-jetbrains-mono-nerd-font"
mas "Xcode", id: 497799835     # App Store 앱 (brew install mas 필요)

dump로 뽑은 목록에는 의존성으로 딸려 온 패키지까지 섞이지 않고 직접 설치한 것만 들어가지만, 한 번 써 보고 버린 도구도 남아 있습니다. 새 맥으로 옮기기 전에 한 번 훑어서 정리하는 게 좋습니다.

dotfiles

.zshrc, .gitconfig, .ssh/config(키 제외) 같은 설정 파일을 Git 저장소에 두고 심볼릭 링크로 연결합니다. 링크를 손으로 만들기보다 GNU Stow(brew install stow)나 chezmoi를 쓰면 새 맥에서 한 명령으로 복원됩니다.

# ~/dotfiles/zsh/.zshrc, ~/dotfiles/git/.gitconfig 구조일 때
cd ~/dotfiles && stow zsh git

앱 설정을 동기화해 준다는 Mackup은 최근 macOS에서 환경설정 파일을 심볼릭 링크로 바꾸는 방식이 앱 설정을 깨뜨리는 문제가 알려져 있어서, 쓸 거라면 README의 macOS 관련 경고를 먼저 확인하세요. 저는 셸·Git·에디터 설정만 dotfiles로 관리하고 GUI 앱 설정은 각 앱의 동기화 기능에 맡기는 쪽이 덜 번거로웠습니다.


문제 해결

증상원인해결
brew: command not found (설치 직후)/opt/homebrew/bin이 PATH에 없음eval "$(/opt/homebrew/bin/brew shellenv)"를 ~/.zprofile에 추가
Error: homebrew/cask-fonts was deprecated폐지된 탭brew tap 줄 삭제, 폰트 cask는 그대로 설치
No available formula with the name "exa"exa 제거됨eza 설치
externally-managed-environmentHomebrew Python의 PEP 668 보호venv, pipx, uv tool install 사용
xcrun: error: invalid active developer pathmacOS 업데이트 후 CLT 무효화xcode-select --install
헤더·라이브러리를 못 찾음 (Apple Silicon)/usr/local 경로 하드코딩$(brew --prefix) 기준으로 경로 지정
EACCES로 npm install -g 실패시스템 Node 사용 또는 권한 꼬임nvm·mise로 설치한 Node 사용 (sudo 금지)
포트가 이미 사용 중다른 프로세스가 점유lsof -i :3000으로 PID 확인 후 종료. macOS의 AirPlay 수신 모드가 5000·7000번을 쓰므로 Flask 기본 포트가 막힐 수 있음

마지막 포트 문제는 macOS Monterey 이후 자주 보는 함정입니다. Flask 개발 서버를 기본 포트 5000으로 띄웠는데 Address already in use가 나면, 대개 AirPlay 수신 모드(ControlCenter 프로세스)입니다. 시스템 설정 → 일반 → AirDrop 및 Handoff에서 AirPlay 수신 모드를 끄거나 다른 포트를 쓰면 됩니다.


같이 보면 좋은 글

참고 자료