ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 언어가 하나 늘 때 코드를 고치지 않으려면 — 표준 tags.scm 쿼리로 다언어 추출하기
    IT 2026. 7. 5. 22:00
    언어가 하나 늘 때 코드를 고치지 않으려면 — 표준 tags.scm 쿼리로 다언어 추출하기

    배경: 한 언어로 시작했지만, 여러 언어를 다뤄야 했다

    나는 여러 코드 저장소(repo)를 읽어 설명 문서(코드 위키)를 자동 생성하는 개인 프로젝트를 만들고 있다. 그 첫 단계는 코드에서 "어떤 함수와 모듈이 어디에 있는가"를 뽑아내는 추출기다. 처음엔 대상 저장소가 전부 파이썬이라, 추출기도 파이썬 한 언어 전용으로 충분했다.

    그런데 다루는 저장소가 늘면서 본체가 타입스크립트(TypeScript)·C#·PHP·C·자바스크립트(JavaScript)인 코드까지 들어왔다. 이런 저장소는 파이썬 파일이 거의 없어서, 파이썬 전용 추출기로는 함수·모듈이 하나도 안 잡힌다. 그러면 그 저장소의 위키는 텅 비게 된다. "모든 저장소에 위키"를 만들려면, 추출기가 이 언어들의 함수·모듈도 담아야 했다.

    문제: 언어를 늘릴 때마다 코드가 불어나면 안 된다

    가장 손쉬운 방법은 "언어별로 갈래를 나누는 것"이다. 파이썬이면 이렇게, 타입스크립트면 저렇게, C#이면 또 다르게… 이른바 조건 분기(if/elif)로 처리하는 방식이다. 처음 한두 언어는 그럭저럭 굴러간다. 하지만 언어가 늘수록 갈래가 곱절로 늘고, 각 갈래마다 비슷한 추출 코드가 복사되어 붙는다.

    diagram

    다이어그램 설명. 조건 분기 방식이 언어가 늘 때 어떻게 커지는지 보여준다. 파일이 어떤 언어인지 차례로 묻고, 맞으면 그 언어 전용 추출 코드로 들어간다. 문제는 새 언어를 지원할 때마다 질문 갈래가 하나 더 붙고, 그 아래에 또 한 벌의 추출 코드가 복사된다는 점이다. 함정은 이 복사된 코드들이 거의 비슷하다는 데 있다 — 한 곳에서 버그를 고치면 나머지 갈래에도 똑같이 고쳐야 하고, 빠뜨리면 언어마다 동작이 달라진다.

    정리하면 필요한 건 이것이다. 언어는 얼마든지 늘리되, 추출기 코드 자체는 거의 손대지 않는 구조. 그리고 가능하면, 이런 일을 위해 이미 널리 쓰이는 표준 방식이 있다면 그걸 따르는 게 낫다.

    해결: 언어별 "무엇이 함수인가"를 표준 쿼리(tags.scm)로 선언한다

    핵심 직관 — 다른 점은 데이터로, 그 데이터의 표준 형식을 쓴다

    핵심 발상은 단순하다. 언어마다 실제로 다른 것은 결국 "이 언어에서 무엇을 함수·메서드 정의로 볼 것인가"뿐이고, 나머지 절차(파일을 파싱해 트리를 만들고, 정의를 골라 이름과 줄 범위를 읽는 일)는 모든 언어가 똑같다. 그렇다면 다른 부분만 데이터로 선언하고 같은 절차는 하나의 공통 엔진으로 두면 된다.

    그런데 이 "무엇이 함수인가"를 선언하는 일에는 이미 널리 쓰이는 표준 형식이 있다. tree-sitter의 tags.scm 쿼리다. tags.scm은 각 언어 문법이 "이런 모양의 마디는 함수 정의, 저런 모양은 클래스 정의, 이건 호출"이라고 선언해 둔 쿼리 파일이다. 각 언어 문법 패키지에 표준으로 동봉되며, 깃허브(GitHub)의 코드 내비게이션 같은 도구들이 바로 이 tags.scm으로 심볼을 찾는다.

    비유하자면 만능 리모컨과 같다. 리모컨 본체(추출 엔진)는 하나이고, "기기별 신호 코드표"(언어별 tags.scm 쿼리)만 갈아 끼운다. 더구나 이 코드표는 업계 표준 규격이라, 대부분의 언어는 코드표가 이미 만들어져 동봉돼 있다.

    diagram

    다이어그램 설명. 표준 쿼리 방식의 전체 그림이다. 언어마다 표준 tags.scm 쿼리가 따로 있고, 모든 쿼리가 가운데의 같은 엔진으로 흘러 들어간다. 조건 분기 방식과의 결정적 차이는, 언어가 늘어도 엔진 코드에 갈래가 생기지 않고 그 언어의 표준 쿼리를 끼우기만 한다는 점이다. 핵심은 "무엇이 함수인가"를 코드가 아니라 표준 데이터(쿼리)로 표현했다는 것 — 엔진은 그 쿼리를 실행할 뿐이다.

    tags.scm 쿼리는 무엇을 선언하나

    tags.scm의 한 패턴은 "이런 모양의 마디를 찾아, 그 전체와 이름을 표시(캡처)하라"는 규칙이다. 예를 들어 파이썬의 함수 정의 규칙은 (function_definition name: (identifier) @name) @definition.function 한 줄로 표현된다. 풀어 읽으면 "함수 정의 마디를 찾아 그 전체를 @definition.function으로, 그 안의 이름을 @name으로 표시하라"는 뜻이다.

    diagram

    다이어그램 설명. 쿼리 한 줄의 속을 펼쳐 보면 세 가지를 담는다. 어떤 모양의 마디를 찾을지, 그 정의 전체를 어디에 표시할지(시작·끝 줄을 읽는 데 씀), 그 이름을 어디에 표시할지다. 핵심은 이름 추출까지 쿼리가 책임진다는 점이다 — 예전에는 "C는 이름이 선언자 안쪽에 숨어 있으니 따로 파고들자" 같은 특수 코드를 내가 짜야 했는데, 이제는 그 언어의 표준 쿼리가 이름을 어디서 꺼낼지 이미 알고 있다.

    엔진이 결과에서 골라내는 것

    tags.scm은 함수만 선언하지 않는다. 클래스 정의, 함수 호출, 상수까지 함께 표시한다. 그래서 엔진은 쿼리 결과 중 함수·메서드 정의 표시만 골라, 그 이름과 줄 범위를 읽는다.

    diagram

    다이어그램 설명. 표준 쿼리를 돌리면 함수뿐 아니라 클래스·호출·상수까지 한꺼번에 표시되므로, 엔진은 그중 함수·메서드 정의에 해당하는 표시만 추려 쓴다. 핵심은 "정의 전체 표시"에서 줄 범위를, "이름 표시"에서 이름을 따로 읽어 함수 노드를 만든다는 점이다.

    표준을 쓰며 부딪힌 세 가지 현실

    "표준 쿼리를 그대로 쓴다"는 말처럼 깔끔하지만은 않았다. 세 가지 현실적 보정이 필요했다.

    • C#은 표준 쿼리가 없다: 대부분 언어는 tags.scm이 문법 패키지에 동봉돼 있는데, C#만 빠져 있었다. 그래서 C#용 tags.scm은 표준 관례를 따라 직접 한 벌 작성해 넣었다.
    • 타입스크립트는 혼자선 부족하다: 타입스크립트의 표준 tags.scm은 타입 시그니처(본문 없는 선언)만 잡고 실제 함수 본문은 안 잡는다. 타입스크립트가 자바스크립트의 확장이기 때문인데, 그래서 표준 관례대로 자바스크립트 쿼리에 타입스크립트 추가분을 합쳐서 쓴다.

    설명. 두 가지 모두 "표준을 쓰면 공짜로 다 된다"가 환상임을 보여준다. 표준은 강력하지만, 빠진 언어는 직접 채워야 하고(C#), 언어 간 상속 관계를 알아야 한다(타입스크립트). 핵심은 그럼에도 자체 규격을 고집하기보다 표준에 맞추는 편이, 장기적으로 더 적은 코드와 더 넓은 호환을 준다는 점이다.

    효과: 언어 추가가 "표준 쿼리 한 줄"이 된다

    diagram

    다이어그램 설명. 이 설계가 주는 가장 큰 효과를 보여준다. 새 언어를 지원하려 해도 추출 엔진 코드는 한 줄도 건드리지 않고, 그 언어의 표준 쿼리를 연결하기만 하면 된다. 핵심은 "확장의 비용"이 코드 수정에서 표준 데이터 연결로 내려갔다는 점이다 — 코드를 고치지 않으니 기존에 잘 돌던 언어들이 새 언어 때문에 깨질 위험도 사라진다.

    diagram

    다이어그램 설명. 이 패턴이 따르는 오래된 설계 원칙을 그림으로 옮긴 것이다. 위쪽은 "확장에는 열려 있다" — 언어별 표준 쿼리를 계속 더할 수 있다. 아래쪽은 "수정에는 닫혀 있다" — 그래도 공통 엔진 코드는 그대로다. 이 두 성질이 합쳐지면, 기능이 늘어나는데도 기존 코드가 흔들리지 않는다. 핵심은 "자주 바뀌는 것(언어 목록)"과 "거의 안 바뀌는 것(추출 절차)"을 갈라, 변화를 표준 데이터 쪽에만 가둔 데 있다.

    핵심 효과 정리

    • 표준 채택: 자체 규격 대신 널리 쓰이는 tags.scm 쿼리를 따른다. 대부분 언어는 표준 쿼리가 이미 만들어져 있어 그대로 쓴다.
    • 이름 추출을 쿼리에 위임: C의 선언자 중첩이나 명명 화살표 함수 같은 까다로운 경우를, 내가 손으로 짠 특수 코드 없이 그 언어의 표준 쿼리가 처리한다.
    • 언어 추가가 데이터: 엔진 코드 변경 없이 표준 쿼리를 연결하면 새 언어가 처리된다.
    • 일관성: 모든 언어가 같은 추출 엔진을 지나므로, 결과의 형태(노드·주소 규칙)가 언어마다 어긋나지 않는다.

    내 코드 위키 자동생성 시스템과의 연결

    코드를 읽어 위키를 자동 생성하려면, 다루는 모든 저장소에서 함수·모듈을 빠짐없이 뽑아야 한다. 그런데 저장소마다 주력 언어가 다르다. 만약 언어를 늘리는 일이 매번 추출기 코드를 뜯어고치는 일이었다면, 언어 확장은 곧 위험하고 더딘 작업이 됐을 것이다.

    그래서 내 시스템은 "무엇이 함수인가"를 코드가 아니라 표준 tags.scm 쿼리로 선언하고, 추출 절차는 하나의 엔진으로 통일했다. 덕분에 파이썬에서 출발한 추출기가, 엔진을 거의 고치지 않고도 타입스크립트·C#·PHP·C·자바스크립트 저장소까지 함수·모듈을 담을 수 있게 됐다. 더 좋은 점은, 이게 내가 발명한 사설 방식이 아니라 코드 도구들이 두루 쓰는 표준이라는 것이다 — 표준에 올라타면, 빠진 조각(C# 쿼리)만 채우고 나머지는 생태계가 만들어 둔 것을 그대로 빌려 쓸 수 있다. 이 결정 하나가 "모든 저장소에 위키"라는 목표를, 매번의 코드 수술이 아니라 표준 데이터 연결의 문제로 바꿔 놓았다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.