파서는 달라도 AST는 이어진다: JS 파서와 ESTree 이야기

이가 은·4일 전

frontend

목록 보기
4/4

JS 파서 생태계

JS 생태계엔 파서가 하나가 아니다.
ESLint는 Espree를, Babel은 자체 파서를, swc와 oxc는 각자 Rust로 만든 파서를 사용한다.
그런데 이 중 상당수는 같은 트리 형식(ESTree)을 공유하고, 일부는 의도적으로 거기서 벗어나 있다.

그렇다면 같은 JavaScript를 읽으면서도, 서로 다른 파서와 AST는 왜 생겨났을까?

물론 이보다 앞서 parse_js 같은 JavaScript 파서들도 있었다.
다만 현재 JavaScript 도구 생태계로 이어지는 Esprima, Acorn 계열의 흐름은 2012년부터 본격적으로 갈라지기 시작한다.

여기서 파서와 AST는 구분해서 볼 필요가 있다. 파서는 소스 코드를 읽어 트리를 만드는 구현체이고, AST는 그 결과를 어떤 형태로 표현할지에 대한 구조다. 서로 다른 파서가 같은 AST 형식을 만들 수도 있고, 같은 JavaScript를 파싱하면서 서로 다른 AST를 만들 수도 있다.

Acorn의 탄생

2012년 7월, Ariya Hidayat은 자바스크립트 파서 Esprima의 설계 방향을 설명하며 이렇게 썼다.

Behind Esprima: Fast, Readable, Heavily Tested, Error Tolerant, Forward Looking

Speed Comparison - Esprima vs parse-js

당시에 Esprima는 다른 파서들과의 차별성을 두려고 했다.

속도를 최우선으로 고려하여 설계되었고, 가독성, 테스트, 에러 허용, 미래지향까지 다섯 가지를 강조했다.

Acorn: yet another JavaScript parser

Marijn Haverbeke가 Esprima의 성능 주장에 도전해서 Acorn을 만들었다.

Acorn의 장점

  • 문서화가 잘 되어있고 널리 알려진 AST 포맷 기반
  • Esprima보다 코드량이 절반 수준이었다.
  • 당시 벤치마크에서 소스 위치 정보를 기록할 경우 Esprima보다 약 5배 빠른 결과를 보였다.

Acorn은 2012년 9월 v0.0.1로 시작했다.
추후 ESLint에서는 Esprima가 하던 파싱을 Espree를 거쳐 Acorn이 맡게 된다.

Espree의 변천사

ESLint는 최초 공개 버전에서는 Esprima를 그대로 사용했다.
바로 가져다 쓸 수 있는 완성된 파서였고, ESLint가 Esprima가 만드는 SpiderMonkey AST를 전제로 만들어졌기 때문이다.

ESLint에서는 v0.10.2에서 마지막으로 Esprima를 사용하고, v0.11.0부터 Espree를 사용하기 시작했다.

왜 Espree를 사용하게 됐을까

Esprima는 1.2.2에서 1.2.3까지 8개월의 공백(2014-05-19 ~ 2015-01-18)이 있었다.
ESLint는 2014년 블로그에서 Esprima의 ES6 지원이 공개되지 않았고 몇 달째 새 릴리스가 없었다고 밝혔다. ES6와 JSX를 넣으려다 문제를 많이 겪은 끝에, 직접 통제할 수 있는 파서를 만들기로 했다.

Espree는 ES6 작업 전 Esprima v1.2.2에서 포크하여 자체적으로 시작되었다.

ESLint는 2014년에 Acorn도 검토했지만 채택하지 않았다. Acorn도 같은 AST를 만들었지만, ESLint가 의존하던 Esprima의 토큰/주석 방식과 달라 맞추는 작업이 너무 컸다.

ESLint 파서 교체 흐름 도식화

2015년 들어 Esprima의 유지보수가 다시 재개되면서, ESLint 팀은 Espree를 다시 Esprima와 통합할 가능성도 검토했다.
다만 언어 기능을 선택적으로 켜고 끄는 기능은 Esprima 로드맵에 있었지만, 실제 반영까지는 시간이 걸릴 수 있었다.
ESLint는 이런 제약을 고려해, 확장 구조를 갖춘 Acorn을 기반으로 Espree를 다시 설계하는 쪽을 선택했다.

