이 글 목차

tmux 스마트 세션 자동 시작
iTerm2 Profile Command(`tmux-smart-attach`)로 tmux를 자동 시작해서, 새 터미널 창마다 독립된 tmux 세션을 갖고 닫힌 창의 세션은 재사용하는 방법이에요.
새 iTerm2 창(Cmd+N)마다 자체 tmux 세션으로 열리되, 닫힌 창의 detach된 세션은 재사용하고 싶었어요. 가장 뻔한 방법 — iTerm2의 “Send text at start”에 tmux new -A -s main을 넣는 것 — 은 항상 같은 main 세션에 다시 attach돼요. 그러면 모든 창이 독립된 작업 공간 대신 같은 pane과 내용을 보여주죠.
모든 컨텍스트에서 안정적으로 동작하는 방법에 안착할 때까지 세 번을 갈아엎었어요.
업데이트 (2026-07-04): 이제 제 환경에서는 터미널 워크스페이스 매니저(Herdr)가 그 레이어를 맡고 있어서 더 이상 기본 방식은 아니에요. 한번은 Herdr의
[terminal].default_shell을tmux-smart-attachwrapper에 연결해 봤는데 그렇게 하니까 새 Herdr pane마다 tmux가 중첩된 채로 시작하더라고요. 원하던 게 아니라서 다시 빼버렸어요. 그래도 독립형 iTerm2 스타일 창에서는 이 wrapper가 여전히 유효하고 tmux 중첩을 정말 원할 때는 Herdr에서 명시적으로 켜는 옵션으로 쓸 수 있어요(아래 Herdr 설정 참고). Herdr 버전은 기본 터미널 정책이 아니라 opt-in으로 생각하면 돼요.
뻔한 방법의 문제
iTerm2의 “Send text at start” 설정에 tmux new -A -s main\n을 넣으면 -A 플래그가 문제예요. -A는 “있으면 attach, 없으면 생성”이라는 뜻이거든요. 모든 창이 같은 명령을 실행하니까 전부 main에 attach돼요. 독립된 작업 공간이 아니라, 모든 창에 걸쳐 하나의 공유 세션이 생기는 거죠.
세 번의 반복
안착하기 전에 세 가지 방법을 시도했어요:
| 옵션 | 장점 | 단점 |
|---|---|---|
| iTerm2 “Send text at start” | 간단, 한 줄 설정 | 조건 분기 없음, 모든 창이 같은 세션 |
| .zshrc 자동 시작(폐기됨) | 전체 셸 로직, 창별 세션 | 비터미널 zsh 컨텍스트가 깨짐 (VS Code, .zshrc 소싱 스크립트) |
| iTerm2 Profile Command(선택) | 셸 프로파일과 분리, 스킵 조건 불필요 | 빠른 닫기/열기 시 알려진 race condition |
Herdr default_shell wrapper | Herdr 네이티브 설정, 앱 재시작 없이 reload | tmux를 Herdr 아래에 중첩, 키·마우스 처리에서 의도적 트레이드오프 필요 |
.zshrc 방식은 처음엔 괜찮아 보였어요 — 비iTerm2 컨텍스트를 건너뛰려고 guard 조건($TERM_PROGRAM, [[ -t 0 ]])을 넣었죠. 하지만 zsh 프로파일을 소싱하는 프로그램들(VS Code 통합 터미널, zsh -l을 쓰는 셸 스크립트)이 여전히 tmux 블록을 예상치 못하게 실행시킬 수 있었어요. 근본 문제는 이거예요: .zshrc는 너무 많은 컨텍스트에서 실행돼서 안전하게 게이트하기 어려워요.
해결 방법: iTerm2 Profile Command
tmux 시작을 iTerm2 Profile Command로 옮기면 컨텍스트 문제가 완전히 사라져요 — 스크립트는 iTerm2가 탭을 열 때만 실행되고 VS Code나 SSH, 스크립트 컨텍스트에서는 절대 실행되지 않아요.
스크립트 위치: ~/.local/bin/tmux-smart-attach
#!/bin/zsh -l
# tmux-smart-attach — iTerm2 profile startup command
# 1. Attach to first detached session (lowest numbered)
# 2. Create new session named with lowest unused number
# Ensure tmux is found (login shell via -l loads PATH)
detached=$(tmux ls -F '#{session_name} #{session_attached}' 2>/dev/null
| awk '$2 == 0 {print $1; exit}')
if [ -n "$detached" ]; then
exec tmux attach -t "$detached"
else
# Find lowest unused session number (fills gaps: 0,3 → creates 1)
taken=$(tmux ls -F '#{session_name}' 2>/dev/null | sort -n)
n=0
while echo "$taken" | grep -qx "$n"; do
((n++))
done
exec tmux new-session -s "$n"
fi iTerm2 설정: Profiles → General → Command → “Custom Shell” 선택 → 절대 경로 지정: /Users/<username>/.local/bin/tmux-smart-attach
선택 사항: Herdr에서 명시적으로 켜기
Herdr를 터미널 워크스페이스 매니저로 쓰면서 각 pane을 tmux 안에서 시작하고 싶다면, Herdr의 터미널 실행 파일을 같은 wrapper로 지정하고 서버 설정을 reload하면 돼요:
[terminal]
default_shell = "/Users/brandonwie/.local/bin/tmux-smart-attach"
shell_mode = "non_login" herdr server reload-config shell_mode = "non_login"은 Herdr가 이 wrapper를 pane 프로세스로 실행하게 해요. wrapper 자체의 #!/bin/zsh -l shebang이 tmux를 exec하기 전에 로그인 셸 환경을 먼저 로드하니까, PATH와 도구는 그대로 쓸 수 있어요. Herdr 0.7.1에서 직접 확인해보니 reload-config 직후 새로 분할한 pane이 tmux new-session -s 0으로 뜨더라고요.
Herdr를 원래의 pane 시작 방식으로 되돌리려면, [terminal] 오버라이드 전체를 지우고 다시 reload하면 돼요:
herdr server reload-config 이걸 opt-in으로 둔 데는 이유가 있어요 — tmux를 Herdr 아래에 중첩시키거든요. 그러니 각 Herdr pane이 그냥 평범한 셸로 열리길 바란다면 [terminal].default_shell은 설정하지 않고 두면 돼요.
동작 방식
스크립트는 간단한 결정 트리를 따라요:
- detach된 tmux 세션이 있는지 확인해요 (
#{session_attached}가 0인 세션) - 있으면 첫 번째 detached 세션(가장 낮은 번호)에 attach해요
- 없으면 가장 낮은 미사용 번호로 새 세션을 만들어요
세션 번호 매기기
| 세션 | 생성 시점 |
|---|---|
0 | 첫 번째 창, 세션 없음 |
1 | 두 번째 창, 0이 attached 상태 |
2 | 세 번째 창, 0과 1 모두 attached 상태 |
창을 닫으면 세션이 kill이 아니라 detach돼요. 다음 Cmd+N은 새로 만드는 대신 첫 번째 detached 세션에 다시 연결돼요.
gap-filling 로직 덕분에 세션 1을 kill하면(tmux kill-session -t 1) 세션 0과 2가 남아요. 다음 Cmd+N은 세션 3이 아니라 세션 1(빈자리)을 만들어요.
핵심 구현 디테일
#!/bin/zsh -l (로그인 셸 shebang) — .zprofile과 .zshrc를 로드해서 PATH에 Homebrew, asdf, 모든 도구가 포함돼요. ”.zshrc 로딩이 깨진다”던 예전 우려는 틀렸어요 — -l 플래그가 tmux 안에서도 전체 셸 환경을 보장하거든요.
스킵 조건 불필요 — .zshrc 방식과 달리, 이 스크립트는 iTerm2가 탭을 열 때만 실행돼요. VS Code 터미널, SSH 세션, 스크립트는 절대 호출하지 않아요.
exec tmux가 스크립트 프로세스를 교체 — tmux를 나간 뒤 남는 셸이 없어요. tmux 세션을 종료하면 터미널 창이 깔끔하게 닫혀요.
#{session_attached} 포맷 변수 — 클라이언트가 attached면 tmux가 1, detached면 0을 반환해요. “창에서 활성”인 세션과 “저장됐지만 창이 닫힌” 세션을 구분하는 방법이에요.
알려진 제한 사항
빠른 닫기/열기 시 race condition이 있어요: Cmd+W 직후 바로 Cmd+N을 하면 중복 세션이 생길 수 있어요. iTerm2가 창을 완전히 닫는 데 ~1초가 걸리거든요(애니메이션 + pty 정리). 그 사이 tmux 서버는 이전 세션을 여전히 attached로 봐요. kill -0 PID 체크와 sleep-retry 방식을 시도했지만 iTerm2의 닫기 파이프라인이 non-blocking 해결책을 쓰기엔 너무 느려요. 실제로는 문제를 거의 일으키지 않아요 — 중복 세션은 다음번에 그냥 재사용되거든요.
언제 사용하면 좋을까
- 각 터미널 창마다 자체 tmux 세션을 원할 때
- detach된 세션 재사용이 필요할 때 (창 닫고, 다시 열면, 세션이 돌아와요)
- 여러 터미널 컨텍스트(iTerm2, VS Code, SSH)에서 서로 다른 tmux 동작이 필요할 때
- 새 Herdr pane마다 tmux 안에서 시작하길 원하고 그에 따라오는 multiplexer 중첩 동작을 감수할 때
언제 사용하지 않으면 좋을까
- 의도적으로 모든 창에서 같은 tmux 세션을 쓰고 싶을 때 (페어 프로그래밍, 공유 뷰)
- iTerm2의
-CCcontrol mode를 쓸 때 (셸이 아니라 iTerm2가 라이프사이클을 관리해야 해요) - SSH 연결 끊김 후에도 tmux 세션이 유지돼야 하는 서버에서 (다른 패턴 —
exec없이tmux attach || tmux new사용) - 터미널 워크스페이스 매니저(Herdr 등)가 이미 최상위 워크스페이스 모델을 맡고 있을 때 — 그 아래에서 tmux를 자동 시작하면 중첩이 생기고, 터미널 레벨의 키·마우스 처리와 충돌할 수 있어요
- Herdr pane이 기본적으로 평범한 셸로 열리길 기대할 때 — 이 경우 Herdr의
[terminal].default_shell을 설정하지 않고 두면 Herdr가$SHELL로 폴백해요
마무리
.zshrc에서 tmux를 자동 시작하면 비터미널 zsh 컨텍스트가 깨져요. 로직을 iTerm2 Profile Command로 옮기면 완전히 분리돼요 — 스크립트가 iTerm2가 탭을 열 때만 실행되니까요. #!/bin/zsh -l shebang이 전체 셸 환경 로드를 보장하고, gap-filling을 쓰는 숫자 세션 이름이 창마다 자체 작업 공간을 주고, detached 세션 재사용 덕분에 창을 닫아도 작업을 잃지 않아요.