트렌드브리프

추상 구문 트리(AST), 코드를 트리로 다루는 원리와 실무 도구

게시 · 약 5분 분량

핵심 요약 추상 구문 트리(AST)는 소스 코드를 트리 구조로 표현해 컴파일러·린터·트랜스파일러가 코드를 다루는 방식이다. 개념과 만들어지는 원리, 파이썬 ast 모듈 예제, Babel·ESLint·tree-sitter 활용을 정리했다.

목차
  1. 추상 구문 트리란 무엇인가
  2. 소스 코드가 AST가 되기까지
  3. 파이썬 ast 모듈로 직접 다뤄 보기
  4. 실무 도구는 AST를 어떻게 쓰나
  5. 개발자가 AST를 직접 만지는 순간

추상 구문 트리(AST, Abstract Syntax Tree)는 소스 코드를 트리 형태의 자료구조로 바꿔 놓은 것이다. 세미콜론·괄호·공백처럼 문법을 지키려고 붙인 껍데기는 걷어내고, 연산자·피연산자·제어 흐름 같은 논리 구조만 남긴다. 컴파일러·린터·코드 포매터·트랜스파일러가 코드를 문자열이 아니라 구조로 다뤄야 할 때 거의 예외 없이 이 AST를 거친다.

이 글은 AST를 처음 접하는 개발자를 기준으로 개념, 만들어지는 원리, 그리고 파이썬·자바스크립트 진영의 대표 도구를 다룬다. 특정 언어의 문법 전체나 컴파일러 최적화 단계까지 파고들지는 않는다. 파이썬 예제는 표준 라이브러리 ast 모듈 기준이며, 파이썬 공식 문서를 근거로 한다.

추상 구문 트리란 무엇인가

AST는 파서가 소스 코드를 문법 규칙에 맞춰 분석해 만든 트리다. 핵심은 구조를 트리에 담는다는 점이다. 예를 들어 1 + 2 * 3이라는 식을 보자. 곱셈이 덧셈보다 먼저 계산되는데, AST에서는 이 우선순위가 트리 모양으로 드러난다. 최상위 노드는 덧셈이고, 그 오른쪽 자식이 곱셈 노드가 된다. 괄호를 따로 표시하지 않아도 트리의 부모·자식 관계만으로 계산 순서가 정해진다.

여기서 추상이라는 말이 붙는 이유가 나온다. 파서가 처음 만드는 트리는 파스 트리(parse tree) 또는 구체 구문 트리(CST, Concrete Syntax Tree)라고 부른다. 여기에는 괄호·세미콜론·쉼표 같은 문법 토큰이 전부 노드로 들어 있다. AST는 이 가운데 의미에 영향을 주지 않는 껍데기를 걷어낸 형태다. 그래서 같은 코드라도 CST보다 AST가 훨씬 단순하다.

소스 코드가 AST가 되기까지

크게 두 단계를 거친다. 어휘 분석(lexing)과 구문 분석(parsing)이다. 먼저 렉서(lexer)가 코드 문자열을 토큰(token, 의미를 가진 최소 단위)으로 쪼갠다. x = 1 + 2x, =, 1, +, 2라는 다섯 개 토큰으로 나뉜다. 이 과정에서 공백과 주석은 버려진다.

다음으로 파서(parser)가 이 토큰들을 문법 규칙에 맞춰 트리로 엮는다. 짝이 맞지 않는 괄호나 빠진 세미콜론 같은 문법 오류는 대부분 이 단계에서 걸린다. 언어마다 문법(grammar)이 다르므로 만들어지는 노드 구성도 제각각이다. 가령 스위프트의 옵셔널·컬렉션 문법은 스위프트 컴파일러가 자체 AST로 표현하고, 파이썬은 파이썬대로 다른 노드 집합을 쓴다. AST가 만들어진 뒤에는 의미 분석(semantic analysis)이 트리를 훑으며 변수가 선언됐는지, 타입이 맞는지를 검사한다.

파이썬 ast 모듈로 직접 다뤄 보기

