3-24. 이름으로 되찾지 않는다 — 런타임 helper의 심볼화와 그래프 유지 확대
3-23이 합성 변수에 심볼을 붙이기 시작한 구간이라면, 이어진 닷새는 나중에 이름으로 다시 찾아 연결하던 길을 하나씩 지운 구간이다. 생성 시점에 심볼을 등록하고 사후 재탐색을 걷어 냈으며, 단일 파일 변환의 런타임 helper와 번들러 런타임 factory도 심볼로 옮겼다. 번들러 사전 처리에서는 재분석 없이 기존 의미 그래프를 유지하는 문법을 계속 넓혔다.
이 편은 3-23의 기준 a9802a2e(10월 4일 23:49 KST) 이후부터 10월 9일 23:36 KST의 41a44cec까지 공개 main에 들어간 279커밋, 271개 PR(#5059부터 #5329까지)을 다룬다. 아래 변경은 main에 반영됐지만, 10월 10일 확인한 최신 GitHub 릴리즈는 여전히 v0.1.10이다.
날짜와 개수는 위 커밋까지의 main 커미터 시각을 한국 시간으로 집계했다. 271개 PR은 모두 main에 병합됐고, 병합일(KST) 기준으로 날짜순 47·45·60·56·63개다. 제목 유형은 fix 220개, test 21개, refactor 13개, perf 11개, docs 4개, style·ci 각 1개다.
생성 시점에 붙이고, 다시 찾는 길을 지운다
10월 5일 하루에 47개 PR이 들어갔고, 그중 32개는 제목에 bind·register·capture가 들어간다. _this·_arguments 캡처, ES5 파생 생성자의 _newTarget, Stage 3 데코레이터의 _metadata·_classDecorators·_classThis·멤버별 initializer 배열처럼 변환기가 만드는 이름을, 완성된 AST를 이름으로 다시 훑어 찾는 대신 만드는 순간 SymbolId와 owner 스코프에 등록한다.
그 끝에서 보정 경로가 실제로 지워졌다. #5092는 Stage 3 데코레이터 wrapper의 이름 기반 fallback을 없애고, 직접 등록되지 않은 생성 지역 변수가 있으면 추측하지 않고 실패하게 했다. 이어 #5096부터 #5099까지 네 PR이 남은 호출부를 하나씩 줄였고, 마지막 PR이 남은 trackGeneratedLocalSymbols 호출과 범용 GeneratedLocalSpec 스캐너 API를 함께 삭제했다. 이후 며칠 동안 generator wrapper 임시 변수(#5161), hoist된 임시 변수(#5165), async wrapper 임시 변수(#5266)에서도 이름만 보고 기존 심볼을 재사용하던 경로를 지우고, (var ScopeId, 할당 Span)이 일치할 때만 잇도록 바꿨다.
10월 5일 새벽의 #5062는 3-23 끝에 남은 결함으로 적었던 경우를 다뤘다. 분석기가 하나의 SymbolId로 합친 비단순 매개변수와 같은 이름의 본문 var를, 매개변수 초기화 식이 그 바인딩을 참조할 때 별도 심볼로 나누고 본문 실행 직전에 값을 복사한다. PR은 참조를 다시 본문 심볼에 두면 [4,4]로, 값 복사를 빼면 [5,4]로 회귀하는 것을 확인했다. 본문 lexical 선언의 TDZ는 여전히 남은 결함으로 문서에 기록돼 있다.
10월 6일 낮에는 #5115부터 #5120까지 여섯 PR이 private 루프 임시 변수, Flow match, 익명 클래스의 self 참조·static field·static block wrapper, 클래스 메서드 스코프를 차례로 고쳤다. PR 본문에 적힌 전체 오라클 기준 exact 누락 바인딩은 33개에서 0개로 줄었고, 미분류 참조는 #5115 직후 47개에서 #5120 뒤 8개가 됐다.
런타임 helper도 심볼이다
10월 7일 정오부터 저녁까지 #5177에서 #5203까지 27개 PR이 같은 제목 형식(emit … helper by symbol identity)으로 들어갔다. 단일 파일 변환이 출력하는 __extends·__generator·__rest·__async·__values부터 클래스·private·데코레이터·using helper까지, preamble 선언 이름을 호출부와 같은 helper SymbolId에서 가져온다. 호출 없이 다른 helper 안에서만 쓰이는 helper는 preamble 전용 합성 심볼로 등록했다.
이 작업이 기존 출력 결함도 드러냈다. #5196에 따르면 ES2017 출력에서 사용자 코드가 __asyncGenerator라는 이름을 쓰면 그 바인딩이 helper 선언을 덮고, 호출은 __asyncGenerator2를 가리켜 Node에서 ReferenceError가 났다. 10월 8일 0시 35분의 #5211은 helper preamble을 출력한 뒤 식별자를 문자열로 바꾸던 rewriter 자체를 제거했다. __asyncValues 안의 __values, async generator 안의 __await처럼 템플릿 속에 숨은 참조도 심볼로 등록했다.
번들러 쪽은 10월 7일 아침부터 움직였다. #5170은 CJS 래퍼의 암묵적 exports·module 매개변수에 SymbolId를 부여하고, #5172부터 #5176까지는 CJS 런타임 factory와 내부 지역 변수(cb·mod·__require 등)를 그래프 수준 합성 심볼로 옮겼다. 단일 번들은 linker의 일반 이름 결정에 넣고, 코드 분할·preserve-modules는 청크마다 rename 표가 초기화돼도 같은 이름을 복원한다. 이어 binary asset의 __toBinary(#5204), ESM factory __esm(#5205), __toESM·__export를 포함한 ESM interop helper 10종(#5206)이 같은 방식으로 들어갔다.
codegen이 이름을 추측하지 않게
codegen이 이름을 문자열로 치환하거나 추측하던 경로도 줄었다. 10월 6일 #5106은 namespace export 참조의 codegen 문자열 재작성을 없애고 변환기가 매개변수 심볼에 연결된 member AST로 낮추게 했다. #5107·#5108·#5109는 의미 정보가 있는 경로에서 namespace·enum IIFE 매개변수와 enum 멤버 owner의 SymbolId가 없으면 오류로 멈추게 했다. 10월 8일에는 ESM wrapper·Worklet 초기화·단일 파일·번들 본문의 TypeScript IIFE 출력에도 같은 요구가 적용됐다(#5245·#5246·#5247).
10월 9일 새벽 #5272는 codegen에 SymbolId 기반 최종 이름 resolver를 추가했다. linker rename, 변환 override, 합성 이름 순으로 이름을 고르고, 일반 식별자·namespace 접두사·enum IIFE 매개변수·export default 자기 참조가 이 resolver를 쓴다. 아침의 #5288은 합성 이름 allocator가 이미 내준 fallback 이름도 충돌로 보게 했다. _loop가 _loop2로 밀린 뒤 따로 _loop2를 요청하면 _loop22를 받는 식이다. 다만 이 resolver가 에픽이 목표로 하는 단일 이름 결정 단계를 완성한 것은 아니다. 같은 날 #5270 본문도 비-minify 최종 allocator를 남은 범위로 적었다.
재분석 없이 그래프를 유지하는 문법을 넓힌다
이 구간 PR 가운데 (bundler): retain이 들어간 제목만 83개로, 전체의 약 31%다. 3-23에서 시작한 방식 그대로, 번들러 사전 처리에서 어떤 모양이면 변환 뒤 의미 분석을 다시 돌리지 않고 편집된 그래프를 써도 되는지를 문법 단위로 넓혔다.
10월 6일 오후 #5124는 모듈 최상위의 이름 있는 빈 클래스 선언만 허용했다. 그 뒤로 메서드·static 메서드·접근자, 빈 생성자, 생성자 안의 리터럴 대입, 정확히 연결된 식별자 읽기, 단순 매개변수, var, 산술·단항·조건식, if·return·throw·반복문·switch·try·레이블까지 PR마다 한 단계씩 넓어졌고, 10월 7일 새벽 #5155의 for-in·for-of까지 이어졌다. 10월 8일에는 ES5 **·??·논리 대입(#5230부터 #5233까지)과 optional chaining의 여러 형태가, 10월 9일에는 public static field 초기화 식과 단순 let·const 루프 헤더(#5277·#5278·#5329)가 들어갔다.
각 PR은 허용 조건과 함께, 조건 밖의 모양이 재분석으로 돌아가는지를 음성 대조로 확인한다. 예를 들어 #5127은 메서드 하강이 전역 Object.defineProperty를 쓰므로, 소스가 Object를 바인딩하면 그래프를 유지하지 않는다. 10월 9일 제목이 perf인 11개 PR(static field 계열과 공백 축소 경로)도 본문에 실행 시간 측정값이 없으므로, 여기서도 속도 수치를 말하지 않는다. #5278은 ES5 루프 헤더의 TDZ 동작이 재분석 경로에서도 출력에 보존되지 않는다는 기존 문제를 확인했지만 고치지 않았다고 적었다.
옮기는 길에 드러난 출력 결함
이 구간에서 고친 결함 중, 생성된 코드가 실제로 실패하거나 원본과 다른 값을 내던 것만 추리면 다음과 같다.
- #5087(10월 5일) — Stage 3 데코레이터에서 이름이 같은 instance·static
method가 같은 데코레이터 배열을 써 ES5 실행 시 뒤의 값으로 덮였다. 멤버마다 충돌 없는 배열을 배정한다. - #5196(10월 7일) — 앞서 본
__asyncGenerator이름 충돌로 단일 파일 출력이ReferenceError를 냈다. - #5228(10월 8일) — UMD·AMD·IIFE 출력의 외부 모듈 factory 매개변수 충돌을 고쳤다.
@scope/foo와foo가 둘 다Foo가 돼 두 import가 같은 변수로 연결되거나, 외부 매개변수React와 소스의const React가 겹쳐SyntaxError: Identifier 'React' has already been declared가 나거나, 생성된 CJS wrapper 변수와 겹쳐 외부 값이 덮이던 세 사례를 재현했다. - #5229(10월 8일) — 공백 축소 출력의 CJS wrapper 매개변수
$e·$m충돌 검사가 원문 철자를 비교해,$e처럼 escape로 쓴 같은 이름을 놓치고 번들이SyntaxError로 실패했다. - #5237(10월 8일) — ES5 객체 구조 분해의 계산된 key를 한 번만 평가하도록 임시 변수에 캡처한다. 수정 전처럼 key 식을 다시 방문하게 되돌리면, getter가 다음 key를 바꾸는 입력에서 원본의
5 second 9대신9 second 9가 나온다. - #5290(10월 9일) — 번들된
using의 런타임 helper 호출을 같은 이름의 함수 매개변수나 directeval이 보는 사용자 바인딩이 가로챌 수 있었다. 모든 입력 스코프를 피해 별칭을 고르고, 코드 분할과 preserve-modules에서도 같은 이름을 쓰게 했다. - #5296(10월 9일) — direct
eval이나with때문에 이름을 바꿀 수 없는 스코프에서**를Math.pow로 낮추면Math가 사용자 바인딩에 붙잡힐 수 있었다. 이런 스코프에서는 원래 연산자를 남기고, 대상 타깃이**를 지원하지 않을 수 있다는 경고ZNTC1502를 낸다. - #5326·#5327(10월 9일) — ES5 optional chaining에서 optional method call 뒤에 tail이 이어지면 메서드의
this가 사라지던 결함과, 첫 optional call이 short-circuit돼도 뒤의.next접근과 인자 평가가 실행되던 결함을 고쳤다.
실행 실패를 고쳤지만 원인이 이관 과정의 누락이었던 경우도 있다. #5213(10월 8일)은 identifier minify가 CJS 콜백의 exports·module 매개변수 이름을 바꿀 때 본문 참조만 바뀌고 wrapper header는 고정 문자열로 남아 번들 실행이 실패하던 문제를 고쳤다. PR은 이를 #4819 이관에서 놓친 emitter 경계로 기록했다.
통과의 기준을 스키마로 잠근다
검사 쪽은 "보고서가 깨끗하다"는 판정 자체를 우회할 수 없게 만드는 데 집중했다. exact identity·strict coverage·scope owner·post-minify·사전 처리 보고서마다 필드 집합을 고정하고, 필드 누락·중복, 잘못된 값, 등록되지 않은 필드가 있으면 실패하게 했다(#5212·#5216·#5267 등). 10월 9일 #5319는 필드 이름과 분류를 fingerprint로 고정해, 개수만 같은 진단 항목 교체도 잡는다. 진단 파일 경로에 보고서 marker 문자열이 들어가도 오인하지 않도록 파서도 다듬었다.
오라클 fixture도 고정했다. #5255는 fixture 경로 목록 297개와 타깃 6개, 실행 수 1,782를 고정했고, #5264는 경로를 유지한 채 본문을 줄여도 걸리도록 파일 내용까지 fingerprint에 넣었다. 10월 9일 #5320이 fixture 하나를 더해 298개 × 6개 타깃이 됐다.
새 불변식도 생겼다. 서로 다른 lexical owner가 같은 ScopeId를 공유하면 실패하는 duplicate_scope_owner(#5227), 문장별 참조 위치와 한 참조 노드의 중복 사용 검사(#5235·#5236·#5237), 선언 행과 AST 선언 노드를 잇는 declaration_identity_mismatch(#5289, semantic codec v12), minify 뒤 감사에서 helper·전역 표식이 같은 이름의 지역 바인딩으로 재해석되거나 심볼 표 밖 ID가 남는 경우를 잡는 검사(#5279부터 #5282까지)다. 몇몇 PR은 기존 감사가 통과시키던 변조를 먼저 재현했다. #5286은 합성 바인딩을 형제 스코프로 옮긴 변이와 var를 잘못된 블록 스코프에 둔 변이가 기존 감사를 통과했다고 적었다.
CI는 초록이 아니었다
로컬 검증은 촘촘해졌지만 main의 CI 워크플로는 이 구간 대부분 실패 상태였다. 10월 5일 05:15 KST의 #5067 커밋 실행이 마지막 성공이고, 바로 다음 커밋부터 10월 9일 마지막 커밋까지 성공으로 끝난 실행은 없다. 그 사이 87회가 실패했고 174회는 취소됐다. 첫 실패는 Ubuntu 통합 테스트였고, 마지막 커밋에서는 통합·NAPI/CLI·macOS 네이티브·실제 프로젝트 스모크 테스트가 실패했다.
원격 CI 결과를 기다리지 않고 로컬 검증으로 병합했다고 적은 PR 본문도 12개다. 10월 8일 #5241은 base main의 CLI 테스트가 React.version을 기대하지만 출력이 같은 import를 React$1 별칭으로 참조해 실패하고, 스모크 테스트도 실패한 상태라고 기록했다.
남은 것
#4819는 10월 10일 확인 시점에도 열려 있다. 10월 5일과 6일에 잠깐 닫혔다가 곧 다시 열렸다. 마이그레이션 문서는 생성자 전수 이관, 통합된 최종 이름 결정, 재분석 제거를 아직 완료하지 않았다고 적고, 본문 lexical 선언의 ES5 TDZ도 남은 경계로 둔다. ES5/RN TLA 소비자를 promise로 잇는 PR #4631도 여전히 열려 있다.
main의 대기 중인 changeset 파일은 3-23 기준 커밋의 27개에서 이 편 마지막 커밋의 73개로 늘었지만, 이 구간의 변경을 담은 새 릴리즈는 확인되지 않았다.