확장성: ‘100% Lisp’ 오류
Source: Hacker News
소개
그래서 저는 Lisp 언어로 작성된 Emacs‑like 편집기를 홍보하는 몇몇 글을 보았는데, 가장 흔한 주장 중 하나는 다음과 같습니다:
“This Lisp로 작성되었고 This Lisp로 스크립팅할 수 있어 뛰어난 확장성을 제공합니다.”1
틀린 말은 아니지만, 몇 가지를 간과하고 있다고 생각합니다.
그런데: 새해 복 많이 받으세요!
1. 논증
예를 들어, Irreal의 Lem: An Alternative To Emacs? 글에서는 (강조는 그대로 유지) 다음과 같이 주장합니다:
One thing I like about it is that it’s 100 % Common Lisp. There’s no C core; just Lisp all the way down. That makes it easier to customize or extend any part of the editor.
이 주장은 듣기 좋은 것처럼 보입니다. 저장소를 살펴보면 Lem은 대략 90 % Lisp 코드이며, 편집기 코드와 사용자 커스터마이징이 Common Lisp 런타임 안에 존재하므로 어떤 부분이든 즉시 확장할 수 있을 것처럼 보이죠, 맞나요?
그렇다면 정말 그런가요?
composition-function-table을 제공해서 편집기의 스크립트 언어로 폰트 리가처를 직접 프로그래밍할 수 있나요?- 임의의 인코딩 시스템과 해당 문자 집합(
define-charset)을, Unicode나 기존 표준이 지원하는 범위를 넘어 정의할 수 있는 API가 있나요? - 파일을 한 줄에 모두 표시하도록 Emacs의 display tables처럼 줄 바꿈 문자를 “덮어쓸(overridden)” 수 있나요?
…
이 예시들은 Emacs의 다소 희귀한 기능들에서 발췌한 것이며, 무한히 더 나열할 수 있습니다. 이런 기능들은 “100 % Lisp” 시스템의 **비순수(non‑pure) 10 %**에 해당하므로, 대부분의 편집기가 지원하기는 어려울 것이라 생각합니다.
솔직히 말해서, 저는 이런 기능들을 싫어합니다—Emacs 클론을 설계하면서 IPC 프로토콜을 만들기 시작한 이래로 계속해서 저를 괴롭혀 왔거든요. 그런데도 아이러니하게도, 제가 겪은(그리고 아직도 겪고 있는) 어려움은 Emacs의 확장성 때문에 발생했지, 그 확장성이 부족해서가 아닙니다.
2. 100 % Lisp는 없습니다 ¶
예를 들어, Steel Bank Common Lisp—일반적인 Common Lisp 런타임—은 스레드 원시 기능을 제공하고, OS와 인터페이스하며, 어셈블리 코드를 활용해야 하기 때문에 대부분이 Lisp로 작성되어 있지 않습니다. 따라서 이러한 저수준 부분은 사용자 정의가 불가능합니다.
같은 제한은 (그래픽) 편집기에도 적용됩니다. 모든 GUI 프로그램과 마찬가지로 일반적으로 다음을 지원하고 싶을 것입니다:
- 폰트 폴백
- 입력 방식 – 입력 방식에 관한 Wikipedia 기사 참고
- 스크린 리더 – AccessKit 플랫폼 어댑터 설명 참고
이 모든 항목은 플랫폼‑특정 API와의 상호 작용을 필요로 하므로 커스터마이징이 크게 제한됩니다. 외부 함수 인터페이스(FFI)를 사용하면 핵심을 “순수”하게 유지할 수 있지만, 그 경계를 넘어선 확장은 할 수 없습니다:
- Webview는 폰트‑리거처 내부를 노출하지 않으며, CSS가 유일하고 제한된 제어 수단입니다.
- 입력 방식은 키보드 이벤트와 충돌하고 플랫폼‑특정이기 때문에 다루기가 까다롭습니다.
- 스크린 리더는 이식성을 원한다면 전용 라이브러리를 통해 사용하는 것이 가장 좋습니다.
따라서 이러한 플랫폼‑특정 문제들을 모두 처리하는 진정한 “순수” Lisp 프로그램은 현재로서는 사실상 불가능합니다. 이것이 반드시 나쁜 것만은 아니며, 시스템을 얼마나 확장할 수 있는지에 대한 한계를 의미합니다.
그럼에도 불구하고 올바른 빌딩 블록을 갖추면 개발자들은 종종 이러한 “확장 불가능” 부분을 우회하는 놀라운 방법을 찾아냅니다.
3. Workaround‑ish extensibility {#workaround-ish-extensibility}
먼저 사람들이 에디터를 확장하기 위해 사용하는 몇 가지 접근 방식을 살펴보겠습니다:
-
Neovim 및 많은 TUI 에디터는 ANSI가 제공하는 범위에 제한되기 때문에 기본 스크롤바 지원이 없습니다.
extmark와virt_text로 가장 오른쪽 열을 색칠하면 — nvim‑scrollbar를 참고 — Neovim에 예쁜 스크롤바를 표시할 수 있습니다. -
기본 Emacs는 커서 애니메이션을 지원하지 않습니다. 하지만 사람들은 다음과 같은 우회 방법을 찾아냈습니다:
- cursor‑animation patch와 같은 패치를 적용합니다.
- Emacs Lisp를 Python으로 확장합니다(예: holo‑layer “jelly cursor animation”). 이 방법은 PyQt 또는 컴포지터 명령을 호출해 오버레이 창을 제어하고 움직이는 커서를 표시합니다.
-
**Emacs Application Framework (EAF)**는 먼저 PyQt로 GUI 프로그램을 구현한 뒤 해당 PyQt 창을 Emacs 버퍼 창에 삽입함으로써 Emacs용 GUI 프로그램을 작성할 수 있게 해줍니다. 모든 Wayland 컴포지터가 사용자 창을 프로그래밍 방식으로 배치하기 위한 API를 제공하는 것은 아니므로, 이것이 EAF의 제한 요소가 될 수 있습니다.
-
EXWM (Emacs X Window Manager) – GitHub 프로젝트를 참고하세요.
여기서 얻을 수 있는 교훈은 많은 것이 생각보다 더 확장 가능하다는 점입니다. 사람들이 다양한 우회 방법을 계속해서 고안해 내는 것이 놀라울 정도이며, “순수성”과 관계없이 모두가 확장의 한 형태로 간주됩니다.
4. “Spacebar heating” 확장성
우회 방법을 통해 확장하는 것에 비해 “순수 Lisp”로 확장하는 것은 더 쉽기도 하고 더 어려울 수도 있습니다. 코딩 규칙과 기존 코드에 여전히 제약을 받으며, 무언가를 깨뜨리지 않고 모든 것을 확장하는 것은 불가능에 가깝습니다.
단일 함수 오버라이드
Org‑mode 파일을 HTML로 내보낼 때, Org‑mode는 기본적으로 무작위 HTML ID 앵커를 생성합니다. 이를 바꾸려면 ID를 생성하는 함수를 단순히 오버라이드하면 된다고 생각할 수 있습니다:
(advice-add #'org-export-get-reference :around #'org-html-stable-ids--get-reference)
하지만 Org‑mode는 때때로 org-html--reference를 직접 호출하여 오버라이드를 우회합니다. 따라서 내부 함수를 다음과 같이 다시 지정해야 합니다:
(advice-add #'org-html--reference :override #'org-html-stable-ids--reference)
내부 함수와의 문제
관례상 Emacs Lisp 코드에서는 함수 이름에 이중 대시(--)를 사용해 해당 함수가 내부용임을 나타냅니다(예: org-html--reference). 이러한 함수를 오버라이드하는 것은 가능하지만, 매우 깨지기 쉬운 작업이 됩니다. 향후 업데이트가 내부 API를 변경하면 코드가 깨질 위험이 있습니다.
el‑patch 사용
el-patch 패키지는 거의 모든 Lisp 코드에 “패치”를 적용할 수 있게 해 줍니다. 함수 내부 깊숙한 곳까지도 수정할 수 있습니다:
;; Original function
(defun company-statistics--load ()
"Restore statistics."
(load company-statistics-file 'noerror nil 'nosuffix))
;; Patching
(el-patch-feature company-statistics)
(with-eval-after-load 'company-statistics
(el-patch-defun company-statistics--load ()
"Restore statistics."
(load company-statistics-file 'noerror
;; The patch
(el-patch-swap nil 'nomessage)
'nosuffix)))
;; Patched version (what you get after applying the patch)
(defun company-statistics--load ()
"Restore statistics."
(load company-statistics-file 'noerror 'nomessage 'nosuffix))
el-patch는 el-patch-validate도 제공하는데, 이는 패치가 여전히 유효하고 의도치 않게 깨지지 않았는지 확인하는 데 도움을 줍니다. 그럼에도 불구하고, 업스트림 코드가 바뀔 때마다 모든 패치를 유지 관리해야 합니다.
트레이드‑오프
확장 가능한 시스템은 다음과 같은 딜레마에 직면합니다:
- 강한 캡슐화 – 결국 어떤 부분은 사용자 정의가 불가능합니다.
- 완전한 노출 – 유지 관리자는 뒤로 호환성을, 사용자는 앞으로 호환성을 깨뜨릴 위험을 감수합니다.
“편집기의 어느 부분이든 확장 가능하게” 허용하면 역설적으로 코드의 모든 부분이 확장 불가능해집니다. 왜냐하면 어느 한 부분이 바뀌면 누군가의 워크플로우가 깨질 수 있기 때문입니다.
xkcd 1172 – 워크플로우, 일명 “spacebar heating”
Emacs의 다중 언어 격리/API는 완벽하지 않지만 큰 도움이 됩니다. Emacs가 순수 Lisp만으로, 모든 것이 확장 가능하도록 작성되었다면, 진행 중인 Emacs 클론을 만드는 일은 사실상 불가능에 가깝습니다—특히 “spacebar heating” 문제를 어느 정도 해소하면서도 호환성을 유지하고 싶다면 말이죠.
5. 확장성은 노력에 달려 있다 ¶
Note: “100 % Lisp” 주장은 게으른 마케팅에 불과합니다. Lisp로 Lisp‑확장 에디터를 작성한다고 해서 에디터가 자동으로 더 확장 가능해지는 것은 아닙니다. 확장성은 API 인터페이스를 신중히 설계하고, 역사를 배우며, 사용자 요구에 귀 기울이고—무엇보다도—실제로 인터페이스 코드를 작성하는 데 시간과 노력을 투자함으로써 얻어집니다.
이 블로그 글은 제가 Emacs의 composition-function-table에 충격을 받아 내뱉은 rant thread를 기반으로 작성되었습니다. Emacs는 언제나 놀라움과 기이함이 끊이지 않으며, 특히 Emacs를 복제해 자신만의 Emacsen을 만들 때 더욱 그렇습니다.
Clarification: 이 글은 Lem에 반대하는 것이 아닙니다. 저는 Lem에서 많은 영감을 얻었고 그 훌륭한 설계들을 높이 평가합니다. 단지 제가 생각하기에 가장 편리한 예시였을 뿐이니, 혹시라도 편향적으로 보일 경우 양해 부탁드립니다.
6. 각주
Table 1: 더 나은 걸 위해, 맞죠?
“I want to write a better editor.” 😄
With better accessibility, right? (silent gaze) 😓
Also, please don’t mistake maintainability for extensibility. For example, Racket has migrated from its previous C core to a Racket core powered by Chez Scheme. Quoting from Rebuilding Racket on Chez Scheme Experience Report2:
… 이런 것들에 정확한 수치를 매기기 어렵지만 최소한 하나의 수치는 알려줄 수 있어요. 이것은 Racket 매크로 확장자를 수정하려는 사람들의 수입니다.
이것은 C였을 때: 우리 둘이 했어요. 그리고 이미 여섯 명이 (C‑to‑Chez 마이그레이션 후) 있습니다. 기억하세요: 처음 16년과 마지막 16년 동안 C 부분이 있었고, 그 여섯 명과 새로운 구현은 단 2년 만에 이루어졌습니다. 그래서 우리는 정말 유지보수가 더 나아질 것이라고 확신합니다.
So I do believe a mostly Lispy codebase can be better maintained and potentially attract more contributors than one in C. (But Emacs is mostly Lispy anyway.)3
This work is licensed under the Creative Commons Attribution‑ShareAlike 4.0 International license.
