터미널의 복잡한 내부와 현재 한계를 짚고, Jupyter식 프런트엔드·셸 통합·장기 프로세스 관리·데이터플로 추적·영속성 등을 기반으로 단계적으로 구축 가능한 ‘미래의 터미널’ 청사진을 제안한다.
터미널 내부는 엉망진창이다. 그중 많은 것들은 80년대에 누군가 내린 결정을 지금은 바꿀 수 없어서 그대로 남아 있는 것뿐이다. —Julia Evans
인프라를 재설계하려면 이렇게 해야 한다. Rich [Hickey]는 [Clojure를 만들 때] Lisp 위에 대충 뭔가를 얹은 게 아니다. 전체 Lisp를 통째로 옮기듯 설계를 전부 한 번에 움직였다. —Gary Bernhardt
아주아주 높은 수준에서 보면, 터미널은 네 부분으로 구성된다:
위에서 약간은 거짓말을 했다. "입력"은 텍스트만이 아니다. 실행 중인 프로세스에 보낼 수 있는 시그널도 포함된다. 키 입력을 시그널로 변환하는 일은 PTY의 몫이다.
마찬가지로, "출력"도 텍스트만이 아니다. ANSI 이스케이프 시퀀스 스트림으로, 터미널 에뮬레이터가 이를 사용해 풍부한 서식을 표시한다.
나는 터미널로 별난 것들을 좀 한다. 하지만 내가 해볼 수 있는 꼼수의 양은 꽤 제한적이다. 터미널 자체가 제한적이기 때문이다. 그 제한점들을 모두 나열하진 않겠다. 이미 여러번재탕되었으니까. 대신, 더 나은 터미널이 어떤 모습일지 상상해보려 한다.
대부분의 사람들이 터미널의 유사물로 가장 익숙한 것은 Jupyter Notebook일 것이다. 이는 "전통적인" VT100 에뮬레이터에서는 불가능한 많은 멋진 기능을 제공한다:





Jupyter는 "커널"(이 경우 파이썬 인터프리터)과 "렌더러"(브라우저가 표시하는 웹 애플리케이션)로 동작한다. 셸을 커널로 쓰는 Jupyter Notebook을 상상해볼 수도 있다. 그러면 셸 명령을 실행할 때 Jupyter의 좋은 기능들을 모두 누릴 수 있을 것이다. 하지만 곧바로 몇 가지 문제에 부딪힌다:
vi나 top을 실행할 생각은 아예 접어야 한다.rm -rf 같은 명령이 포함되지 않기에 "모두 다시 실행"은 그나마 낫다).알고 보면 이 모든 문제는 해결 가능하다.
오늘날 Warp라는 터미널이 있다. Warp는 터미널과 셸 사이에 네이티브 통합을 구축했다. 터미널이 각 명령의 시작과 끝, 그 출력, 그리고 당신이 입력한 내용을 이해한다. 그 결과, 아주 보기 좋게 렌더링할 수 있다:

