Tree-sitter와 언어 서버(LSP)가 무엇이며, 각각이 어떤 문제를 해결하는지 관찰 가능하고 실용적인 관점에서 비교한다.
URL: https://lambdaland.org/posts/2026-01-21_tree-sitter_vs_lsp/
Title: Explainer: Tree-sitter vs. LSP
오늘 좋은 질문을 받았다. Tree-sitter와 언어 서버의 차이가 무엇이냐는 것이었다. 나는 이 두 도구가 내부적으로 어떻게 작동하는지 깊게 이해하고 있는 건 아니라서, 관찰 가능한, 실용적인 관점에서 설명해 보려고 한다.
Tree-sitter는 _파서 생성기(parser generator)_다. 즉, 프로그래밍 언어에 대한 기술(설명)을 Tree-sitter에 주면, 그 언어를 파싱해 주는 프로그램을 만들어 준다. Tree-sitter의 특별한 점은 a.) 빠르다는 것, b.) 입력에 _문법 오류(syntax errors)_가 있어도 견딜 수 있다는 것이다. 이 두 가지 성질 덕분에 Tree-sitter는 텍스트 에디터에서 구문 강조(syntax highlighting) 엔진을 만들기에 이상적이다. 프로그램을 편집할 때는, 대부분의 시간 동안 그 프로그램이 문법적으로 유효하지 않은 상태에 놓인다. 그 시간 동안 타이핑하는 도중에 색깔이 바뀌거나(깜빡이거나) 아예 망가지는 일을 원치 않을 것이다. 단순한 정규식 기반 구문 강조기는 이런 문제를 자주 겪는다.
Tree-sitter는 파스 트리(parse tree)에 대해 질의할 수 있는 쿼리 언어도 제공한다. 나는 개발하려고 하는 Emacs 패키지에서 이것을 사용해 Citar 인용/서지 도구에 Typst 지원을 추가하고 있다. Tree-sitter에게 특정 문법 객체를 찾아 달라고 요청할 수 있는데, 이는 정규식을 쓰는 것보다 더 안전하고 견고하다. Typst 엔진 자체가 하는 것과 비슷한 수준의 파싱을 할 수 있기 때문이다.
요컨대, Tree-sitter는 우연히 비슷해 보이는 정규식에 기대는 대신, 언어 구현체가 프로그램을 파싱하는 방식에 충실한 구문 강조를 제공한다.
_언어 서버(language server)_는 프로그램을 분석하고, 그 프로그램에 관한 흥미로운 정보를 텍스트 에디터에 보고해 줄 수 있는 프로그램이다. Language Server Protocol (LSP)이라는 표준은 텍스트 에디터와 서버 사이를 오가는 JSON 메시지의 종류를 정의한다. 이 프로토콜은 공개 표준이며, 어떤 언어와 어떤 텍스트 에디터든 이 프로토콜을 활용해 시스템에서 똑똑한 프로그래밍 보조 기능을 얻을 수 있다. 언어 서버는 예를 들어 심볼의 정의 위치 찾기, 커서 위치에서 가능한 자동 완성 후보 등과 같은 정보를 텍스트 에디터에 제공하고, 에디터는 이를 어떻게/언제 표시하거나 사용할지 결정할 수 있다.
언어 서버는 “N×M 문제”를 해결한다. N개의 프로그래밍 언어와 M개의 텍스트 에디터가 있다면, 언어 분석기 구현이 N×M개 필요하다는 뜻이다. 이제는 각 언어가 언어 서버 하나만 갖추면 되고, 각 에디터는 LSP 프로토콜을 말할 수만 있으면 된다.
언어 서버가 강력한 이유는 언어의 런타임과 컴파일러 툴체인에 연결해 사용자 질의에 대해 의미론적으로 정확한(semantically correct) 답을 얻을 수 있기 때문이다. 예를 들어 pop 함수가 두 버전 있다고 하자. 하나는 stack 라이브러리에서 가져온 것이고, 다른 하나는 heap 라이브러리에서 가져온 것이다. Emacs의 dumb-jump 같은 도구를 쓴다면—여기서 말해 두고 싶은데, 나는 dumb-jump가 정말 멋진 도구라고 생각하며 결코 폄하하려는 게 아니다. 이 도구는 한계를 솔직히 밝히고 있고, 언어 서버가 없을 때 유용할 수 있다—pop 호출로부터 정의로 점프하려고 할 때, 해당 지점에서 어떤 모듈이 스코프에 들어와 있는지 확신할 수 없어서 어디로 가야 할지 헷갈릴 수 있다. 반면 언어 서버는 이런 정보에 접근할 수 있어야 하며, 혼동하지 않을 것이다.
언어 서버를 구문 강조에 사용하는 것도 가능 하다. 다만 왜 굳이 그렇게 하고 싶어 하는지(또는 왜 하지 말아야 하는지)에 대해 특별히 강력한 이유가 있는지는 나는 잘 모르겠다. 언어 서버는 더 복잡한 프로그램일 수 있어서 문법에 관한 특히 자세한 정보를 노출할 수도 있지만, Tree-sitter보다 느릴 수도 있다.
Emacs 내장 LSP 클라이언트인 Eglot은 최근 언어 서버가 제공하는 구문 강조를 지원하기 위해 eglot-semantic-tokens-mode를 추가했다. 나는 Rust 코드에서 이것을 조금 써 봤는데 괜찮아 보였다. 다만 Tree-sitter 기반 구문 강조가 내게는 이미 충분히 잘 동작하고 있어서, LSP 기반 하이라이팅을 써야 할 설득력 있는 이유를 찾지 않는 한 아마 계속 Tree-sitter 쪽을 쓸 것 같다.
업데이트: HN의 한 댓글 덕분에, 언어 서버로 구문 강조를 하고 싶은 좋은 이유를 이제 알게 되었다. Rust 언어 서버 rust-analyzer는 변수 참조가 가변(mutable)인지 아닌지를 텍스트 에디터에 알려줄 수 있다. 즉, mut 참조를 mut가 아닌 참조와 다르게 하이라이팅할 수 있다는 뜻이다. 팁을 준 David Barsky에게 감사한다!
위의 글은 전부 내가 썼다. 나는 글의 어떤 부분도 LLM에게 생성해 달라고 요청하지 않았다. 내 블로그에서 무언가를 읽을 때마다, 그것은 100% 사람—나, Ashton Wiersdorf—에게서 나온 것임을 알아 주었으면 한다.
나는 LLM이 무가치하다거나 절대 사용하면 안 된다고 말할 정도로 반(反)AI인 것은 아니다. 나도 LLM을 조금 써 봤다. 나는 LLM이 언어 간 번역에 환상적으로 뛰어나다고 생각한다. 이런 건 LLM이 잘해야 하는 일처럼 보인다. 또 내가 쓰는 코드의 지루한 부분을 작성하는 데도 도움이 된다. 하지만 대부분의 경우, 까다로운 부분의 코드는 내가 직접 쓰는 속도가, 원하는 것을 LLM에 설명해서 얻는 속도와 비슷한 편이라고 느낀다.
LLM이 위와 비슷한, 피상적인 텍스트 더미를 생성할 수도 있다는 걸 안다. 솔직히 그 텍스트도 꽤 도움이 됐을 수 있다. 하지만 방금 읽은 내용은 이 주제에 대해 생각한 사람이, 당신이 이해하도록 돕기 위해 노력하여, 손가락으로 직접 입력한 것이다. 여기 있는 단어 하나하나의 의미를 이해하는 진짜 인간에게서 나온 글이다. 나는 문법으로 장난치거나 답처럼 보이는 블로그 글을 생성하지 않는다. 여기에 진짜 의미가 있다. 즐기고, 나아가 더 많은 의미를 만들어 내길 바란다.