3-22. 다운레벨링의 의미를 지키는 심볼 표
v0.1.10에서 dev 서버의 CSS 흐름을 고친 뒤, 다음 닷새는 트랜스포머 안으로 들어갔다. 출력에 오래된 문법만 남기는 것에 더해, 변환 전후 프로그램이 같은 값을 내는지를 확인하고 그 과정에서 드러난 결함을 고쳤다.
이 편은 2026년 9월 22일부터 26일 16:40 KST까지 공개 main에 들어간 95커밋을 다룬다. 기준은 80042462다. 아래 변경은 main에 반영됐지만, 9월 26일 확인한 최신 GitHub 릴리즈는 여전히 v0.1.10이다.
날짜와 개수는 위 커밋까지의 main 커미터 시각을 한국 시간으로 집계했다.
지원 비트 하나가 놓친 의미
출발은 타겟 판정이었다. async generator와 public class field를 각자의 지원 비트로 낮추고, ES5용 interop 산출물에 ES6 문법이 남지 않게 했다. 하지만 문법이 사라진다고 프로그램의 의미까지 보존되지는 않았다.
yield*가 위임한 iterator의 .return(v) 값을 보존하는지, async generator의 yield와 return 재개가 맞는지, 함수 본문을 다른 함수로 옮길 때 arguments가 원래 것을 가리키는지 차례로 드러났다. for-of가 일찍 끝날 때 iterator를 닫는 일, 루프마다 새 let 바인딩을 만드는 일도 같은 구간의 수정이다. 위임 반환값 수정과 iterator 종료 수정은 출력 문자열만 비교해서는 놓치기 쉬운 사례다.
객체 메서드의 super도 바깥의 같은 이름을 보는 것으로는 부족했다. 메서드가 속한 객체의 프로토타입을 보도록 고친 뒤, 그 객체를 전달하는 합성 매개변수 _obj가 사용자 이름을 가리지 않도록 다시 고쳤다. 개별 문법을 낮추는 일과 이름·스코프를 지키는 일이 계속 맞물렸다.
Node의 실행 결과를 기준으로
9월 24일 다운레벨 런타임 오라클 스위트가 들어갔다. 도입 시점의 프로그램 151개를 네이티브 Node로 실행하고, ES5·ES2015·ES2017·ESNext·Hermes 타겟의 번들에 minify 여부를 곱한 열 가지 결과와 비교한다. 여기서 Hermes는 출력 타겟이고, 실제 Hermes 기기에서 돌렸다는 뜻은 아니다.
알려진 실패는 이슈 번호와 함께 fixture의 어느 조합이 실패하는지를 기록한다. 실패 집합이 달라지면 실패하고, 고쳐져도 목록을 정리해야 한다. 테스트가 통과했다는 말만으로 모든 조합이 정상이라고 읽히지 않게 한 것이다. 이 스위트를 추가하는 중에도 구조 분해 관련 결함을 더 찾았다.
같은 날 단일 파일 변환 경로도 비교에 넣었다. 번들러의 이름 변경과 transpile의 이름 변경은 다른 경로이므로 한쪽만 정상이어도 충분하지 않다. 실행 기준은 Node 24 이상이며, 최신 Node가 ES5 산출물에 남은 최신 문법을 실행해 버릴 수 있어 타겟 문법 준수는 이 오라클만으로 검증되지 않는다. 현재 스위트의 범위
for-of·상태 기계 for-in·for-await를 공통 AST 풀이로 바꾸는 작업도 이 검증 기반 위에서 진행했다. 변환 경로마다 같은 반복문의 의미를 따로 구현하던 부담을 줄이는 방향이다.
이름 문자열 대신 어느 변수를 가리키는지
반복되는 결함의 구조는 #4760에 정리돼 있다. ES5에서는 블록의 let·const를 var로 낮추면서 같은 함수 안 이름 충돌을 풀어야 한다. 기존 구현은 방문 도중 이름 문자열 스택을 보고 x를 x$1로 바꿨다. 참조가 어느 선언을 가리키는지보다 문자열 이름과 방문 경로에 기대는 방식이었다.
분석기는 이미 원본 선언과 참조에 심볼 ID를 붙이고 있었다. 그래서 고치는 방향도 그 표를 쓰는 쪽이었다.
- 변환기가 새로 만든 사용자 식별자에서 심볼 ID가 빠지는지 검사한다.
- 기본값 매개변수·상태 기계·추출 루프처럼 참조가 새 노드로 옮겨가는 경로에 심볼을 전달한다.
- 식별자 생성 함수를 사용자 바인딩·합성 이름 등 가리키는 대상별로 나누고, 우회하는 직접 생성을 감사한다.
- 낮춘 뒤의 함수 스코프를 기준으로 심볼별 이름을 정하고, 기존 문자열 스택을 제거한다.
9월 25일에는 새 심볼 표와 옛 이름 스택을 병행 비교한 뒤, 이름 스택을 완전히 제거했다. direct eval이 있는 블록은 이름을 바꾸면 문자열로 참조하는 변수의 의미가 바뀔 수 있어 별도 처리가 필요했다. minify 역시 낮추기 전 스코프에서 이름을 정하면 서로 다른 블록이 같은 var로 합쳐진다. 변환 뒤 재분석과 export·import·클래스 식 이름 보존 보완이 함께 들어갔다.
실제 번들에서 사라진 코드도 복구했다
9월 25일에는 트리 셰이킹 결함 둘도 고쳤다. 클래스의 computed key가 별도 문장으로 이동하면 원래 소스 위치로 추정한 사용 문장이 틀려 namespace export가 사라졌다. 이를 분석기의 실제 참조로 찾도록 바꿨다.
또 const proto = Ctor.prototype 같은 별칭을 통한 쓰기를 죽은 쓰기로 지워, minify한 axios에서 AxiosURLSearchParams.append가 없어지는 경우가 있었다. 새 객체에 대한 쓰기만 제거하도록 범위를 좁혔다. 문법 변환의 내부 정리와 함께 실제 라이브러리의 동작을 복구한 변경이다.
같은 날 구조 분해의 나머지 요소가 대입에서 빠지거나, 객체 rest 선언을 낮추며 let·const가 var로 바뀌는 문제도 고쳤다. 배열 rest 수정, 선언 종류 보존이 각각 들어갔다. 클래스 밖으로 옮긴 static 필드 초기값의 this·super도 원래 클래스 기준으로 낮추도록 수정했다.
변환 중에도 살아 있는 심볼과 스코프
9월 26일에는 변환용 가변 심볼 편집기가 들어갔다. 원본 심볼을 복사하는 데서 더 나아가, 변환 중 생기는 임시 변수와 매개변수·helper import도 선언과 참조로 연결한다.
스코프를 소유한 노드를 변환 뒤까지 추적하고, 부모 스코프를 안전하게 바꾸는 경로를 붙였다. 루프에서 추출된 호출 인자와 복사된 참조의 원래 노드도 기록한다. optional chain·nullish·decorator·tagged template처럼 합성 식별자가 많이 생기는 경로들이 이 작업의 대상이다.
이 편의 끝은 다운레벨링 완료 선언이 아니다. 심볼과 스코프가 변환을 통과하는 방식을 구조적으로 고친 구간이다. ES5/RN TLA 소비자를 promise로 잇는 PR #4631은 9월 26일에도 열려 있고, 위 변경을 담은 새 릴리즈도 아직 확인되지 않았다. main의 진전과 사용자가 설치하는 판을 구분해 두었다.