나만의 텍스트 에디터를 만들고, 매일 사용하고 있어요

2 weeks ago 8

  • 기존 Howl 에디터의 한계(개발 중단, 느린 검색, SSH 비호환, 터미널 미지원)로 인해 직접 새로운 TUI 텍스트 에디터를 개발함
  • Helix, VS Code, Vim, Neovim, Emacs 등 13개 에디터를 시도했으나 원하는 조작 감각(Fingerspitzengefühl) 을 충족하는 에디터가 없었음
  • 초기에는 개인 맞춤형 최소 기능만 구현하고, 성능·유니코드·다국어 지원 등은 후순위로 두며 점진적으로 확장함
  • 개발 과정에서 자체 정규식 엔진, 파일 브라우저, TUI 기반 렌더링, 터미널 버퍼 통합 등을 직접 구현함
  • 프로젝트 전역 검색, 구문 강조, 캐시 처리, 멀티스레드 작업 분배 등에서 성능 최적화 기법을 다수 적용함
  • 결과적으로 자신만의 워크플로에 완벽히 맞는 도구를 완성하며, 생산성과 프로그래밍에 대한 즐거움을 되찾았다고 밝힘

기존 에디터의 한계와 대안 탐색

  • 약 10년간 사용한 Howl 에디터의 문제점이 직접 개발하게된 계기
    • 수년간 개발이 중단되어 직접 포크를 유지해왔으나, MoonScript로 작성되어 깊이 있는 수정이 어려웠음
    • 프로젝트 전체 파일 검색 성능이 부족해 작업 흐름이 끊기는 문제 발생
    • GUI 에디터여서 SSH 연결을 통한 원격 사용이 불가
    • 통합 터미널이 없어 외부 명령 실행 시 라이브 상호작용 불가, ANSI 이스케이프 코드 대부분 미지원
  • Helix, VS Code, Sublime Text, Vim, Zed, Neovim, Emacs, Geany, Micro, Lite XL, Lapce, GNOME Builder, Kakoune 등 13개 에디터를 시도
    • 각각 장점이 있었으나, 원하는 조작 감각(Fingerspitzengefühl) 을 충족하지 못함
    • Helix를 가장 오래 사용했으나 한 달 후 흥미를 잃음

초기 개발 전략

  • 시작 단계에서 범위를 최소한으로 제한
    • 자신 외의 사용자를 위한 기능 배제, 모든 설정을 하드코딩
    • 성능 최적화는 미루고 String 기반 버퍼로 시작
    • 유니코드 그래핌 완전 지원 배제, £ 기호가 단일 컬럼을 차지하면 충분하다는 판단
    • 구문 강조는 주로 사용하는 소수 언어만 지원, 나머지는 범용 구분자 기반 강조로 대체
  • 두 번째 시도에서 간단한 TUI 프레임워크를 먼저 구축했으나, 시간이 지나며 대부분을 걷어내고 더 직접적이고 세밀한 접근으로 전환

Dogfooding 실천

  • 에디터가 단일 파일을 열고 편집·저장할 수 있는 최소 기능 임계점에 도달한 후 세 가지 실천을 시작
    • nano 대신 자체 에디터를 사용하여 시스템 파일 편집이나 메모 작성 시 강제로 사용
    • 빠진 기능, 버그, 이상 동작, 한계를 발견할 때마다 프로젝트 README.md에 기록
    • 짜증이 날 정도의 문제는 즉시 수정
  • 이 세 가지 실천으로 작업량이 월 1시간에서 주 수 시간으로 증가
  • 전체 약 10,000줄의 코드 중 거의 대부분이 최근 6개월 내에 작성됨

커서 조작

  • 커서 조작은 구현 난이도가 높은 영역
    • ctrl + shift + left 같은 키 조합의 동작은 사용자에게 당연하지만 로직 구현은 복잡
  • 고수준 입력을 원시 연산의 조합으로 구현하는 것이 핵심 조언
    • 예: 단어 단위 백스페이스 → 단어 단위 커서 이동 + 범위 선택 + 삭제로 분해
  • undo/redo 구현 시 이 3개 동작을 하나의 그룹으로 묶어야 직관적 결과 보장
  • 모달 에디터가 이런 원시 연산을 사용자에게 직접 노출하는 이유가 이해됨

