
Webview를 그리는 프로토콜을 내장한 터미널 에뮬레이터를 시작하며 발견한 과제점
요약
터미널 내에 Webview를 인라인으로 임베딩할 수 있는 확장 프로토콜을 탑재한 터미널 에뮬레이터 PoC 개발 사례를 소개합니다. AI 에이전트의 시각적 콘텐츠 출력과 TUI 내 웹 앱 활용 가능성을 탐색하며, 구현 과정에서의 아키텍처와 과제점을 다룹니다.
핵심 포인트
- 터미널 내 인라인 Webview 임베딩을 통한 그래피컬 콘텐츠 표시 가능
- OSC 프로토콜과 IPC를 활용한 터미널-Webview 간 양방향 통신 구현
- AI 에이전트의 마크다운 설계서 및 HTML 목업 프리뷰 활용 기대
- 터미널 에뮬레이터, Webview 호스트, 브라우저 엔진의 3계층 아키텍처
리포지토리는 다음과 같습니다.
시작하며
제목에서 알 수 있듯이, Webview를 터미널 내에 임베딩할 수 있는 확장 프로토콜을 탑재한 터미널 에뮬레이터를 시제품으로 만들어 보았습니다.
iTerm2 등의 Webview 탭과는 달리, 터미널 앱 내에 인라인(inline)으로 Webview를 임베딩할 수 있는 것이 특징입니다. 따라서 예를 들어 TUI 앱 내에서 고도의 그래프 그리기 작업만 Webview가 담당하게 하는 부분적인 사용도 가능하며, 아래와 같이 TUI 전체를 Webview로 만듦으로써 기존의 Web 앱을 그대로 터미널에서 사용하는 것도 가능합니다.

