ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 코드를 글자가 아니라 구조로 읽기 — tree-sitter로 함수와 모듈을 뽑아내기
    IT 2026. 7. 5. 21:00
    코드를 글자가 아니라 구조로 읽기 — tree-sitter로 함수와 모듈을 뽑아내기

    배경: 위키를 자동 생성하려면 먼저 "구조"를 알아야 한다

    나는 여러 코드 저장소(repo)를 읽어 설명 문서(코드 위키)를 자동으로 생성하는 개인 프로젝트를 만들고 있다. 사람이 손으로 위키를 쓰는 게 아니라, 코드 자체를 진실의 원천으로 삼아 기계가 위키를 써내는 시스템이다.

    그러려면 가장 먼저 풀어야 할 질문이 있다. "이 저장소 안에 어떤 함수와 모듈이 있고, 각각 몇 번째 줄에 있는가?" 이걸 모르면 "이 함수는 이런 일을 한다"는 문장을 단 한 줄도 근거 있게 쓸 수 없다. 즉 위키의 모든 서술은 "코드의 어디"라는 좌표 위에 세워져야 하고, 그 좌표 목록을 만드는 일이 모든 작업의 출발점이다.

    문제: 글자만 봐서는 "진짜 함수"를 못 짚는다

    가장 단순한 방법은 grep으로 def (파이썬 함수 선언 키워드) 같은 패턴을 찾는 것이다. 빠르고 쉽다. 하지만 grep은 코드를 글자(텍스트)로만 본다. 코드의 문법 규칙을 모르기 때문에 다음을 구별하지 못한다.

    • 주석 속 단어: # def process를 호출하지 말 것 같은 주석 안의 글자도 함수 선언으로 오해한다.
    • 문자열 속 단어: 로그 메시지 "def process started" 안의 글자도 잡아온다.
    • 여러 줄에 걸친 정의: 매개변수가 여러 줄로 나뉘거나 데코레이터가 붙으면 어디가 진짜 함수의 시작이고 끝인지 모른다.
    • 언어마다 다른 규칙: 파이썬은 def, 자바스크립트는 function, C#은 또 다르다. 언어마다 정규식을 새로 짜야 한다.

    ▲ 글자로 읽기 — grep

    diagram

    다이어그램 설명. 글자 검색이 왜 "진짜 함수"를 못 짚는지 보여준다. "def process"라는 글자를 텍스트로 찾으면, 그 글자가 함수 선언 자리에 있든 주석 안에 있든 로그 문자열 안에 있든 가리지 않고 전부 후보로 올라온다. 코드의 문법을 모르기 때문에 "어느 것이 진짜 함수 정의인가"를 끝내 가려내지 못한다. 함정은, 검색이 빠르다는 장점에 가려 "틀린 후보를 섞어 준다"는 약점이 잘 안 보인다는 점이다 — 자동 생성 문서에서는 단 하나의 오탐도 거짓 서술의 씨앗이 된다.

    ▲ 구조로 읽기 — tree-sitter

    diagram

    다이어그램 설명. 구조 파싱이 같은 코드를 어떻게 다르게 읽는지 보여준다. tree-sitter는 먼저 코드를 문법 규칙으로 파싱해 구문 트리로 바꾸고, 그 트리에서 "함수 정의"라는 종류의 마디만 골라낸다. 주석이나 문자열, 여러 줄 정의는 문법상 애초에 다른 종류의 마디로 분류되므로 함수 후보에 끼지 못한다. 핵심은 "글자가 같은가"가 아니라 "문법상 무엇인가"로 판단한다는 점이다 — 그래서 같은 "def process"라도 진짜 함수 정의 자리만 정확히 집어낸다.

    해결: tree-sitter로 코드를 "문법 트리"로 바꾼다

    핵심 직관 — 글자를 구조로 승격시킨다

    여기서 쓰는 도구가 tree-sitter다. tree-sitter는 소스 코드를 빠르게 파싱(parsing — 문법 규칙에 따라 구조를 분석)해서 구문 트리(syntax tree — 코드의 문법 구조를 나무 모양으로 표현한 것)로 만들어주는 파서다. 깃허브(GitHub)에서 출발해 지금은 수많은 에디터와 코드 도구의 구문 분석 엔진으로 널리 쓰인다.

    비유하자면, grep은 문장을 글자들의 나열로만 보는 반면, tree-sitter는 문장을 읽고 "이건 주어, 이건 동사, 이건 목적어"라고 문법 성분으로 분해하는 것과 같다. 일단 문법 구조를 손에 쥐면, "동사만 모아줘" 같은 요청에 정확히 답할 수 있다. 코드에서 "함수 정의만 모아줘"가 바로 그런 요청이다.

    diagram

    다이어그램 설명. 추출의 한 흐름을 한 줄로 보여준다. 소스 코드를 파서에 넣으면 문법 구조가 나무 모양으로 나오고, 그 나무에서 표준 쿼리로 "함수 정의"에 해당하는 가지만 골라내면, 함수의 이름과 그것이 차지하는 줄 범위가 깔끔하게 떨어진다. 핵심은 중간의 구문 트리 단계다 — 글자를 구조로 한 번 승격시켜 두면, 그 뒤의 모든 질문이 "구조에서 골라내기"라는 정확한 작업으로 바뀐다.

    구문 트리는 어떻게 생겼나

    구문 트리는 코드의 문법 성분을 부모-자식 관계로 표현한다. 예를 들어 함수 하나는 "함수 정의"라는 노드(node — 트리의 마디)가 되고, 그 아래에 "이름", "매개변수 목록", "본문" 같은 자식 노드가 달린다.

    diagram

    다이어그램 설명. 파일 하나가 어떻게 구조로 펼쳐지는지 보여준다. 파일 전체가 맨 위 마디이고, 그 아래에 함수 정의 마디가 달리며, 함수 정의 마디는 다시 이름·매개변수·본문이라는 자식 마디로 나뉜다. 내 추출기는 이 나무에 표준 쿼리(tags.scm)를 돌려 "함수 정의" 마디와 그 안의 이름 마디를 골라, 이름·시작·끝 위치를 읽는다.

    여러 언어를 하나의 추출기로 — 표준 tags.scm 쿼리

    내 시스템은 파이썬뿐 아니라 타입스크립트(TypeScript), 자바스크립트(JavaScript), C#, PHP, C까지 다룬다. 언어마다 추출 코드를 따로 짜면 언어가 늘 때마다 코드가 불어난다. 그래서 "무엇이 함수인가"를 코드가 아니라 각 언어의 표준 쿼리로 선언하고, 하나의 엔진이 그 쿼리를 실행하게 했다. 여기 쓰는 표준 형식이 tree-sitter의 tags.scm 쿼리다 — "이런 모양의 마디는 함수 정의, 저런 모양은 클래스 정의"라고 선언해 둔 쿼리 파일로, 깃허브 코드 내비게이션 같은 도구도 같은 방식을 쓴다.

    • 대부분 언어(파이썬·자바스크립트·PHP·C 등)는 표준 tags.scm 쿼리가 문법 패키지에 이미 동봉돼 있어 그대로 쓴다.
    • C#만 동봉된 쿼리가 없어, 같은 표준 관례를 따라 직접 한 벌 작성해 넣었다.
    • 타입스크립트는 자바스크립트의 확장이라, 자바스크립트 쿼리에 타입스크립트 추가분을 합쳐서 쓴다(타입스크립트 쿼리만으로는 함수 본문을 못 잡는다).

    diagram

    다이어그램 설명. 언어가 늘어도 추출 코드는 하나로 유지되는 구조를 보여준다. 가운데의 추출 엔진은 그대로 두고, 위쪽에 그 언어의 표준 쿼리만 연결하면 그 언어의 파일도 같은 방식으로 처리된다. 핵심은 언어별 차이를 코드 분기가 아니라 표준 데이터(쿼리)로 옮긴 것 — 더구나 내가 만든 사설 규격이 아니라 업계 표준이라, 대부분 언어는 쿼리가 이미 만들어져 있다.

    무엇을 뽑아 어떻게 저장하나 — 주소와 라인

    추출기는 각 파일에서 저장소·모듈(파일)·함수를 노드로 만들고, "저장소가 모듈을 소유하고(OWNS), 모듈이 함수를 소유한다"는 포함 관계를 엣지(edge — 노드 사이의 연결선)로 잇는다. 파이썬 파일에 대해서는 import 의존(DEPENDS_ON)도 함께 뽑는다.

    diagram

    다이어그램 설명. 추출 결과가 어떤 모양으로 쌓이는지 보여준다. 저장소 아래에 파일이, 파일 아래에 함수가 매달리는 단순한 포함 관계다. 가장 중요한 건 맨 아래 함수에 붙는 주소다 — 저장소와 커밋, 언어, 파일 경로, 함수 이름, 그리고 줄 번호까지 담아 "이 함수는 세상에서 단 하나뿐인 이 자리"라고 못 박는다. 이 주소가 있어야 같은 이름의 함수가 여러 개여도 헷갈리지 않고, 위키의 모든 서술에 "그 근거는 이 파일 이 줄"이라는 검증 가능한 좌표(라인 앵커)를 달 수 있다.

    일부러 안 한 것들

    가벼움과 정확성을 위해 의도적으로 범위를 좁힌 결정들이 있다.

    • 무거운 언어 서버를 쓰지 않는다: 정확한 분석을 위해 언어별 서버를 띄우는 방식도 있지만, 설치·기동·통신이 무겁다. tree-sitter는 의존성 없이 충분히 빠르고 결정적이라, "가벼운 결정적 추출"이라는 원칙에 더 맞았다.
    • 이름 없는 익명 함수는 노드로 만들지 않는다: 이름이 없는 함수는 안정적인 주소를 붙일 수 없어 라인 앵커의 근거가 되지 못한다. 그래서 익명 함수는 제외한다(반면 이름 있는 화살표 함수는 표준 쿼리가 이름을 근거로 잡아 포함한다).

    여기까지가 tree-sitter의 일 — 그 너머는?

    중요한 경계가 하나 있다. tree-sitter는 "여기 process를 호출하는 자리가 있다"는 구문까지는 안다. 하지만 "이 process가 어느 정의를 가리키는가"라는 의미는 모른다. 이름 해석은 tree-sitter의 일이 아니다.

    diagram

    다이어그램 설명. tree-sitter가 어디서 멈추는지를 분명히 한다. 구문 파서는 "이건 함수 호출이고 이름은 process"까지는 정확히 알지만, 같은 이름의 정의가 여럿일 때 "지금 이 자리의 process가 그중 무엇인가"는 답하지 못한다. 그 의미 해석은 다음 단계의 몫이다. 함정은 "구조를 안다"는 사실 때문에 "의미도 안다"고 착각하기 쉽다는 점 — 구조를 아는 것과 이름이 무엇을 가리키는지 아는 것은 엄연히 다른 층위다.

    효과: 무엇이 가능해졌나

    • 결정적 추출: 같은 코드를 넣으면 항상 같은 트리, 같은 함수 목록이 나온다. 추측이나 환각이 끼어들 여지가 없어, 위키 검증의 단단한 토대가 된다.
    • 다언어를 하나로: 여섯 가지 언어를 같은 엔진과 표준 tags.scm 쿼리로 처리한다. 언어 추가는 그 언어의 표준 쿼리를 연결하는 일이다.
    • 오류에 강함: 코드 한 곳에 문법 오류가 있어도 파서가 그 부분만 건너뛰고 나머지를 파싱한다. 깨진 파일 하나가 전체를 멈추지 않는다.
    • 빠름: 버전 관리에 등록된 파일만 골라 파싱하고, 바뀌지 않은 저장소는 이전 결과를 재사용해 다시 파싱하지 않는다.
    • 라인 앵커의 출처: 모든 함수에 "파일 몇 번째 줄"이라는 검증 가능한 좌표가 붙는다. 위키의 모든 주장을 코드에 묶어두는 첫 단추다.

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

    코드를 읽어 문서를 자동으로 써내는 시스템에서, 모든 서술은 "실제로 그 코드 어디에 있는가"로 검증 가능해야 한다. 그 검증의 출발점이 바로 "이 저장소에 어떤 함수·모듈이 어디에 있는가"라는 정확한 목록이고, tree-sitter가 그 목록을 결정적으로 만들어 준다.

    그래서 내 시스템의 추출 첫 단계는 전적으로 tree-sitter 위에 서 있다. 코드를 구문 트리로 바꾸고, 각 언어의 표준 tags.scm 쿼리로 여러 언어의 함수·모듈을 같은 방식으로 뽑고, 각 함수에 "저장소·커밋·언어·경로·이름·줄"이라는 주소를 달아 그래프에 심는다. 이렇게 만든 결정적 골격 위에, 다음 단계가 "이 함수가 저 함수를 호출한다" 같은 의미 관계를 얹고, 최종적으로 라인 앵커로 모든 주장을 검증한다. tree-sitter가 없으면 위키는 "그럴듯하지만 코드 어디에도 묶이지 않은 글"이 되고, 있으면 "코드의 구조에 단단히 박힌 지도"의 첫 장이 된다.


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

Designed by Tistory.