파일 브라우저

  • Howl의 파일 브라우저가 다른 에디터로 넘어가지 못한 결정적 이유
    • 즉시 업데이트되는 퍼지 필터가 탁월하여, 1~2 키 입력만으로 원하는 파일 검색이 대부분 가능
    • 파일이 존재하지 않으면 인라인으로 생성 가능
    • ~/ 입력 시 홈 디렉토리로 자동 전환
    • 메인 편집 창에서 열려는 파일의 미리보기 표시
  • 다른 에디터들이 파일 열기 문제를 마우스 의존, GTK 기본 대화상자, 파일명 추측 등으로 해결하는 것에 대한 불만
  • 자체 구현 시 Levenshtein distance 같은 복잡한 방식 대신 단순 기준 3가지로 충분
    • 필터 구문으로 시작하는지 여부
    • 필터 구문을 포함하는지 여부
    • 가장 최근 수정/접근 시간
  • 대소문자 무시 매칭을 허용하되, 대소문자 일치 시 순위를 약간 상향
  • 수만 개 파일이 있는 프로젝트에서도 2 키 입력 후 약 95% 확률로 상위 2개 안에 원하는 파일 위치

정규식 엔진

  • 정규식은 프로젝트 전체 검색, 구문 강조, 버퍼 내 찾기 세 곳에서 사용
  • 기존 regex-automata 크레이트 대신 직접 구현한 이유
    • Rust의 raw string 문법 같은 컨텍스트 민감 엣지 케이스 처리 필요
    • 프로젝트 자체가 자체 스택 구축과 이해를 위한 연습
  • 초기 구현은 파싱 크레이트 chumsky로 정규식 구문을 파싱하고 AST를 매 문자마다 순회하는 느린 방식
  • 이후 단계적 최적화 수행
    • 단일 패스 옵티마이저: 반복 출현하는 문자 매칭 그룹을 단일 String 노드로 변환하여 정확한 문자열 검색 수행
    • 공통 접두사 추출: 예를 들어 hel[(lo)p]에서 공통 접두사 hel을 발견하여 해당 위치에서만 매칭 수행 → 프로젝트 전체 검색에서 큰 성능 향상
    • AST 워커를 Rust 동적 호출 기반의 threaded code VM으로 재구현
    • threaded code VM을 CPS(Continuation-Passing Style) 형태로 변환, 각 VM 명령이 후속 명령을 tail-call하여 컴파일러 최적화 활용
    • Rust의 느린 동적 함수 호출을 vtable 룩업 없이 래핑, 많은 정규식 명령의 코드젠이 몇 개의 기계어 명령으로 축소
    • 가능한 한 많은 정규식 명령을 유니코드 코드포인트 대신 바이트 단위로 구현, UTF-8 설계 덕분에 ASCII 최적화 기법이 멀티바이트 코드포인트에서도 유효
  • jump LUT 체인으로의 컴파일도 시도했으나, 벤치마크 결과 threaded code 대비 20~30% 빠른 정도에 불과하고 유연성이 크게 저하되어 채택하지 않음
  • 최종 결과: Rust용 가장 복잡한 구문 강조에서 50,000줄짜리 자동 생성 바인딩 파일을 클린 상태에서 10밀리초 미만에 전체 강조 완료

구문 강조 캐시

  • 초기에는 매 변경마다 전체 파일을 재강조하는 방식이었으나, 큰 파일에서 성능 저하 발생
  • 온디맨드 토큰 강조 캐시 구현
    • 대략 동일한 크기의 청크 단위로 토큰을 강조
    • 버퍼에 변경(damage)이 발생하면 해당 위치와 겹치거나 이후의 청크만 무효화
  • 가장 비관적인 경우(대형 파일 중간 편집)에도 damage 이전의 강조 상태는 유지되고, 화면 아래쪽 이후는 강조 정보가 요청되지 않아 처리 불필요
  • 수요 기반(demand-driven) 접근이므로, 동일 버퍼의 서로 다른 부분을 보는 복수 패널에서도 정상 동작