2015년 무렵 Acorn은 Espree가 필요로 하는 확장 지점을 이미 갖고 있었고, Espree는 이때부터 Acorn 위에서 동작하면서도 Esprima와 유사한 API와 결과를 제공하는 방향으로 다시 개발되기 시작했다.
지금도 Espree README.md는 Acorn이 "modular architecture"를 가졌다고 설명한다

Espree v3.0.0 Alpha 1 released

ESTree의 탄생과 그 이후

Espree가 Acorn 위에서 동작해도 기존 Esprima와 같은 AST 형태에 맞추려고 노력했던 것처럼, 파서와 도구가 같은 AST를 써야 한다는 문제는 Espree만의 고민이 아니었다. 이 문제를 해결하려는 시도가 ESTree였다.

SpiderMonkey Parser API와 AST 포맷

Mozilla 엔지니어가 SpiderMonkey 파서를 API로 공개하고 AST 포맷을 문서화했다.

An API for parsing JavaScript 블로그 글

이 포맷은 이후 JavaScript 소스를 다루는 여러 파서와 도구에서 사용되기 시작했다.

Esprima ast image

Esprima도 이 포멧을 따랐다. Behind Esprima에서 Ariya Hidayat은 이 포멧이 ECMAScript 스펙을 가깝게 따르고 있어서 채택했다.

For the abstract syntax tree (AST), I decided to settle for the same format used in Mozilla SpiderMonkey parser reflection. The reason is simple, the format is closely following the actual ECMAScript language specification.

Acorn도 같은 포멧을 썼다. Marijn Haverbeke는 Acorn 소개 글에서 Acorn이 만드는 AST를 "well-documented, widely used AST format"이라 부르며, 그 포멧의 설명으로 SpiderMonkey Parser API를 가리켰다.

표준화의 계기

SpiderMonkey 포멧은 공용어가 되었지만, 원래는 Mozilla의 한 엔진이 공개한 API의 출력 형식이었다.
JavaScript는 계속 발전해 나갔다. ESTree는 발전하는 JavaScript를 따라갈 수 있도록, 도구를 만들고 쓰는 사람들이 함께 발전시키는 커뮤니티 표준이다. Esprima가 ES6 기능을 더하는 과정에서 다른 파서들과 협업하며 정의하기 시작했고, Esprima, SpiderMonkey 파서, Acorn, Babel 등이 참여했다.

jQuery 블로그 - Esprima 2.1 Released(2015)

다른 길을 걷는 파서들

ESTree는 여러 도구가 서로 호환되도록 만든 표준이다. 하지만 모든 파서가 ESTree를 그대로 내부 AST로 쓰는 건 아니다.
Babel, SWC, Oxc는 각자 다른 AST를 쓴다.

Babel

Babel

There are two things that almost any tooling of any programming language depends on really heavily: parsers and transpilers. ... The history of these tools in JavaScript has been long and sad. Everyone is constantly reimplementing the same things and it’s created an absolute mess.

Not Born to Die는 6to5가 Babel로 이름을 바꾸며 쓴 글이다. 이 글에서 Babel은 파서와 트랜스파일러를 다들 반복 구현하는 상황을 끝내고, 생태계가 기댈 공유 기반이 되겠다는 목표로 출발했다고 설명한다.
@babel/parser(이전 이름 Babylon)는 Acorn과 acorn-jsx를 크게 바탕으로 만들어졌다.

Babel 파서의 특징

  • 최신 표준 ECMAScript 문법을 기본 지원한다.
  • 주석 정보를 AST 노드에 연결한다.
  • JSX, Flow, TypeScript 문법을 지원한다.
  • 아직 표준화되지 않은 일부 ECMAScript 제안도 parser plugin으로 지원한다.

Babel AST는 ESTree 스펙을 바탕으로 하되, ESTree와 호환되지 않는 부분이 있다. 이 차이는 Babel 문서에 목록으로 명시되어 있고, 플러그인을 쓰면 ESTree 형태로 되돌릴 수 있다.

