Zellij로 터미널 멀티플렉서 쉽게 입문하기
Haze @nebuleto@hackers.pub
밖에서 맥북 에어나 아이패드같은 가벼운 기기에서 SSH를 통해 원격 작업을 해보고 싶어져서 테스트를 해보면서 연결이 끊기면 작업하던 내용이 멈추거나 하는 것들이 불편해 세션 유지 도구가 필요하겠다는 생각이 들었습니다. 하지만 저는 예전부터 tmux를 써보려고 여러 차례 시도했지만 어떻게 해도 손에 익지 않아 금방 포기하게 되었는데, 옆에서 이야기를 들으셨던
@hongminhee洪 民憙 (Hong Minhee) 님에게 추천을 받아서 Zellij를 알게 되었고 시도해보게 되었습니다.
Zellij를 사용해보니 기대했던 것 이상으로 사용 경험이 좋고 커스터마이징 등 설정도 어렵지 않아서 매우 만족하고 있습니다. 그래서 오늘은 Zellij를 소개하고 저의 경험과 설정도 공유해보려고 합니다.
Zellij란?
Zellij은 Rust로 작성된 터미널 멀티플렉서입니다. tmux와 같은 범주의 도구로 터미널 안에서 탭을 만들거나 화면을 분할하고 세션을 유지할 수 있습니다.
tmux와 비교했을 때 제가 느낀 차이는 진입 장벽입니다. Zellij을 처음 실행하면 화면 하단에 현재 모드에서 쓸 수 있는 단축키가 바로 표시됩니다. 설정 파일은 KDL이라는 포맷을 쓰는데 읽거나 작성하는데 크게 어렵지 않습니다. 모드 기반 인터페이스라는 점은 tmux와 비슷하지만, 뭘 눌러야 하는지 화면에서 바로 알 수 있어서 문서를 뒤질 필요가 적습니다.
Zellij는 여러 패키지 매니저를 지원하고 Cargo를 이용해 직접 빌드하는 것도 가능합니다. Homebrew가 설치된 macOS 환경이라면 한 줄이면 됩니다.
brew install zellij
키 바인딩과 머슬 메모리
저는 터미널 에뮬레이터에서 제공하는 기능들을 적극적으로 활용하는 편입니다. Ghostty는 자체적으로 탭 관리, 화면 분할, 검색 등 많은 기능을 내장하고 있습니다. 그래서 처음엔 Zellij를 테스트해볼 때, 이런 서로 중복되는 기능을 어떻게 처리할지에 대한 고민도 있었습니다. Ghostty가 앞에서 해당 키를 처리하고 Zellij에겐 해당 이벤트가 전달되지 않겠지만, 같은 단축키로 바인딩하면 어느 쪽의 기능을 써야할지 모호해집니다.
제가 시도한 방법은 터미널 에뮬레이터는 계속 Ghostty를 쓰지만, 이러한 기능은 모두 Zellij에게 위임하는 방향을 선택했습니다. 하지만 Ghostty의 단축키 바인딩을 그대로 써서 단축키에 익숙해지는데 시간을 들이지 않고 싶었습니다. Ghostty에서 unbind 처리를 해주면 모든 단축키 입력도 Zellij까지 전달되어, Zellij에서 이를 받아서 처리하는 형태가 되었습니다. 의외로 설정은 어렵지 않아서 바로 손에 익을 수 있었습니다.
Ghostty의 키 바인딩 해제하기
# ~/.config/ghostty/config
# Zellij에게 넘길 키 바인딩을 전부 해제
keybind = cmd+t=unbind # 새 탭
keybind = cmd+n=unbind # 새 탭
keybind = cmd+w=unbind # 닫기
keybind = cmd+d=unbind # 세로 분할
keybind = cmd+shift+d=unbind # 가로 분할
keybind = cmd+f=unbind # 검색
keybind = cmd+c=unbind # 복사
keybind = cmd+k=unbind # 화면 지우기
keybind = cmd+[=unbind # 이전 Pane
keybind = cmd+]=unbind # 다음 Pane
keybind = cmd+1=unbind # 탭 1~9 전환
keybind = cmd+digit_1=unbind
keybind = cmd+2=unbind
keybind = cmd+digit_2=unbind
# ... cmd+9, cmd+digit_9까지 동일
# macOS의 Option 키를 Alt로 다루기 위한 설정
macos-option-as-alt = true
원래부터 있던 폰트나 테마 등의 Ghostty 설정은 수정하지 않았습니다.
익숙한 단축키를 그대로 Zellij에서 쓰기
Ghostty에서 풀어준 키를 Zellij 쪽에서 받습니다. 여기서 제가 신경 쓴 건 하나로, 기존 Ghostty에서의 머슬 메모리를 그대로 유지하는 것입니다.
// ~/.config/zellij/config.kdl
keybinds clear-defaults=false {
normal {
// Split — Ghostty와 같은 단축키
bind "Super d" { NewPane "Right"; SwitchToMode "normal"; }
bind "Super Shift d" { NewPane "down"; SwitchToMode "normal"; }
// 탭
bind "Super t" { NewTab; }
bind "Super n" { NewTab; }
bind "Super w" { CloseFocus; }
// Pane 이동 — Ghostty와 같은 단축키
bind "Super [" { FocusPreviousPane; }
bind "Super ]" { FocusNextPane; }
// 탭 전환 — Ghostty와 같은 단축키
bind "Super 1" { GoToTab 1; SwitchToMode "normal"; }
bind "Super 2" { GoToTab 2; SwitchToMode "normal"; }
// ... Super 9까지 동일
// 검색, 복사, 화면 지우기
bind "Super f" { SwitchToMode "entersearch"; }
bind "Super c" { Copy; }
bind "Super k" { Clear; Write 10; }
}
}
Cmd + D로 세로 분할, Cmd + Shift + D로 가로 분할. Cmd + F로 검색. Cmd + T나 Cmd + N으로 새 탭. Cmd + [과 Cmd + ]으로 Pane 이동. Cmd + 1~Cmd + 9로 탭 전환. 전부 Ghostty를 쓸 때와 같습니다. 단축키를 새로 외울 필요가 없습니다.
clear-defaults=false는 Zellij의 기본 키 바인딩을 유지하면서 위에 덮어쓰겠다는 뜻입니다. 다만 일부 기본 키는 다른 도구와 충돌해서 풀어줬습니다.
// Claude Code 등에서 쓰는 Ctrl + O, Ctrl + B와 충돌 방지
unbind "Ctrl o"
unbind "Ctrl b"
unbind "Ctrl s"
unbind "Ctrl q"
추가로 Ghostty는 마우스를 통해 분할된 탭 영역의 사이즈를 바꿀 수 있지만 Zellij는 그렇지 않기 때문에, 영역(Pane)의 크기를 편하게 바꿀 수 있도록 Alt + Shift + Arrow에 바인딩했습니다.
bind "Alt Shift left" { Resize "Increase left"; }
bind "Alt Shift down" { Resize "Increase down"; }
bind "Alt Shift up" { Resize "Increase up"; }
bind "Alt Shift right" { Resize "Increase right"; }
Alt 키 관련 참고 사항
macOS 터미널에서 Alt + Left/Right로 단어 단위 커서 이동을 쓰는 분이 많을 것입니다. Zellij의 기본 키 바인딩 중 Alt f가 이 시퀀스와 겹치는 문제가 있습니다(#3850). 그래서 Workaround로서 Alt f를 unbind하고 다시 bind해서 해결했습니다.
unbind "Alt f"
bind "Alt F" { ToggleFloatingPanes; }
키 바인딩 외 설정
하나의 세션 공유
// config.kdl
session_name "Main"
attach_to_session true
on_force_close "detach"
지금 제 설정은 모든 곳에서 하나의 세션을 공유하는 형태입니다. attach_to_session true로 설정하면 Zellij을 실행할 때 기존 세션이 있으면 그대로 붙습니다. 새 터미널 창을 열어도, 터미널을 껐다 켜도 작업 중이던 화면이 유지됩니다. on_force_close "detach"는 강제 종료 시에도 세션을 죽이지 않고 분리만 해줍니다.
이게 처음 Zellij를 도입한 이유이기도 합니다. 다른 곳에서 터미널을 열거나 SSH로 접속하면 같은 세션을 공유하고 이어서 작업할 수 있습니다. 연결을 끊고 자리를 비웠다가 돌아와도 그대로 잘 돌아가는 것을 확인할 수 있습니다.
zjstatus로 상단 바 꾸미기
Zellij에는 기본 상태 바가 있지만 좀 더 제 취향에 맞게 꾸미고 싶어서 zjstatus를 쓰고 있습니다. 별도 설치는 필요 없습니다. 레이아웃 파일에 wasm URL을 적어두면 Zellij이 처음 실행할 때 사용자에게 확인을 받은 뒤, 알아서 받아옵니다.
// ~/.config/zellij/layouts/default.kdl
layout {
default_tab_template {
pane size=2 borderless=true {
plugin location="https://github.com/dj95/zjstatus/releases/latest/download/zjstatus.wasm" {
// 좌측: 모드 + 세션 이름
format_left "{mode} #[fg=#eceff4,bold]{session}"
// 중앙: 탭 목록
format_center "{tabs}"
// 우측: Git 브랜치 + 시간
format_right "{command_git_branch} {datetime}"
// 모드별 표시
mode_normal "#[bg=#2e3440,fg=#5e81ac,bold] NORMAL "
mode_locked "#[bg=#5e81ac,fg=#2e3440,bold] LOCKED "
mode_resize "#[bg=#b48ead,fg=#2e3440,bold]⇋ RESIZE "
// 탭 표시 — 활성 탭은 bold + italic
tab_normal "#[fg=#4c566a] {index} {name} "
tab_active "#[fg=#d8dee9,bold,italic] {index} {name} "
// Git 브랜치 (10초 간격 갱신)
command_git_branch_command "git rev-parse --abbrev-ref HEAD"
command_git_branch_format "#[fg=#5e81ac,bold] {stdout}"
command_git_branch_interval "10"
// 시간
datetime "#[fg=#2e3440,bg=#d8dee9,bold] {format}"
datetime_format "%Y-%m-%d %H:%M"
datetime_timezone "Asia/Seoul"
}
}
children
// 단축키가 익숙하지 않다면 하단에 단축키 가이드용 기본 상태 바를 추가할 수 있습니다.
pane size=1 borderless=true {
plugin location="zellij:status-bar";
}
}
}
상단 바에는 현재 모드, 세션 이름, 탭 목록, Git 브랜치, 시간이 표시됩니다. 현재 어떤 모드에 있는지, 어떤 브랜치에서 작업 중인지 한눈에 볼 수 있어서 좋습니다. 색상은 Nord 컬러 팔레트를 기준으로 지정했습니다.
지금 제 설정에서는 하단 Zellij 기본 상태 바를 제외했지만, Zellij의 단축키를 가이드해주는게 필요하다면 저렇게 남겨둘 수도 있습니다.
색상은 전부 Nord 팔레트를 따랐습니다. Ghostty에서 OneNord 테마를, zjstatus에서는 Nord 계열 색상 코드를 직접 지정해서 톤을 맞췄습니다.
아쉬운 점: Bell 알림
Zellij을 쓰면서 한 가지 아쉬운 부분은 터미널에서 Bell과 알림 지원이 불완전하다는 것입니다.
현재 활성화된 탭의 영역에서 발생하는 bell은 터미널 에뮬레이터까지 전달되지만, 비활성 탭이나 숨겨진 플로팅 영역에서 발생하는 bell은 해당 영역을 다시 볼 때까지 전달되지 않습니다(#3245). 이 동작에 대한 논의가 #4595에서 진행 중이고, OSC 99(데스크톱 알림 프로토콜) 지원도 아직 구현되지 않은 상태입니다(#3451).
코딩 에이전트 등 TUI를 사용하는 여러 경우에서 작업이 끝나도 알림을 받지 못하는 상황이 생길 수 있습니다.
terminal-notifier를 통한 우회
terminal-notifier를 활용하면 터미널에서 macOS 알림을 직접 보낼 수 있습니다.
brew install terminal-notifier
Claude Code를 사용한다면 Hook을 통해 필요한 시점에 terminal-notifier로 알림을 보내 이 문제를 우회할 수 있습니다. Codex의 경우 아직 Hook 기능을 지원하지 않는데, 최근 이 기능을 개발 중이라는 코멘트가 있습니다. (openai/codex#2109)
결론
어쩌다 보니 터미널 에뮬레이터에 종속되지 않는 환경이 만들어졌습니다. 저는 여전히 Ghostty를 정말 좋아하지만, 이 설정은 Alacritty나 WezTerm 등을 써도 그대로 쓸 수 있습니다. 하지만 그게 이 글의 요점은 아닙니다.
제가 말하고 싶은 건, 이 설정을 통해 Ghostty를 쓸 때와 전혀 다르지 않은 느낌으로 Zellij을 쓸 수 있고 더 이상 터미널 멀티플렉서를 어려워하지 않아도 된다는 것입니다. 탭 영역 분할, Tab, 검색, 탭 혹은 탭 내 영역 간 이동 전부 같은 손동작으로 됩니다. 거기에 세션 유지, floating pane, 커스터마이징 가능한 상태 바까지 얹어집니다. 머슬 메모리를 크게 바꾸지 않으면서도 터미널의 사용 경험이 크게 좋아졌습니다.