tmux 한 세션으로 프론트/백/인프라 개발환경 굴리기
들어가며
한 프로젝트가 back-end, front-end, infrastructure 세 폴더로 나뉘어 있으면, 터미널을 세 개 띄워 놓고 왔다갔다 하기 마련입니다. 여기에 claude나 codex 같은 CLI 에이전트까지 각 폴더에서 돌리려고 하면 창이 금세 지저분해집니다.
이번 글에서는 tmux 한 세션으로 이 셋을 묶고, 스크립트 하나로 재현 가능하게 만든 과정을 정리합니다. 주로 아래 내용을 다룹니다.
- window와 pane를 나눠서 fe/be/infra를 한 화면에 배치하는 법
- 에이전트를 띄우는 자리와 서비스를 구동하는 자리를 분리한 이유
- copy-mode를 vi로 바꾸고 WSL 클립보드(
clip.exe)와 연동한 설정 - 방향키 대신 이름과 위치로 이동하도록 바인딩한 부분
아래에서 다루는 start-dev.sh와 ~/.tmux.conf 전문은 gist에 올려 두었습니다. 통째로 가져가서 폴더 이름만 바꿔 쓰면 됩니다.
1. tmux 용어부터 정리
먼저 세 가지만 구분하면 됩니다.
- session: 작업 공간 전체입니다. 터미널을 닫아도 백그라운드에 살아 있습니다.
- window: session 안의 탭 같은 개념입니다. 한 번에 하나만 화면에 보입니다.
- pane: window를 화면 분할한 것입니다. 여러 개를 동시에 볼 수 있습니다.
여기서 중요한 점 하나는, window나 pane마다 독립된 셸 프로세스가 돈다는 것입니다. fe pane에서 도는 dev 서버 로그가 be pane으로 넘어가는 일은 없습니다. 안 보이는 window의 프로세스도 백그라운드에서 계속 실행됩니다.
2. 원하는 레이아웃
세 컴포넌트를 이렇게 배치하기로 했습니다.
┌──────────┬──────────┐
│ 1 백엔드 │ │
├──────────┤ 2 인프라 │
│ 3 프론트 │ │
└──────────┴──────────┘
좌상은 백엔드, 우측 전체는 인프라, 좌하는 프론트입니다. tmux에서 이 모양을 만들려면 pane를 만드는 순서가 중요합니다.
- 백엔드 폴더에서 세션을 시작합니다. 이 pane가 전체 화면입니다.
- 좌우로 분할해 오른쪽에 인프라를 놓습니다.
- 왼쪽(백엔드) pane를 다시 상하로 분할해 아래에 프론트를 놓습니다.
pane 번호는 tmux가 위치에 따라 재정렬하기 때문에, 번호로 targeting하면 헷갈립니다. 대신 각 pane의 고유 id를 변수로 잡아서 다루면 안정적입니다.
3. 에이전트 자리와 실행 자리를 나눈다
여기서 한 가지 문제가 있었습니다. claude나 codex를 띄운 pane는 그 대화형 프로세스가 화면을 점유합니다. 그래서 같은 pane에서 서버를 구동하거나 테스트를 돌릴 수 없습니다. 에이전트를 종료해야 그 셸로 돌아옵니다.
그래서 window를 두 개로 나눴습니다.
aiwindow: fe/be/infra 3분할, 각 pane에서 claude나 codex 실행runwindow: 같은 3분할이지만 그냥 셸, 서버 구동과 테스트 실행용
컴포넌트마다 "에이전트 자리 + 실행 자리"가 한 쌍씩 생기는 구조입니다. Ctrl-b로 두 window를 탭처럼 오가면 됩니다. 두 window의 pane는 서로 독립된 셸이라, ai에서 도는 에이전트와 run에서 도는 서버가 간섭하지 않습니다.
4. start-dev.sh
여기까지를 스크립트 하나로 묶었습니다. 도구는 argv로 받아서, claude를 일괄로 켤지 codex를 일괄로 켤지 실행할 때 정합니다.
#!/usr/bin/env bash
set -euo pipefail
SESSION="dr"
BASE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TOOL="${1:-claude}"
# 세션 종료 서브커맨드
if [[ "$TOOL" == "kill" ]]; then
tmux kill-session -t "$SESSION" 2>/dev/null && echo "세션 종료됨." || echo "세션 없음."
exit 0
fi
case "$TOOL" in
claude|codex|none) ;;
*) echo "알 수 없는 도구: '$TOOL'" >&2; exit 1 ;;
esac
# 이미 세션이 있으면 그냥 붙기
if tmux has-session -t "$SESSION" 2>/dev/null; then
exec tmux attach-session -t "$SESSION"
fi
BE="$BASE/back-end"
INFRA="$BASE/infrastructure"
FE="$BASE/front-end"
# window를 3분할(백엔드 좌상 / 인프라 우 / 프론트 좌하)하고,
# 각 pane에서 cmd가 있으면 실행한다.
split_three() {
local win="$1" cmd="$2"
local p_be p_infra p_fe
p_be="$(tmux list-panes -t "$win" -F '#{pane_id}')"
p_infra="$(tmux split-window -h -t "$p_be" -c "$INFRA" -P -F '#{pane_id}')"
p_fe="$(tmux split-window -v -t "$p_be" -c "$FE" -P -F '#{pane_id}')"
tmux select-pane -t "$p_be" -T "1 back-end"
tmux select-pane -t "$p_infra" -T "2 infrastructure"
tmux select-pane -t "$p_fe" -T "3 front-end"
if [[ -n "$cmd" ]]; then
tmux send-keys -t "$p_be" "$cmd" C-m
tmux send-keys -t "$p_infra" "$cmd" C-m
tmux send-keys -t "$p_fe" "$cmd" C-m
fi
tmux select-pane -t "$p_be"
}
if [[ "$TOOL" == "none" ]]; then CMD=""; else CMD="$TOOL"; fi
# pane 경계에 이름 표시
tmux set -g pane-border-status top
tmux set -g pane-border-format " #{pane_title} "
# window 0: ai (도구 실행)
tmux new-session -d -s "$SESSION" -n ai -c "$BE"
split_three "$SESSION:ai" "$CMD"
# window 1: run (셸만)
tmux new-window -t "$SESSION" -n run -c "$BE"
split_three "$SESSION:run" ""
tmux select-window -t "$SESSION:ai"
exec tmux attach-session -t "$SESSION"
사용법은 이렇습니다.
./start-dev.sh # ai window에서 claude 일괄 실행 (기본값)
./start-dev.sh codex # ai window에서 codex 일괄 실행
./start-dev.sh none # 도구 없이 셸만 열기
./start-dev.sh kill # 세션 종료
pane를 위치가 아니라 고유 id(p_be, p_infra, p_fe)로 잡았기 때문에, tmux의 pane 번호 재정렬에 휘둘리지 않고 항상 같은 레이아웃이 나옵니다. 이미 세션이 있으면 새로 만들지 않고 attach만 하므로, 실수로 두 번 실행해도 안전합니다.
5. copy-mode를 vi로 바꾸고 클립보드와 연동
tmux의 스크롤 모드는 사실 copy-mode입니다. 스크롤과 텍스트 선택, 복사가 한 세트입니다. 그런데 기본값은 emacs 키맵이라, vi에 익숙하다면 손에 안 맞습니다.
여기서 한 가지 자주 겪는 문제가 있습니다. 좌우로 나뉜 pane에서 터미널 드래그로 선택하면, 터미널은 pane 경계를 모르기 때문에 화면의 가로줄 전체를 잡습니다. 옆 pane 내용까지 딸려 옵니다. copy-mode는 pane 단위로 동작하므로 이 문제가 없습니다.
WSL이라면 clip.exe로 선택 내용을 윈도우 클립보드까지 넘길 수 있습니다. ~/.tmux.conf에 아래를 넣었습니다.
# copy-mode: vi 키맵 + WSL(clip.exe) 클립보드 연동
setw -g mode-keys vi
bind -T copy-mode-vi v send -X begin-selection
bind -T copy-mode-vi y send -X copy-pipe-and-cancel "iconv -f UTF-8 -t UTF-16LE | clip.exe"
bind -T copy-mode-vi Enter send -X copy-pipe-and-cancel "iconv -f UTF-8 -t UTF-16LE | clip.exe"
# 마우스 드래그 선택 → 놓으면 클립보드로 복사 (pane 경계 인식됨)
set -g mouse on
bind -T copy-mode-vi MouseDragEnd1Pane send -X copy-pipe-and-cancel "iconv -f UTF-8 -t UTF-16LE | clip.exe"
여기서 clip.exe에 바로 파이프하지 않고 iconv를 한 단계 거친 이유가 있습니다. 처음에는 copy-pipe-and-cancel "clip.exe"로만 두었는데, 영어는 멀쩡한데 한글만 아래처럼 깨졌습니다.
?��? ?�스??ABC 123
원인은 인코딩이었습니다. tmux는 선택 내용을 UTF-8로 넘기는데, clip.exe는 이 바이트를 UTF-8로 해석하지 못합니다. 그래서 멀티바이트인 한글만 깨지고 ASCII는 살아남은 것입니다. iconv -f UTF-8 -t UTF-16LE로 UTF-16LE로 변환해 넘기면 clip.exe가 올바르게 받아들여, 한글도 깨지지 않고 복사됩니다. iconv는 리눅스에 기본으로 있어 따로 설치할 것도 없습니다.
이제 Ctrl-b [로 copy-mode에 들어가 v로 선택하고 y를 누르면, pane 내용만 정확히 잡혀서 윈도우 어디서나 Ctrl+V로 붙습니다. 한글도 깨지지 않습니다. 마우스로 드래그해도 pane 단위로 선택되고, 손을 떼는 순간 클립보드로 복사됩니다.
vi copy-mode에는 반 페이지 스크롤이 기본으로 들어 있습니다. Ctrl-u와 Ctrl-d가 반 페이지, Ctrl-b와 Ctrl-f가 한 페이지, g와 G가 맨 위와 맨 아래입니다.
마우스 휠도 매끄럽게 넘어가도록 바인딩을 추가했습니다.
# 셸에서 휠 위로 → copy-mode 진입 후 3줄씩 스크롤,
# less/vim 같은 프로그램 안에서는 휠을 그대로 전달
bind -n WheelUpPane if-shell -F -t = "#{mouse_any_flag}" "send -M" "if -Ft= '#{pane_in_mode}' 'send -X scroll-up' 'copy-mode -e; send -X -N 3 scroll-up'"
bind -n WheelDownPane if-shell -F -t = "#{mouse_any_flag}" "send -M" "if -Ft= '#{pane_in_mode}' 'send -X -N 3 scroll-down' 'send -M'"
bind -T copy-mode-vi WheelUpPane send -X -N 3 scroll-up
bind -T copy-mode-vi WheelDownPane send -X -N 3 scroll-down
6. 방향키 대신 이름과 위치로 이동
Ctrl-b 다음에 방향키로 pane를 오가는 것이 은근히 불편합니다. window도 번호(Ctrl-b 0, 1)보다 이름으로 가는 편이 직관적입니다. 그래서 단축키를 잡았습니다.
# window 이동 (이름 단축키)
bind a select-window -t ai
bind s select-window -t run
# pane 이동 (위치 단축키, 방향키 대신)
bind b select-pane -t '{top-left}' # back-end
bind i select-pane -t '{top-right}' # infrastructure
bind f select-pane -t '{bottom-left}' # front-end
이제 Ctrl-b a와 Ctrl-b s로 window를, Ctrl-b b/i/f로 pane를 한 번에 옮깁니다. pane는 제목이 아니라 위치({top-left} 등)로 잡았습니다. tmux의 pane 제목은 표시용이라 이동 타깃으로 쓸 수 없기 때문입니다. 레이아웃이 고정돼 있으니 좌상은 백엔드, 우측은 인프라, 좌하는 프론트로 항상 일치합니다.
기억이 안 날 때 쓰는 기본 기능도 있습니다. Ctrl-b q를 누르면 각 pane에 번호가 크게 뜨고, 그 숫자를 누르면 바로 점프합니다. Ctrl-b w는 window와 pane를 트리로 보여줍니다.
한 가지 헷갈리기 쉬운 점이 있습니다. 방향키로 pane를 옮기다 보면 pane 크기가 제멋대로 바뀔 때가 있는데, 이건 버그가 아니라 tmux의 기본 크기 조정 기능이 발동한 것입니다. Ctrl-b에 이어 방향키를 한 번 누르면 pane 이동이지만, 방향키를 짧은 시간 안에 연타하면 크기 조정으로 넘어갑니다. tmux의 크기 조정 바인딩은 repeat 속성이 있어서, prefix를 다시 누르지 않아도 연속으로 먹기 때문입니다.
Ctrl-b ↑/↓/←/→ # pane 이동
Ctrl-b Ctrl-↑/↓/←/→ # 1칸씩 크기 조정 (repeat)
Ctrl-b Alt-↑/↓/←/→ # 5칸씩 크기 조정 (repeat)
레이아웃이 틀어졌다면 프리셋으로 되돌리면 됩니다. Ctrl-b Alt-1은 좌우 균등, Ctrl-b Alt-2는 상하 균등, Ctrl-b Space는 다음 레이아웃으로 순환합니다. 아니면 ./start-dev.sh kill 후 다시 실행하면 스크립트가 만든 초기 레이아웃으로 돌아옵니다.
7. 에디터와의 분업
tmux와 에디터는 경쟁이 아니라 분업입니다. 에디터(VSCode나 PyCharm)는 코드 읽기와 쓰기, 리팩터링, 디버거를 맡고, tmux는 서버 구동과 로그 감시, 테스트, 에이전트 실행을 맡습니다.
장시간 도는 서버나 에이전트는 에디터 내장 터미널이 아니라 tmux에 두는 편이 낫습니다. 에디터를 껐다 켜도 tmux 세션은 살아 있어서 서버가 죽지 않기 때문입니다. 에디터 내장 터미널은 그때그때 짧은 명령용으로 쓰면 됩니다.
VSCode와 PyCharm은 넓게 볼지 깊게 볼지로 나눠 생각하면 편합니다. 파이썬과 TS, 인프라 코드가 섞인 폴리글랏 구조에서는 루트를 한 창에 열어 두는 VSCode가 마찰이 적습니다. 반면 백엔드 파이썬을 정밀하게 팔 때(대규모 리팩터링, 까다로운 디버깅, DB 스키마 작업)는 PyCharm이 진가를 냅니다. 백엔드만 PyCharm으로 따로 열고 나머지는 VSCode와 tmux로 굴리는 조합도 흔합니다.
마치며
이번 구성의 핵심은 두 가지였습니다.
첫째, 재현 가능하게 만드는 것입니다. 매번 손으로 window와 pane를 나누는 대신, start-dev.sh 하나로 항상 같은 레이아웃을 띄웁니다. 도구는 argv로 받아서 claude든 codex든 실행 시점에 고릅니다.
둘째, 자리를 나누는 것입니다. 에이전트가 화면을 점유한다는 제약을 ai와 run window로 풀었고, copy-mode와 이동 단축키를 손에 맞게 다듬어 왔다갔다 하는 비용을 줄였습니다.
tmux는 익숙해지면 세션을 껐다 켜도 작업이 그대로 이어진다는 점이 가장 큰 장점입니다. 여기에 스크립트와 .tmux.conf를 한 번 잡아 두면, 새 프로젝트에서도 폴더 이름만 바꿔 그대로 재사용할 수 있습니다.
감사합니다.