이는 (대부분) 터미널과 셸에 내장된 표준 기능(커스텀 DCS)을 사용해서 구현된다. 설명은 여기에서 읽을 수 있다. OSC 133 이스케이프 코드를 사용해 더 비침습적으로도 할 수 있다. Warp가 왜 그렇게 하지 않았는지는 모르겠지만, 괜찮다.
iTerm2도 비슷한 일을 한다. 그 덕에 정말 많은 기능을 제공할 수 있다: 단축키 하나로 명령 간을 탐색하기; 명령이 끝나면 알려주기; 출력이 화면 밖으로 넘어가면 현재 명령을 "오버레이"로 보여주기.
이건 사실 세 가지 문제다. 첫째는 오래 사는 프로세스와 "상호작용"하는 것. 둘째는 프로세스를 죽이지 않고 "일시중단"하는 것. 셋째는 프로세스 상태를 건드리지 않으면서 "연결을 끊었다가" 필요할 때 다시 연결하는 것이다.
프로세스와 상호작용하려면 양방향 통신이 필요하다. 즉, "셀 출력"이 동시에 입력이 되어야 한다. 예를 들어 top, gdb, vim 같은 모든 TUI가 그렇다1. 다행히 Jupyter는 이 부분을 정말 잘한다! 설계 전체가 대화형 출력을 중심으로, 이를 변경하고 갱신할 수 있게 되어 있다.
덧붙여, Matklad가 A Better Shell에서 설명한 것처럼, 터미널에는 항상 "자유 입력 셀"이 있기를 기대한다. 창의 위쪽 절반에는 대화형 프로세스가, 아래쪽 절반에는 입력 셀이 있는 형태다. Jupyter도 오늘날 이걸 할 수 있지만, "셀 추가"가 자동이 아니라 수동이다.
프로세스를 "일시중단"하는 것은 보통 "작업 제어(job control)"라고 부른다. 여기서 더 말할 건 많지 않다. 다만, "현대적인" 터미널이라면 모든 일시중단/백그라운드 프로세스를 덜 강조된(pale) 지속적 비주얼로 보여주길 기대한다. 마치 IntelliJ가 하단 작업 표시줄에 "indexing ..."을 보여주는 것처럼.

터미널 세션의 연결을 끊었다가 다시 연결하는 기존 접근법은 대략 세 가지가 있다(사실 reptyr까지 치면 네 가지다).
이 도구들은 터미널 에뮬레이터와 프로그램 사이에 아예 터미널 에뮬레이터 하나를 더 끼워 넣는다. "서버"가 실제로 PTY를 소유하고 출력을 렌더링하며, "클라이언트"가 그 출력을 "진짜" 터미널 에뮬레이터에 표시하는 방식이다. 이 모델 덕분에 클라이언트를 분리(detach)했다가 나중에 재부착(reattach)하거나, 심지어 여러 클라이언트를 동시에 붙일 수도 있다. 배터리 내장형(batteries-included) 접근법이라고 볼 수 있다. 또한 클라이언트와 서버 모두를 프로그램할 수 있고(다만 요즘은 Kitty, Wezterm 같은 많은 터미널이 자체적으로 확장 가능하다), 터미널 안에서 탭과 창을 조직화할 수 있으며(요즘 많은 데스크톱 환경이 타일링과 잘 갖춰진 키보드 단축키를 제공하긴 하지만), 해커맨처럼 보이는 간지(?)도 챙길 수 있다.

단점은, 음, 이제 터미널 안에서 터미널을 하나 더 돌린다는 점이고, 이는 수반되는 모든 버그를 뜻한다.
iTerm은 사실 tmux 클라이언트를 우회하고 스스로 클라이언트가 되어 서버와 직접 통신함으로써 이를 피한다. 이 모드에서는 "tmux 탭"이 실제로는 iTerm 탭이고, "tmux 패널"은 iTerm 패널이며, 등등이다. 좋은 모델이며, 기존 tmux 셋업과의 통합을 위해 미래의 터미널을 만든다면 이 방식을 채택할 것이다.
Mosh는 설계 공간에서 정말 흥미로운 위치에 있다. 터미널 에뮬레이터의 대체가 아니라, _ssh_의 대체다. 가장 큰 장점은 네트워크가 끊긴 뒤에도 터미널 세션에 재연결할 수 있다는 것이다. 서버에서 상태 머신을 돌리고 뷰포트의 증분(diff)만 클라이언트에 재생하는 방식으로 이를 구현한다. tmux와 비슷한 모델이지만, "멀티플렉싱"(터미널 에뮬레이터가 처리할 것으로 기대)이나 스크롤백(이 또한 터미널의 몫)은 지원하지 않는다. 자체 렌더러를 갖고 있어서 tmux와 유사한 종류의 버그가 있다. 다만 tmux와 달리, "클라이언트"가 네트워크의 당신 쪽에서 실제로 동작하기 때문에 로컬 라인 편집이 즉각적이라는 장점이 있다.
이들 모두는 비슷한 설계 지점을 차지한다: 클라이언트/서버로 세션 분리/재개만 처리하고, 네트워킹이나 스크롤백은 다루지 않으며, 자체 터미널 에뮬레이터도 포함하지 않는다. tmux나 mosh에 비해 결합도가 매우 낮다.
이 둘은 해법이 같으므로 함께 다루겠다: 데이터플로 추적(dataflow tracking)이다.
오늘 바로 이걸 하는 예시는 pluto.jl이다. Julia 컴파일러에 훅을 거는 방식으로 구현한다.