파이썬은 표준 라이브러리에 ast 모듈을 포함한다. 그래서 별도 설치 없이 코드를 트리로 바꿔 볼 수 있다. 파이썬 공식 문서에 따르면 ast.parse()가 소스 문자열을 AST 노드로 파싱하고, ast.dump()가 그 트리 구조를 문자열로 보여 준다. 아래 코드를 그대로 실행하면 대입문 하나가 어떤 노드로 쪼개지는지 확인할 수 있다.

import ast

code = "total = price * count"
tree = ast.parse(code)
print(ast.dump(tree, indent=4))

여기서 한발 더 나가면 ast.NodeVisitor로 특정 종류의 노드만 골라 순회할 수 있다. 아래 예제는 코드에 등장하는 모든 이름, 즉 변수와 함수 이름을 뽑아낸다. visit_Name이라는 이름의 메서드를 정의하면 Name 노드를 만날 때마다 호출되고, node.id에 그 식별자 문자열이 들어 있다.

import ast

class NameCollector(ast.NodeVisitor):
  def visit_Name(self, node):
    print(node.id)
    self.generic_visit(node)

tree = ast.parse("a = b + c")
NameCollector().visit(tree)

이 짧은 코드가 a, b, c를 차례로 출력한다. 실무에서는 이런 순회 위에 규칙을 얹어서 위험한 함수 호출을 찾아내거나, 특정 패턴을 다른 코드로 자동 치환하는 도구를 만든다. 노드 종류와 순회 API는 파이썬 공식 문서의 ast 모듈 페이지에 정리돼 있다.

실무 도구는 AST를 어떻게 쓰나

우리가 매일 쓰는 개발 도구 상당수가 내부에서 AST를 돌린다. 자바스크립트 트랜스파일러 Babel은 @babel/parser로 코드를 AST로 바꾼 뒤, 그 트리를 변형하고 다시 코드로 되돌리는 방식으로 최신 문법을 구형 문법으로 변환한다. 린터인 ESLint는 espree라는 내부 파서로 AST를 만든 다음 노드를 순회하며 규칙 위반을 잡아낸다. Babel과 ESLint는 ESTree라는 공통 AST 형식 규격을 공유한다.

에디터 쪽에서는 tree-sitter가 널리 쓰인다. GitHub가 만든 증분 파싱, 곧 코드가 편집될 때 바뀐 부분만 다시 파싱하는 시스템이다. Neovim이나 Zed 같은 에디터가 실시간 문법 하이라이팅과 코드 구조 파악에 이를 쓴다. 코드를 붙여 넣으면 AST를 곧바로 보여 주는 AST explorer 같은 웹 도구도 있어, 파서가 내 코드를 어떤 트리로 보는지 눈으로 확인하기 좋다.

AST 기반 정적 검사의 발상은 코드 스타일 점검을 넘어 아키텍처 규칙 검증으로도 넓어진다. 스위프트 진영에서 아키텍처 규칙을 유닛 테스트로 강제하는 접근이 그런 흐름의 한 예다. 아래 표는 대표 도구를 용도별로 정리한 것이다.

도구 대상 언어 주 용도
파이썬 ast 파이썬 코드 분석·변환(표준 라이브러리)
@babel/parser 자바스크립트·타입스크립트 트랜스파일·코드 변환
ESLint(espree) 자바스크립트 정적 검사(린팅)
tree-sitter 다국어 에디터 실시간 파싱·하이라이팅

개발자가 AST를 직접 만지는 순간

대부분은 도구 뒤에 숨어 있어서 AST를 의식할 일이 없다. 하지만 직접 다뤄야 할 순간이 온다. 팀 규칙을 강제하는 커스텀 린트 규칙을 만들 때, 수백 개 파일에 걸친 API 이름을 한 번에 바꾸는 코드모드(codemod)를 짤 때, 새 언어의 에디터 플러그인을 붙일 때가 그렇다. 이럴 때 코드를 문자열로 놓고 정규식으로 씨름하면 금세 한계에 부딪힌다. 코드의 구조를 트리로 다루는 순간 문제는 한결 다루기 쉬워진다. AST를 한 번 이해해 두면, 정규식으로는 위태롭던 작업이 트리 순회 문제로 바뀐다.

더 많은 글 보기 RSS 구독