왜 xterm.js를 버렸나
이날은 내가 만드는 터미널 앱 Agentree의 터미널 엔진을 통째로 바꾼 날이었다.
xterm.js를 Rust 네이티브로 교체했다.
그리고 새 엔진에서 한글 입력이 깨져서 그것도 고쳤다.
하루가 꽤 빡셌다.
Agentree는 Tauri 기반 터미널 앱이다.
macOS에서 웹 뷰 안에 터미널을 그려야 한다.
그래서 xterm.js를 쓰고 있었다.
근데 세 가지 문제가 있었다.
첫째, 한글 입력이 깨졌다.
macOS 웹 뷰의 조합 이벤트가 다른 브라우저랑 달랐다.
조합 시작이랑 끝이 중복으로 발생하고 순서도 어긋났다.
xterm.js가 이걸 제대로 처리 못 했다.
둘째, 패치 의존성이 무거웠다.
xterm.js를 쓰려면 패치 패키지로 수정을 때려 넣어야 했다.
패치 디렉토리에 설치 후 스크립트까지. 유지 비용이 계속 드는 거다.
셋째, 번들이 컸다.
xterm.js랑 애드온 4개, 추가 패키지까지. 가벼워야 하는 앱에서 무거운 의존성이었다.
그래서 xterm.js를 버리기로 했다.
Ghostty 터미널에서 VT 파싱 라이브러리를 가져와서 Canvas2D로 직접 그리는 방식으로 바꿨다.
4단계 마이그레이션
엔진 교체를 4단계로 나눠서 했다.
첫 번째는 백엔드다.
xterm.js의 PTY 처리를 Rust 네이티브로 바꿨다.
portable-pty 크레이트로 프로세스를 관리한다.
Tauri 명령으로 프론트엔드에 노출한다.
두 번째는 프론트엔드다.
xterm.js 캔버스 렌더링을 Canvas2D 직접 그리기로 바꿨다.
VT 시퀀스 파싱은 Ghostty 라이브러리한테 맡긴다.
커서, 스크롤백, 색상 처리를 처음부터 다시 만들었다.
세 번째는 의존성 정리다.
패키지 7개를 지웠다.
xterm.js랑 애드온 4개, 추가 패키지 2개. 패치 디렉토리랑 설치 후 스크립트도 삭제했다.
네 번째는 버그 수정이다.
Canvas2D 전환하면서 생긴 입력 지연, 커서 깜빡임, 스크롤 문제를 잡았다.
새 엔진에서 한글이 안 친다
엔진을 바꾸고 나니 새 문제가 생겼다.
한글이 안 쳐졌다.
원인은 단순했다.
Canvas2D는 그냥 그리는 면이다.
IME 조합 이벤트를 받지 않는다.
xterm.js는 DOM 기반이라 이벤트를 받았는데, Canvas2D로 옮기니까 이벤트가 안 온다.
한글은 조합 입력이다.
여러 키를 눌러서 한 글자를 만든다.
조합 과정에서 조합 시작, 조합 갱신, 조합 끝 이벤트가 발생한다.
이걸 받아서 처리해야 하는데, Canvas2D는 이걸 안 받는다.
숨겨진 텍스트 영역
첫 번째 해결은 숨겨진 텍스트 영역 패턴이었다.
보이지 않는 텍스트 영역을 캔버스 위에 올린다.
이 영역이 IME 입력을 받는다.
받은 입력을 터미널로 전달한다.
웹 뷰가 캔버스에 조합 이벤트를 안 보내니까, 텍스트 영역을 통해서 우회하는 거다.
조합 상태 플래그랑 이벤트 가드도 추가했다.
Rust 네이티브 키보드 모듈
최종 해결은 Rust 네이티브 키보드 입력 모듈이었다.
Ghostty의 아키텍처를 본떠서 입력과 파서와 렌더러를 분리했다.
입력을 Rust에서 직접 처리한다.
웹 뷰에 의존하지 않는다.
웹 뷰의 조합 이벤트 문제를 아예 우회한 거다.
키보드 입력을 네이티브에서 받아서 VT 파서에 넘기고 렌더러가 그린다.
각 채널이 독립적으로 동작한다.
한글 입력 문제의 역사
이 한글 입력 문제는 프로젝트 전체에 걸친 싸움이었다.
이날까지 9개 문제를 해결했다.
xterm.js의 키 처리 순서 문제, 웹 뷰의 중복 조합 이벤트, 조합 후 글자 중복, 조합 후 스페이스바 안 됨, 마지막 글자 안 그려짐, 음절 누락 버그, 교체 텍스트 처리, 백스페이스 조합 편집, 조합 중 발생하는 229 키코드 필터링까지. 한글 입력은 단순해 보이지만 구현이 어렵다.
조합 상태를 정확히 추적해야 하고 플랫폼마다 이벤트가 다르다.
이날 엔진을 바꾸면서 근본적으로 해결한 거다.
웹 뷰에 의존하지 않으니까, 웹 뷰의 이벤트 문제가 더 이상 영향을 안 준다.
마무리
엔진을 통째로 바꾸는 건 큰 결심이었다.
근데 결과는 좋았다.
의존성 7개를 지웠고 패치 부채를 없앴고 한글 입력을 근본적으로 해결했다.
가장 큰 교훈은 “문제의 근원을 바꾸는 게 더 낫다”는 거다.
xterm.js 안에서 한글 입력을 고치려면 계속 패치를 때려 넣어야 한다.
근데 엔진을 바꾸면 그 문제 자체가 사라진다.
웹 뷰에 의존하지 않으니까. 물론 새 엔진이 새 문제를 만들었다.
Canvas2D는 IME 이벤트를 안 받는다.
근데 이건 Rust 네이티브 모듈로 해결할 수 있었다.
새 문제가 이전 문제보다 해결하기 쉬웠다.
문제를 고칠 때, 표면을 고칠지 근원을 바꿀지 선택해야 한다.
표면을 고치면 빠르지만 문제가 다시 나타난다.
근원을 바꾸면 느리지만 문제가 사라진다.
이날은 근원을 바꾸는 선택을 했고 그 선택이 맞았다.