이미 있는 차이점을 ESTree에 맞추려면 하위 Babel 플러그인이 모두 새 AST기반으로 수정되어야하는 부담이 생긴다. 그래서 2022년 Babel 메인테이너는 ESTree AST로 되돌릴 계획은 없다고 밝혔다.
다만 새로 나오는 기능은 ESTree 팀과 협업해 차이를 최소화하려 하고, TypeScript AST는 TypeScript-ESTree에 맞추는 작업이 진행 중이다.

Oxc

Oxc

A collection of high-performance JavaScript tools written in Rust

Oxc는 속도를 제품 요구사항으로 본다.
공식 문서는 빠른 도구가 로컬 피드백 루프를 개선하고 CI 비용을 줄인다고 이유를 밝힌다. 린터, 포매터, 변환기 등이 모두 같은 파서 위에 있어서, 파서가 빨라지면 그 효과가 모든 도구로 이어진다.

이 속도를 위해 Oxc는 Rust로 작성되었다. 공식 학습 가이드는 성능 이득 때문에 JavaScript 도구를 Rust나 Go로 만드는 것이 요즘의 흐름이라고 설명한다. 또 성능 좋은 Rust 프로그램의 핵심은 메모리 할당을 줄이고 CPU 연산을 줄이는 것이라고 말한다. Oxc는 이 방향으로 메모리 할당 방식부터 속도를 위해 설계했다.

Oxc 파서의 특징

  • 파서가 SWC보다 3배 빠르다고 주장한다.
  • 성능을 위해 Rust 기반으로 동작한다.
  • JavaScript, TypeScript, JSX와 최신 ECMAScript를 지원한다.

Oxc의 Rust 쪽 내부 AST는 ECMAScript 스펙을 따라 ESTree와 다르게 설계했지만, JavaScript 개발자가 쓰는 oxc-parser가 돌려주는 AST는 ESTree를 따른다.

Oxc가 ESTree를 내부 AST로 그대로 사용하지 않고 표현을 더 세분화한다. 예를 들어 ESTree의 Identifier 하나를 역할에 따라 여러 타입으로 분리한다. (e.g. BindingIdentifier, IdentifierReference, IdentifierName)

SWC

SWC

An extensible Rust-based platform for the next generation of fast developer tools.

SWC(Speedy Web Compiler)는 "Make the web (development) faster"를 내세우는 Rust 기반 컴파일러다.

SWC의 특징

  • Babel보다 단일 스레드에서 20배, 4코어에서 70배 빠르다고 주장한다.
  • TypeScript/JavaScript 컴파일러이고, Rust와 JavaScript 양쪽에서 라이브러리로 쓸 수 있다.
  • Next.js, Parcel, Deno와 같은 툴들이 사용하고 있다.

SWC의 AST는 Babel AST와 비슷하지만 같지는 않고, ESTree와는 많이 다르다. ESTree 출력을 지원하느냐는 질문이 이슈로 올라왔고, 메인테이너는 당시 아직 지원하지 않는다고 답했다.

ESTree를 전제로 만든 다른 도구가 SWC 파서를 쓰려면 SWC AST를 ESTree로 바꿔야 한다. 예를 들어 JavaScript 모듈을 하나로 묶는 번들러 Rollup은 파서를 Acorn에서 SWC로 교체하면서 SWC AST를 ESTree로 변환하는 작업이 필요했다.

결론

파서를 둘러싼 고민은 두 갈래였다.
더 빠르고 직접 다룰 수 있는 파서를 만드는 일과, 파서가 달라져도 도구들이 같은 AST로 이어지게 하는 일이다.

새 파서는 계속 나왔지만, 도구들이 기대는 AST는 쉽게 바꿀 수 없었다. 그래서 ESTree로 이어지는 길을 따로 마련해야 했다.

파서는 달라져도, AST 형식은 도구들이 서로 연결되는 접점으로 남았다.


내용 및 이미지 출처

profile
어제보다 더 나은 오늘의 개발자 🍀

0개의 댓글