프로젝트 전체 검색

  • 검색 프로세스 4단계
    • 현재 디렉토리에서 역방향으로 .git/ 디렉토리를 찾아 프로젝트 루트 결정
    • 프로젝트 루트의 모든 디렉토리를 재귀 순회하며 검색 패턴을 파일 내용에 매칭
    • 각 양성 매칭에서 파일 스니펫을 추출하고 결과 미리보기를 위해 구문 강조 적용
    • 현재 경로로부터의 탐색 거리에 따라 결과 순위 결정 (가까운 파일일수록 높은 순위)
  • 빌드 디렉토리 등을 피하는 기본 필터링 규칙 적용
  • 멀티스레드 처리로, 기본적인 work-stealing 방식으로 스레드 간 작업 할당
    • 모든 스레드가 소비자이자 생산자인 특수한 구조에서 종료 감지 문제 해결
    • 대기 중인 스레드가 원자적 카운터를 증가시키고, 카운터가 워커 수에 도달하고 작업 큐가 비면 전체 종료
  • 정규식 최적화와 현대 SSD 속도 덕분에 Veloren 같은 큰 코드베이스에서도 단순 패턴 검색이 거의 즉시 완료
  • 플레임그래프 상 대부분 IO 바운드 상태
  • 에디터 내에서 대형 코드베이스를 사고 속도로 검색할 수 있는 것이 생산성에 큰 기여

터미널 에뮬레이터 버퍼

  • 패널 기반 에디터에서 하나의 패널을 터미널 창으로 사용하는 기능의 편의성이 큼
  • ANSI 파서를 직접 구현하려 했으나, OSC52, Kitty 키보드 확장 등 최신 터미널 렌더링 기능 지원이 방대
  • alacritty_terminal 크레이트를 활용하여 Alacritty 터미널 에뮬레이터의 이스케이프 시퀀스 파서 및 터미널 상태 관리 로직을 재사용
  • 결과적으로 screen/tmux의 핵심 기능을 대체할 수 있으며, 더 풍부한 이스케이프 시퀀스 지원 제공

렌더링 최적화

  • TUI 기반이지만 원격 모바일 연결 시 대역폭이 여전히 중요
  • 더블 버퍼링: 터미널 화면의 내부 복사본을 이중으로 유지
    • 재그리기 시 이전 프레임과 비교하여 변경된 셀만 ANSI 이스케이프 시퀀스 출력
    • 커서 이동, 스타일 모드 변경 등의 시퀀스도 실제 필요할 때만 출력
  • 대부분의 터미널 에뮬레이터(Ghostty 제외)에서, 에디터의 터미널 패널에서 대형 파일을 cat한 후 에디터를 닫는 것이 호스트 터미널에서 직접 cat하는 것보다 더 빠름
    • alacritty_terminal이 호스트 터미널에 대한 stdout 바이트 처리 비용을 차단해주기 때문

결론: 자신만의 도구를 만들 것

  • 직접 만든 에디터는 내 작업 흐름에 완벽히 맞는 도구로 자리 잡았음
  • 자체 에디터/도구 제작이 무의미한 고통이라는 통념에 반대
  • 네 가지 이점
    • 완벽한 맞춤: 원하는 동작만 수행, 그 이상도 이하도 없음
    • 다양한 기술 학습: 정규식, ANSI, 의사터미널(pty), TUI 디자인, UTF-8의 세부 사항 등 범용적으로 유용한 기술에 대한 깊은 이해 획득
    • 장기적 생산성 향상: 자신의 도구를 완벽히 이해하고 개인 워크플로에 맞춘 기능을 내장하여, 도구와의 마찰 감소
    • 순수한 즐거움: 자기완결적인 문제들을 해결하고 그 결과를 손끝으로 느끼는 경험이 프로그래밍에 대한 사랑을 다시 불러일으킴, 수년 만에 코딩하며 활짝 웃고 혼자 웃음이 나는 경험
  • 텍스트 에디터가 아니어도 좋으니 자신만의 도구를 만들 것을 권장하며, 어려운 부분을 통계 상자(AI 등)에 넘기지 말고 도전 자체를 즐길 것을 강조

Read Entire Article