보면, 의존하는 이전 셀의 변화에 반응해 셀들이 실시간으로 갱신된다. 반대로, 의존성이 바뀌지 않으면 갱신하지 않는다. 이를 필요한 경우에만 코드를 다시 실행하는, 스프레드시트 같은 Jupyter라고 생각할 수 있다.
이를 일반화하기 어렵다고 말할 수도 있다. 비결은 직교적 영속성(orthogonal persistence)이다. 프로세스를 샌드박스하고, 모든 IO를 추적하며, 샌드박스 안의 다른 프로세스와 통신하는 경우(예: 유닉스 소켓, POST 요청)가 아니면 "너무 이상한" 일은 못 하게 막아라. 그러면 프로세스를 꽤 잘 통제할 수 있다! 덕분에 프로세스를 그 입력의 순수 함수로 취급할 수 있는데, 여기서 입력이란 "전체 파일 시스템, 모든 환경 변수, 모든 프로세스 속성"을 말한다.
이 기본기——Jupyter 노트북 프런트엔드, undo/redo, 자동 다시 실행, 영속성, 셸 통합——가 갖춰지면 그 위에 정말 많은 것을 얹을 수 있다. 게다가 점진적으로, 조각조각 쌓아 올릴 수 있다:
사람들은 말할 것이다. jyn, 오픈 소스에서 수직 통합은 못 만든다. 오픈 소스 프로젝트로는 돈을 벌 수 없다. 전환 비용이 너무 크다.
다 사실이다. 가능한 방법을 말하려면, 점진적 도입 이야기를 해야 한다.
내가 이걸 만든다면 단계를 나눌 것이다. 각 단계에서 대안들보다 나은 무언가가 되도록. jj가 바로 이렇게 동작하며 아주 잘 먹힌다. 팀 전체가 한 번에 전환할 필요가 없다. 개인이 jj를 단일 명령에라도 쓰면서도 다른 모두에게 큰 영향을 주지 않는다.
사람들이 터미널 재설계를 떠올릴 때는 항상 터미널 에뮬레이터 재설계를 생각한다. 정확히 잘못된 출발점이다. 사람들은 자기 에뮬레이터에 애착이 있다. 설정을 만지고, 예쁘게 꾸미고, 키바인딩을 쓴다. 모든 것이 다른 모든 것에 영향을 주기 때문에 에뮬레이터를 바꾸는 전환 비용이 크다. 개인 단위의 전환이라 팀 전체를 건드리는 건 아니니 엄청 높진 않지만, 그래도 높다.
대신 CLI 레이어에서 시작하겠다. CLI 프로그램은 설치와 실행이 쉽고 전환 비용이 매우 낮다. 워크플로 전체를 바꾸지 않고도 단발성으로 쓸 수 있다.
그래서, 터미널을 위한 트랜잭션 의미론을 구현한 CLI를 만들겠다. 대략 transaction [start|rollback|commit] 같은 인터페이스를 상상해보라. start 이후에 실행한 모든 것은 되돌릴 수 있다. 이것만으로도 상당히 많은 걸 할 수 있다. 이걸로만 사업을 만들 수도 있다고 본다.
트랜잭션 의미론이 준비되면, tmux와 mosh에서 영속성을 분리해보겠다.
PTY 영속성을 얻으려면 클라이언트/서버 모델을 도입해야 한다. 커널은 정말정말로양쪽 PTY가 항상 연결되어 있다고 기대하기 때문이다. alden 같은 명령이나, 그와 같은 라이브러리를 사용하면(그렇게 복잡하지도 않다) 터미널 에뮬레이터나 PTY 세션 안에서 실행 중인 프로그램에 영향을 주지 않고 간단히 구현할 수 있다.
스크롤백을 얻으려면, 서버가 입력과 출력을 무기한 저장해 두었다가 클라이언트가 재연결할 때 재생하면 된다. 이렇게 하면 "네이티브" 스크롤백을 얻게 된다. 이미 쓰고 있는 터미널 에뮬레이터가 그걸 다른 출력과 정확히 동일하게 처리한다. 왜냐면 보이는 건 다른 출력과 정확히 같기 때문이다. 동시에 임의 지점부터 재생/재개가 가능하다. 이를 위해서는 ANSI 이스케이프 코드를 어느 정도 파싱해야 한다2. 하지만 충분한 노력이면 가능하다.
mosh 같은 네트워크 재개를 얻으려면, 커스텀 서버가 Eternal TCP를 사용할 수 있다(효율을 위해 가능하면 QUIC 위에). 주목할 점은, PTY의 영속성과 네트워크 연결의 영속성은 분리된다는 것이다. 여기서 Eternal TCP는 엄밀히 말해 최적화다. ssh host eternal-pty attach를 반복 실행하는 bash 스크립트 위에도 구축할 수 있다. 다만 네트워크 지연과 패킷 손실 때문에 경험이 별로일 뿐이다. 다시 말하지만, 합성 가능한 구성 요소는 점진적 도입을 가능케 한다.
이 시점에서 이미 tmux처럼 하나의 터미널 세션에 여러 클라이언트를 붙일 수 있다. 하지만 창 관리는 여전히 클라이언트/서버가 아니라 터미널 에뮬레이터가 담당한다. 만약 창 관리를 통합하고 싶다면, 터미널 에뮬레이터가 tmux -CC 프로토콜을 말하면 된다. iTerm처럼.
이 단계의 모든 부분은 트랜잭션 의미론과 독립적으로, 병렬로 진행할 수 있다. 하지만 이들만으로는 사업을 만들 수준의 개선은 아니라고 본다.
이 부분은 클라이언트/서버 모델에 의존한다. 터미널 에뮬레이터와 클라이언트 사이에 서버를 끼워 넣고 나면, 입출력에 메타데이터를 태깅하는 등의 재미있는 일을 시작할 수 있다. 이렇게 하면 모든 데이터에 타임스탬프를 붙일 수 있고3, 입력과 출력을 구분할 수 있다. xterm.js가 대략 이런 식으로 동작한다. 셸 통합과 결합하면, 데이터 레이어에서 셸 프롬프트와 프로그램 출력을 구분하는 것까지 가능해진다.
이제 정말 재밌는 일을 할 수 있다. 터미널 세션의 _구조화된 로그_가 생겼기 때문이다. 이 로그를 asciinema4처럼 녹화물로 재생할 수 있고; 셸 프롬프트를 모든 명령을 다시 실행하지 않고 변환할 수 있고; Jupyter Notebook이나 Atuin Desktop으로 가져올 수 있고; 명령을 저장해 나중에 스크립트로 다시 실행할 수도 있다. 당신의 터미널은 데이터다.
여기서 처음으로 터미널 에뮬레이터에 손댄다. 의도적으로 마지막 단계에 둔 이유는 전환 비용이 가장 크기 때문이다. 지금까지 만든 멋진 기능들을 활용해 좋은 UI를 제공한다. 중첩 트랜잭션이 필요하지 않다면 더 이상 우리 transaction CLI는 필요 없다. 기본적으로 터미널 세션 전체가 트랜잭션 안에서 시작되니까. 위에서 언급한 기능을 전부 얻게 된다. 모든 조각을 맞췄기 때문이다.
담대하고 야심차다. 전체를 다 짓는 데 10년쯤 걸릴 거라고 생각한다. 괜찮다. 난 느긋하니까.
입소문을 내 도울 수 있다 :) 아마 이 글이 누군가에게 영감을 줘 직접 만들기 시작할지도 모른다.