셸·환경변수·프로세스 CLI 심화
PATH·.bashrc·리디렉션·글로빙·ps/top/kill/lsof — 매일 만나는 터미널 문제를 스스로 푸는 법.
Claude Code 한 번 실행하는 데도 `command not found: claude` (PATH 문제), `port 3000 already in use` (프로세스 점유) 같은 장애물이 쏟아진다. 기존 ch03 터미널 토픽은 cd/ls 수준이라 실전 문제를 풀 수 없다. 이 토픽으로 실무에서 막히지 않는 셸 감각을 확보한다.
ch03에서 배운 cd·ls·mkdir은 터미널의 기본 동사다. 이 토픽에서는 실무에서 막혔을 때 푸는 도구들을 다룬다. 세 개 축이다. ① 셸 설정(.bashrc·PATH·환경변수), ② 입출력 조작(리디렉션·파이프·글로빙), ③ 프로세스 감시·제어(ps·kill·lsof·netstat).
이 토픽 한 번 읽고 나면 "포트가 이미 사용 중", "command not found", "환경변수를 어디에 두지?" 이 세 가지 질문에 스스로 답할 수 있게 된다.
PATH가 뭐길래 설치 가이드마다 등장할까? PATH는 셸이 명령어를 찾을 때 뒤지는 폴더 목록이다. `claude`라고 입력하면 셸이 PATH에 적힌 폴더를 순서대로 돌면서 `claude`라는 실행파일을 찾는다. 없으면 `command not found`. 설치 후 `~/.nvm/...`나 `/usr/local/bin` 같은 경로를 PATH에 추가하라는 가이드는 이 목록에 편입시키라는 뜻이다.
# 1) 현재 PATH 확인 (콜론으로 구분된 폴더 목록)
echo $PATH
# → /usr/local/bin:/usr/bin:/bin:/Users/kim/.nvm/versions/node/v20.0.0/bin:...
# 2) 특정 명령어가 어느 경로에서 실행되는지 확인
which claude
# → /Users/kim/.claude/local/claude (← 이 경로가 PATH 안에 있어야 함)
# 3) PATH에 임시로 폴더 추가 (현재 셸만)
export PATH="/new/folder:$PATH"
# 4) 영구 적용 — ~/.bashrc 또는 ~/.zshrc에 추가
echo 'export PATH="$HOME/.claude/local:$PATH"' >> ~/.zshrc
source ~/.zshrc # 현재 셸에 즉시 반영
# Windows (PowerShell)
$env:Path
# 영구 추가는 System Properties → Environment Variables GUI 또는
# [Environment]::SetEnvironmentVariable("Path", "...", "User")`command not found` 90%는 PATH 문제다. `which 명령어`로 위치 확인 → 없으면 PATH 추가.
| 파일 | 언제 읽힘 | 용도 |
|---|---|---|
| `~/.zshrc` | zsh 대화형 셸 시작 시 (macOS 기본) | PATH·별칭·함수 정의 |
| `~/.bashrc` | bash 대화형 셸 시작 시 | 위와 동일 (Linux·Git Bash) |
| `~/.bash_profile` | 로그인 셸 시작 시 | PATH·환경변수 |
| `~/.profile` | 모든 셸 공통 | 로그인 환경변수 |
| `.env` (프로젝트) | 앱이 dotenv로 로드 | API 키·DB URL (Git 업로드 금지) |
어디에 적어야 할까? macOS 기본 셸은 zsh이므로 `~/.zshrc`. Linux·WSL·Git Bash는 `~/.bashrc`. 프로젝트 전용 환경변수(API 키 등)는 프로젝트 루트의 `.env` — 이건 셸이 아니라 앱이 `process.env`로 읽는다.
리디렉션은 명령어의 입출력을 파일·다른 명령어로 돌리는 문법이다. Claude Code가 자동으로 자주 쓰는 패턴이므로 읽을 줄은 알아야 한다.
| 기호 | 이름 | 뜻 | 예시 |
|---|---|---|---|
| `>` | 리디렉션 (덮어쓰기) | stdout을 파일로 | `npm run build > build.log` |
| `>>` | 리디렉션 (추가) | stdout을 파일 끝에 | `date >> history.log` |
| `2>` | stderr 리디렉션 | 에러만 파일로 | `cmd 2> errors.log` |
| `2>&1` | stderr→stdout 병합 | 에러도 같이 | `cmd > all.log 2>&1` |
| `|` | 파이프 | 앞 명령 출력 → 뒤 명령 입력 | `ls | grep .ts` |
| `&&` | AND 체인 | 앞이 성공해야 뒤 실행 | `npm test && npm run build` |
| `||` | OR 체인 | 앞이 실패해야 뒤 실행 | `cmd || echo '실패'` |
# 실전: 빌드 로그를 파일로 저장하면서 화면에도 출력 (tee)
npm run build 2>&1 | tee build.log
# 실전: 에러만 따로 저장하고 정상 출력은 화면에
npm run dev 2> errors.log
# 실전: 테스트 통과 후에만 배포
npm test && npm run deploy
# 실전: 특정 파일 검색해서 결과 개수만 세기
grep -r "TODO" src/ | wc -l
# 실전: 최근 5개 커밋을 파일로 저장
git log --oneline -5 > recent-commits.txt`2>&1`을 자주 본다면 "에러와 정상 출력을 한 파일에 섞는다"는 뜻. Claude가 빌드 로그를 분석할 때 자주 쓴다.
글로빙(globbing)은 `*`·`?`·`{a,b}` 같은 와일드카드로 여러 파일을 한 번에 지정하는 문법이다. Claude에게 `src/**/*.ts 검사해줘`라고 할 때 이 문법을 쓴다.
# * — 0개 이상의 임의 문자
ls src/*.ts # src 바로 아래 .ts 파일
ls src/**/*.ts # src 하위 모든 .ts (재귀)
# ? — 정확히 1개 문자
ls file?.txt # file1.txt, fileA.txt 등
# {a,b} — 나열된 것 중 하나
cp src/{App,Main}.tsx backup/
rm *.{log,tmp,bak}
# [0-9] — 문자 범위
ls log-[0-9][0-9].txt # log-00~log-99`` 재귀 글롭은 zsh·bash(globstar 옵션 필요)에서 작동. `ls src//*.ts` 외우면 트리 전체 스캔 한 줄로 해결.
실전 1순위: `EADDRINUSE: address already in use :::3000` 에러. 바이브코더 90%가 여기서 패닉. 해법은 3줄이다. ① 점유 중인 프로세스 찾기 → ② PID 확인 → ③ 죽이기.
# ━━━ Mac / Linux ━━━
# 1) 3000 포트를 점유 중인 프로세스 찾기
lsof -i :3000
# COMMAND PID USER ...
# node 4231 kim ...
# 2) 특정 PID 죽이기
kill 4231 # 정중하게 종료 (SIGTERM)
kill -9 4231 # 강제 종료 (SIGKILL, 위의 방법이 안 될 때만)
# 한 줄 콤보
lsof -ti:3000 | xargs kill -9
# ━━━ Windows (PowerShell) ━━━
# 1) 3000 포트 점유 프로세스 찾기
netstat -ano | findstr :3000
# TCP 0.0.0.0:3000 ... LISTENING 4231
# 2) PID로 종료
taskkill /F /PID 4231이 4줄이면 포트 충돌은 끝. `kill -9`는 최후의 수단 — 먼저 그냥 `kill`을 시도해서 데이터 저장 기회를 주는 것이 예의다.
| 도구 | 목적 | Mac/Linux | Windows |
|---|---|---|---|
| 프로세스 목록 | 뭐가 돌고 있나 | `ps aux` / `ps -ef` | `tasklist` / `Get-Process` |
| 실시간 모니터링 | CPU/메모리 관찰 | `top` / `htop` | `Get-Process` / 작업관리자 |
| 포트 점유 확인 | 누가 이 포트 쓰나 | `lsof -i :포트` | `netstat -ano | findstr :포트` |
| 프로세스 종료 | 죽이기 | `kill PID` / `kill -9 PID` | `taskkill /F /PID PID` |
| 이름으로 죽이기 | 빠른 정리 | `pkill node` | `taskkill /F /IM node.exe` |
# 실전 시나리오 3종 세트
# ① Node 프로세스가 여러 개 떠있어서 RAM 먹는 중 — 전부 청소
pkill node
# Windows: taskkill /F /IM node.exe
# ② 어제 켜놓은 dev 서버가 어디서 돌고 있지?
ps aux | grep "next dev"
# ③ 시스템 전체 리소스 실시간 관찰
top # q 눌러 종료
# 또는 더 예쁜 htop (brew install htop)`pkill 이름`은 이름으로 찾아 죽인다. 개발 중 좀비 Node 프로세스 정리에 가장 빠름.
Claude Code에게 이렇게 말하면 이 토픽의 내용이 활용된다. 예: "포트 3000이 점유됐어. `lsof -ti:3000 | xargs kill -9`로 정리하고 다시 `npm run dev`해줘." — 명령어를 직접 말해주면 Claude가 허가 받고 실행한다. 모르면 Claude가 OS 감지부터 다시 해야 하므로 느려진다.
한 줄 요약: PATH는 셸이 명령어를 찾는 폴더 목록, `.zshrc`/`.bashrc`는 PATH를 영구 설정하는 곳, `2>&1 | tee`는 로그를 두 군데 보내는 패턴, `lsof -i :포트` + `kill -9`는 포트 충돌 해결 공식.
`command not found: claude` 에러가 떴다. 가장 먼저 의심해야 할 환경변수는?
Mac/Linux에서 3000 포트를 점유 중인 프로세스를 한 줄 명령으로 모두 종료하는 명령은?
`npm test && npm run build`는 테스트가 실패해도 빌드가 실행된다.
`cmd > out.log 2>&1`은 정상 출력과 에러 출력을 모두 out.log에 저장한다.