TUI로 작성한 미니 브라우저
이번 시도의 배경에는 개인적인 수요가 크지만, 그 외에도 AI에 의해 터미널을 이용하는 시간과 그래피컬한 콘텐츠를 리뷰할 기회가 늘어나고 있어 이전보다 수요가 높아지고 있지 않을까 생각했기 때문입니다. 여기서 말하는 그래피컬한 콘텐츠란, 예를 들어 AI가 작성한 마크다운 (Markdown) 형식의 설계서, HTML을 이용한 목업 (Mockup) 디자인의 프리뷰 등을 의미합니다. 이것들을 전용 로컬 서버를 구축하지 않고도 표시할 수 있습니다.
또한, IPC를 통해 Webview와 터미널 앱 간의 양방향 통신도 가능하기 때문에, IPC를 확장하면 AI 에이전트와 Webview의 연동을 더욱 강화할 수 있을 것으로 예상됩니다.
이러한 이유로 작성한 PoC(Proof of Concept)이지만, 제작 과정에서 몇 가지 과제점도 보였습니다. 이 기사에서는 우선 아키텍처(Architecture)에 대해 해설하고, 그 후 과제점을 정리해 나가고자 합니다.
아키텍처 해설
전체 구성 요소와 통신 경로는 다음과 같습니다.
구성 요소와 책임
이 프로토콜을 구현하는 터미널 에뮬레이터에는 그림과 같이 크게 3가지 책임이 발생합니다.
터미널 에뮬레이터 (VT 계층) — 통상적인 단말 처리에 더해 OSC 5379를 해석하고, 마운트 요청을 Webview 호스트로 전달합니다. 마운트 위치는 시퀀스 도달 시점의 커서 위치로부터 결정되기 때문에, 표시 제어만은 통신 소켓이 아닌 PTY의 바이트 스트림 (Byte stream)을 통과시켜야 합니다. 이것이 표시 제어 (OSC)와 통신 제어 (소켓)를 분리하는 이유입니다.
Webview 호스트 — Webview의 등록 (handle 발행), 라이프사이클 (Lifecycle) 관리, 앱과 페이지 간의 메시지 라우팅 (Routing)을 담당합니다. 렌더링 결과를 매 프레임마다 에뮬레이터의 합성 처리로 전달해야 하기 때문에, orzma에서는 에뮬레이터와 동일한 프로세스에 통합했습니다 (후술할 과제 1은 이 통합에서 기인합니다).
Webview — 브라우저 엔진 (orzma에서는 CEF)의 인스턴스입니다. 페이지에는 window.orzma 브릿지가 주입되며, 이를 통해 터미널 앱과 통신합니다.
터미널 앱 입장에서 보면 「소켓을 통해 콘텐츠를 등록하여 handle을 받고, OSC로 마운트하며, 이후에는 브릿지를 통해 페이지와 대화한다」는 흐름이 됩니다.
마운트 위치와 스크롤 추종
Webview는 커서 위치를 기점으로 한 셀 직사각형(Cell rectangle)에 마운트됩니다. 이때 통상 화면에서는 스크롤백(Scrollback) 상의 행에 연결하기 때문에, Webview는 텍스트와 함께 스크롤됩니다. 반면 alt 스크린 (전체 화면 TUI)에서는 화면 좌표에 고정됩니다.
렌더링 합성
Webview 호스트는 브라우저의 렌더링 결과를 오프스크린 렌더링 (Off-screen rendering)으로 텍스트로 받아, 터미널 에뮬레이터가 그것을 셀 그리드 (Cell grid)로 합성합니다. 겹침 순서는 「Webview → 셀의 배경색 → 문자」로 하며, 기본 배경색인 셀은 투명하게 취급하는 것이 포인트입니다. TUI 앱은 마운트 영역을 공백으로 두면 Webview가 그대로 보이고, 문자나 배경색을 그리면 Webview 앞에 표시되므로, 추가적인 프로토콜 없이도 앱 측에서 표시 영역을 제어할 수 있습니다.
통신의 안전성
제어 소켓의 위치와 인증 토큰은 각 페인(Pane)의 PTY에 환경 변수로 주입함으로써 앱에 전달합니다. 접속은 토큰에 의해 발행처인 페인에 연결되며, 앱은 자신이 등록한 handle에 대해서만 마운트나 통신을 할 수 있습니다. handle은 추측 불가능한 랜덤 값이며, 등록된 콘텐츠는 handle마다 격리된 orzma://<handle>/ 오리진 (Origin)에서 배포되므로, Webview끼리 서로의 콘텐츠에 접근할 수는 없습니다.
API 인터페이스
Webview의 표시 제어는 OSC 5379를, 통신 제어는 터미널 측에서 동작하는 Webview 호스트에 대해 Unix 도메인 소켓 (Unix Domain Socket)을 이용하여 통신함으로써 실현하고 있습니다.
또한, GitHub에 문서를 정리해 두었으므로 그쪽도 함께 확인해 주시기 바랍니다.
OSC 5379
이 프로토콜은 Webview의 표시/비표시를 제어합니다. Kitty처럼 APC를 사용하고 싶었지만, 내부적으로 alacritty_terminal이라는 크레이트 (Crate)를 이용하고 있으며, APC용 파싱 (Parsing)을 확장하는 API가 없었기 때문에 우선 OSC를 확장하는 방향으로 타협했습니다.
APC로 구현할 경우에는 독자적인 VT 에뮬레이터 (VT Emulator)를 구현하는 방향이 될 것 같습니다.
Webview는 시퀀스 (Sequence)에 직접 매립하는 방식이 아니라, 호스트 서버에 사전에 Webview를 등록해 두고, 반환된 핸들 (Handle, 후술할 OSC 5379의 view_id로 지정하는 식별자)을 이용하여 그립니다.
mount
OSC 5379 ; mount ; <view_id> ; <rows> ; <cols> [ ; <instance_id> ] ST
커서 위치를 기점으로 지정된 view_id에 대응하는 Webview를 그립니다.
rows, cols로 크기도 지정할 수 있습니다. 또한, 동일한 view_id의 Webview를 여러 개 표시할 경우에는 instance_id 지정이 필요합니다.
unmount
OSC 5379 ; unmount [ ; <view_id> [ ; <instance_id> ] ] ST
지정된 view_id (+ instance_id)의 Webview를 언마운트 (Unmount)하는 프로토콜입니다.
IPC
터미널 앱과 Webview 간에 양방향으로 통신하기 위한 인터페이스입니다. 앱과 호스트 간은 Unix 도메인 소켓, 호스트와 페이지 간은 CEF의 프로세스 간 통신 (IPC)을 통해 중계되며, 페이지 측에는 window.orzma라는 브릿지 (Bridge) API가 주입됩니다.
앱 ─ 도메인 소켓 (NDJSON) ─ 호스트 ─ CEF IPC ─ Webview (window.orzma)
앱 → 호스트
op | 역할 |
|---|---|
hello | 핸드셰이크 (Handshake). 접속 후 첫 번째 줄에서 토큰을 보냄 |
register | Webview 콘텐츠를 등록하고 핸들 (Handle)을 발급받음. dir (디렉토리 배포) / inline (HTML 문자열) / url (원격 URL)의 3가지 소스 중에서 지정 가능 |
unregister | 핸들을 해제함. 마운트 중인 Webview도 제거됨 |
reply | 페이지로부터의 call에 대한 응답. reqId로 대응시킴 |
emit | 페이지로 이벤트를 보냄. window.orzma.on으로 수신됨 |
focus | 마운트된 Webview로 포커스를 이동 (null로 해제) |
navigate | Webview를 뒤로 가기 / 앞으로 가기 / 새로고침 / URL 전환 시킴 |
호스트 → 앱
op | 역할 |
|---|---|
call | 페이지의 window.orzma.call(method, params) 호출. reqId와 함께 도착하며, 앱은 reply로 응답함 |
event | 페이지의 window.orzma.emit(event, payload). 응답이 필요 없는 단방향 이벤트 |
compositing | Webview가 실제로 그려졌음 / 파기되었음을 알림 |
참고로 register에 대한 응답만은 op를 가지지 않는 {"ok":true,"handle":"…”} 형식으로 도착합니다. 또한, 앱 측과 페이지 측 양측 모두에 emit이 있지만 방향이 다르며, 페이지의 emit은 앱에 op:"event"로 도착합니다.
Webview 측 API (window.orzma)
| 메서드 | 역할 |
|---|---|
on(event, handler) / off(event, handler) | 앱으로부터의 emit을 구독 / 해제함 |
emit(event, payload?) | 앱으로 일방향 이벤트를 보냄 |
call(method, params?) | emit과 달리, 앱 측에서 반환값(return value)을 돌려줄 수 있음 |
SDK
Ratatui용으로 ratatui_orzma라는 SDK를 준비했습니다.
이 SDK에는 Webview를 그리기 위한 Widget이 탑재되어 있습니다.
과제
호스트 서버의 구현 비용
먼저 첫 번째는 호스트 서버의 구현 비용이 높다는 점입니다. orzma에서는 CEF를 사용한 호스트 서버를 구현하고 있지만, 단순한 표시 제어뿐만 아니라 JavaScript와의 IPC 통신 등도 구현해야 합니다.
현재 이 호스트 서버는 orzma용으로 최적화되어 통합되어 있기 때문에, 이 프로토콜을 채택하는 터미널은 독자적인 호스트 서버를 구현해야 하며, 이는 도입 장벽이 상당히 높아 솔직히 현실적이지 않다고 느꼈습니다. 이 문제를 해결하기 위한 방안으로, 호스트 서버를 단일 바이너리로 분리하여 백그라운드에서 동작시키는 방법을 생각할 수 있지만, 렌더링(rendering) 처리에 통신이 필요하므로 오버헤드(overhead) 최적화 등을 고려해야 하는 상황이 발생합니다.
CEF 프레임워크의 바이너리 크기
CEF 프레임워크의 크기가 제법 커서 대략 300MB 정도 됩니다.
외부 멀티플렉서(Multiplexer)를 사용할 수 없음 / 대응 비용 발생
tmux와 같은 멀티플렉서를 사이에 두면, 앱은 tmux 서버가 관리하는 PTY 위에서 동작하며, 그 출력은 일단 tmux 내부의 가상 스크린으로 들어온 뒤 일반 텍스트로서 외부 터미널에 다시 그려집니다. 즉, 앱과 터미널 에뮬레이터 사이에 가상 터미널 계층이 한 층 더 끼어드는 구조가 되며, 이 과정에서 tmux가 해석할 수 없는 OSC 5379는 폐기되어 orzma까지 도달하지 못합니다.
tmux에는 이스케이프 시퀀스(escape sequence)를 외부 터미널로 그대로 통과시키는 패스스루(passthrough) 메커니즘(allow-passthrough, tmux 3.3 이후는 기본적으로 비활성화)도 있지만, 설령 시퀀스가 도달하더라도 마운트 위치 문제가 남습니다. 마운트는 페인(pane) 내의 커서 위치를 기점으로 하는 반면, orzma에서 보이는 것은 합성된 후의 화면뿐이라서, 페인 분할이나 윈도우 전환, 스크롤에 의해 해당 영역이 어디에 표시되고 있는지 추적할 수 없기 때문입니다. 이는 kitty의 그래픽 프로토콜 등 이미지 계열 프로토콜이 tmux 위에서 겪는 문제와 같은 종류의 것입니다.
현실적인 대응 방안은 컨트롤 모드(tmux -CC)를 이용하는 것이라고 생각합니다. 이는 tmux가 화면을 그리는 대신 텍스트 기반 프로토콜로 클라이언트와 대화하는 모드로, iTerm2의 tmux 통합처럼 터미널 에뮬레이터 자체가 클라이언트가 되어 윈도우나 페인을 네이티브하게 그립니다. 이 구성이라면 터미널 측에서 각 페인의 위치와 내용을 파악할 수 있으므로 Webview도 올바르게 배치할 수 있지만, 제어 프로토콜 구현에 더해 윈도우·페인 관리 자체를 터미널 측에서 다시 구현해야 하므로 구현 비용이 매우 많이 듭니다.
향후 계획
앞으로도 동기부여가 유지되는 한 꾸준히 개발을 진행할 예정입니다.
PoC(Proof of Concept)는 바이브 코딩(vibe coding)을 많이 사용했는데, 솔직히 설계 수준에서는 파악하고 있지만 코드 수준에서는 블랙박스(black box)가 되어 있는 부분이 있어 어느 정도 직접 다시 작성할까 생각 중입니다.
참고로 이름은 변경될 가능성이 있습니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기