본문으로 건너뛰기
글 목록으로 돌아가기
이 글 목차
~/posts/devops

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_shelltmux-smart-attach wrapper에 연결해 봤는데 그렇게 하니까 새 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 wrapperHerdr 네이티브 설정, 앱 재시작 없이 reloadtmux를 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은 설정하지 않고 두면 돼요.

동작 방식

스크립트는 간단한 결정 트리를 따라요:

  1. detach된 tmux 세션이 있는지 확인해요 (#{session_attached}가 0인 세션)
  2. 있으면 첫 번째 detached 세션(가장 낮은 번호)에 attach해요
  3. 없으면 가장 낮은 미사용 번호로 새 세션을 만들어요

세션 번호 매기기

세션생성 시점
0첫 번째 창, 세션 없음
1두 번째 창, 0이 attached 상태
2세 번째 창, 01 모두 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의 -CC control 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 세션 재사용 덕분에 창을 닫아도 작업을 잃지 않아요.

참고 자료

댓글

글 목록으로 돌아가기
enko