모든 프로그래밍 언어에는 공식 문법이 있다. 컴파일러는 이를 읽고 parse tree를 구축하며, 맞지 않는 것은 모두 거부한다. 에디터는 문법을 완전히 무시하고 사용자가 원하는 대로 입력할 수 있게 한다.
이 괴리는 놀랄 만큼 많은 마찰의 원인이다. Autocomplete는 문맥상 의미 없는 identifier를 제안한다. Syntax highlighting은 리팩토링 중간에 깨진다. 에디터가 사용자가 실수하는 것을 지켜보았음에도 “Unexpected token” 오류가 발생한다. 우리는 이를 당연하게 여긴다. 40년 동안 텍스트 에디터가 유일했기 때문이다.
텍스트는 중간 표현이다. 컴파일러는 parse하는 순간 버린다. 그런데 왜 우리는 텍스트를 편집하는가?
구조화 편집이 실제로 의미하는 것
구조화 편집이란, 진실의 원천이 문자열 버퍼가 아닌 Abstract Syntax Tree라는 뜻이다. 에디터는 사용자의 편의를 위해 트리를 텍스트로 렌더링하지만, 모든 조작은 노드를 직접 다룬다. FunctionDeclaration. BinaryExpression. IfStatement. 이것들이 원자다.
매개변수를 추가하고 싶다면? 커서를 괄호 사이에 두고 타이핑하지 않는다. FunctionDeclaration 노드에서 “add parameter” 작업을 호출한다. 에디터는 placeholder를 가진 새 Parameter 노드를 만든다. 렌더링된 텍스트가 업데이트된다. 괄호의 균형이 맞지 않는 경우는 절대 없다. 괄호는 사용자가 입력하는 문자가 아니라 렌더러가 생성하는 시각적 경계이기 때문이다.
이것은 공상과학이 아니다. Lisp은 1960년대부터 이렇게 작동했다. S-expressions는 정의상 트리이기 때문이다. Smalltalk는 메서드를 grep 가능한 텍스트 파일이 아니라 브라우저에서 선택 가능한 객체로 다루었다. 최근에는 Unison 언어가 코드를 AST 노드의 Merkle tree로 저장하고, JetBrains MPS는 projectional editor로, 문법이 언어와 편집 경험 모두를 정의한다.
구조적 안전성 보장
트리 에디터가 리팩토링할 때 하는 일을 보자. 다음 Python 스크립트는 내부 로직을 근사한다:
import ast
# The canonical representation is a tree, not text
tree = ast.parse("result = calculate(x, y)")
# Find the Call node and append a keyword argument
for node in ast.walk(tree):
if isinstance(node, ast.Call):
node.keywords.append(
ast.keyword(arg='debug', value=ast.Constant(value=True))
)
# Render to text only when needed (Python 3.9+)
print(ast.unparse(tree))
# result = calculate(x, y, debug=True)
이 작업은 구조적으로 안전하다. calculate(x, y debug=True)를 실수로 만들 수 없다. keywords는 string buffer가 아닌 keyword 노드의 typed list이기 때문이다. 렌더러가 쉼표와 등호를 삽입한다. 트리는 항상 구문적으로 유효하다.
대부분의 formatter와 linter도 이런 방식으로 작동한다. Black, prettier, rustfmt는 모두 AST로 parse하고 트리를 변환한 뒤 출력한다. 차이점은 이들이 텍스트를 읽고, 변환하고, 텍스트를 쓴다는 것이다. 트리 에디터는 텍스트 왕복을 생략한다.
아무도 트리 에디터를 쓰지 않는 이유
이것이 그렇게 좋다면, 왜 모든 전문 개발자가 여전히 VS Code, Vim, Emacs를 쓰는가?
첫 번째 이유는 상호작용 속도다. 텍스트 편집은 긴밀한 루프를 최적화한다: 변경을 생각하고, 손가락으로 문자를 입력하고, 눈으로 확인한다. 모든 키 입력은 같은 조작이다. 트리 에디터에서는 “insert an if statement”, “rename a variable”, “extract a method”가 모두 다른 제스처다. 그냥 키를 마구 누를 수 없다. 인지적 오버헤드가 쌓인다.
두 번째 이유는 생태계의 벽이다. 파이프라인의 모든 도구는 텍스트를 전제로 한다. git diff, grep, GitHub code review, 코드 검색, 인터넷의 모든 튜토리얼. 구조화된 형식은 존재하지만, 소스 코드에서 원시 텍스트를 대체한 것은 없다. 네트워크 효과는 절대적이다.
세 번째 이유는 시각적 렌더링 문제다. 프로그래머는 수십 년의 조판 관례를 가진 텍스트로 코드를 읽는다. 주석이 노드의 절반을 설명할 때는 어디에 위치하는가? 사용자가 원하는 것을 아직 지정하지 않은 상태에서 작업 중인 트리를 어떻게 표시하는가? 트리를 읽고 쓸 수 있는 텍스트로 렌더링하는 것은 텍스트를 트리로 렌더링하는 것보다 어렵다.
텍스트 에디터에서의 구조적 명령
에디터를 바꾸지 않아도 트리 인식 이점을 얻을 수 있다. 이미 여러 도구가 AST 위에서 작동하면서 텍스트 인터페이스를 제공한다.
Lisp용 Paredit와 Smartparens는 S-expressions를 트리로 다룬다. 표현식을 상위 스코프로 “slurp”하거나 “barf”로 내보낸다. 괄호는 여전히 텍스트이지만, 편집 명령은 트리 작업이다. 구문 오류를 단 한 번도 만들지 않고 subtree 전체를 이동한다.
현대 IDE의 리팩토링은 코드를 parse하고 AST를 구축한 뒤, 어떤 변수가 매개변수가 될지 판단하고 트리를 재작성한다. IntelliJ에서 “extract method”를 실행할 때, 이는 text replacement를 하는 것이 아니다. 구조적 변환을 수행하고 결과를 출력한다.
Language server는 모든 작업에 대해 내부적으로 AST를 구축한다. Go-to-definition, rename, inline hint는 모두 트리를 순회한다. 프로토콜은 호환성을 위해 텍스트 위치를 전달하지만, 지능은 구조적이다.
에디터를 바꾸지 않고 실험하는 방법
코드를 작성하는 방식과 컴파일러가 이해하는 방식 사이의 간극을 좁히고 싶다면 여기서 시작하라:
-
Python의
ast모듈을 사용하라. 실제 코드를 parse하고,ast.walk로 트리를 순회하며 변환하는 스크립트를 작성하라. 대부분의 Python 도구(flake8, mypy, Black)가 정확히 이것을 한다. -
Tree-sitter를 탐색하라. 온라인 플레이그라운드에서 파일을 parse하고 concrete syntax tree를 살펴보라. 파서가 실제로 보는 것이 무엇인지 확인하라.
-
Lisp의 구조화 편집을 시도하라. Clojure나 Emacs Lisp을 작성한다면 Paredit을 설치하라. 하루 동안 문자가 아닌 expression 단위로 탐색하도록 강제하라. 그 느림은 교육적이다.
텍스트 파일은 사라지지 않을 것이다. 도구가 너무 많고, 역사가 너무 길고, muscle memory가 너무 강하다. 하지만 텍스트 편집과 구조적 이해 사이의 간극은 버그, 마찰, 인지적 부하의 실재하는 원인이다. 이를 해결하기 위해 트리 에디터가 필요한 것이 아니다. 텍스트가 진짜라고 가장하는 것을 그만두어야 한다.