3-14. RFC 시리즈 — References·AST 재설계·HMR 프로파일
기간: 2026년 4월 19일 ~ 4월 23일 (5일) 커밋: zts 약 142개 핵심: References RFC #1634 (PR1
4), oxc 스타일 AST 재설계 RFC #1672 승인, transformer epic + debug infra, HMR #1727 에픽 — lazy sourcemap·mtime cache·profile 계층화, ModuleGraph accessor 리팩터 #1779 (Phase 1a3), require.context #1579 (Phase 1~3), Expo virtual-metro-entry 우회, RegExp lookaround → JSC 위임, test262 162 회귀 복구, single unified mangler 진행 중 — 이 편이 담는 대부분은 에픽으로 오픈된 상태다. 머지 완료된 phase들과 아직 리뷰 중인 phase들이 섞여 있다.
이 편의 위치
3-13의 마지막 — 트리쉐이킹 에픽 시작 + "statement-level symbol graph 완벽히 구현되면 필요 없다"는 인식 — 이 이 편의 출발점이다. 같은 정보를 여러 자료구조가 중복 보관하는 현재 구조를 정리하고, AST 자체를 oxc 스타일로 재설계하는 쪽으로 방향이 잡혀 있다.
동시에 HMR 성능을 detect/emit 단위까지 쪼개 보기 시작했고, RN의 require.context/Expo 같은 생태계 깊은 곳의 문제로 진입한다.
4/19 — References 배열화 RFC #1634 (PR1~4)
매직 상수 수색
새벽:
"*" 같은 문자열 센티넬, 매직 넘버를 상수로 치환. 시리즈 PR은 직렬로만 — 동시에 여러 PR을 열면 리뷰 컨텍스트가 튀어서 금지.
--minify가 결과를 바꾸면 안 된다
--minify는 출력 크기만 바꿀 뿐, 동작은 바꾸지 않는다. 이 원칙은 이후 dead code elimination·const promotion 설계 모두의 기반이 된다.
mangler 축약 전략
mangler가 _default$3 같은 접미사를 더 짧게 — 전반적인 축약 전략 확대.
RFC #1634 — References 배열 하나로
이전 구조에서 stmt_info는 mangler liveness·stmt_referenced·is_exported 플래그 등을 중복 보관하고 있었다. 이게 동기화 버그의 원천이었다. RFC #1634:
- per-reference 배열을 재도입 (PR1/4 — #1636)
- mangler liveness를 References 데이터로 이전 (PR2/4 — #1637)
stmt_referenced중간 캐시를 References에서 재구성 (PR3/4 — #1638)- 문서 반영 (PR4/4 — #1639)
측정에 대한 의심
저녁:
레퍼런스 구현 분석 자체를 의심. 근거 재검증 후 진행.
런타임 헬퍼 축약과 tslib
런타임 헬퍼(__commonJS 등)를 짧게 축약 + tslib 외부화 검토. 4/22에 importHelpers 옵션 구현으로 실현.
statusLine 커스터마이즈
작업 환경 튜닝도 이날:
Opus 주간 쿼터 리셋을 KST 요일/시간 테이블로, 브랜치 → PR 링크로. 프로덕트는 아니지만 매일 쳐다보는 것이라 이게 결정 속도에 직접 영향을 준다.
4/20 — test262 회귀 복구, 단일 AST 대논쟁, RFC #1672 승인
데드 플래그 발견
새벽:
전혀 설정되지 않는 플래그가 존재. RFC #1634의 References 이전과 맞물려 제거.
"나중에 하자"를 의심하기
"나중에 하자"는 관성을 공격. 어차피 해야 할 거면 지금.
test262 회귀 복구
test262가 161~162건 실패 중. 원인은 directive prologue parser 변경. 커밋 범위를 찾아내 근본 수정. 3-3(Test262 100%)의 결과가 유지되어야 한다는 기준선 재확인.
단일 AST 대논쟁
이 편 최대 전환:
현재 zts의 AST는 old_ast / new_ast로 분리된 불변 AST. Transformer는 새 노드를 만들어 new_ast에 append한다.
HMR에서 모듈 재파싱 시 identity가 유지되지 않으면 캐시 무효화가 커진다. 다른 번들러가 HMR에서 더 빠른 이유 중 하나가 이것이다.
RFC #1672 승인
transformer epic RFC #1672 오픈 + 승인. Phase A/B/C/D로 구획:
- Phase A — RFC 문서 + 설계 확정
- Phase B — 선행 최적화 (compiled_cache, NAPI/CLI 통합 profile)
- Phase C — visit 함수들의 identity return (자식 unchanged 시 노드 재생성 스킵)
- Phase D — 실제 in-place mutation (D1a/b/c → D2 ...)
4/21 — Transformer epic, debug infra, HMR 병목 분해
soak test
soak test 개념 도입 — 동일 코드베이스에서 HMR 루프를 오래 돌리며 누수·누적 오류·메모리 증가를 관측.
디버그 인프라 선행
D1 구현 전에 debug infra 선행:
feat(log): category-toggleable debug log infrastructure— 카테고리별(transformer/linker/emitter) on/off- AST lifetime 디버그 인프라 — 각 노드의 생성/변환/소멸을 추적. D1 재진입을 위한 안전장치
Phase B/C 구현
Phase B (선행 최적화):
- B3 — first-build
compiled_cache재사용. 불변일 때 recompile 스킵 - CompiledOutputCache stats log 분리 + resolved module path를 stable 키로
- NAPI watch worker에 compiled_cache wire
Phase C (identity return):
- C2a —
copyNodeDirectidentity 반환 - C2b —
visitUnary/visitBinary자식 unchanged 시 identity - C2c —
visitListNode/visitExtraListidentity
Phase D 진입
- D1a — boundary/root 필드 활성화 (clone 경로부터)
- D1b-1 —
Transformer.ast를*Ast포인터로 전환 - D1b-2 — single-bundle in-place mutation
- D1c — splitting 경로도 in-place +
AstHandlingenum 정리 test(transformer): initInPlace 4종 유닛 테스트 추가
"일단 바벨은 제외하고"
HMR 루프를 detect(변경 감지)와 emit(재번들)로 쪼개서 각각 프로파일 타깃팅.
4/22 — HMR breakdown, lazy sourcemap, CLI/NAPI profile 통합
D1 리버트 + 방어선
새벽:
D 리팩터 직후 OOB 크래시. D1 리버트 + 회귀 방어 테스트 선행. test(transformer): initInPlace 4종 유닛 테스트 추가 (RFC #1672 D1b-2/D1c 검증).
Metro HMR이 빠른 진짜 이유
추론 위주 설명 거부 + 실측 요구. 아침에 결론:
바벨이 느린 게 아니라 Metro가 모듈 단위 캐시를 쥐고 있을 뿐. HMR 재번들 시 변경된 모듈만 Babel을 돌린다. 즉 파서 자체보다 모듈 단위 캐시 전략이 더 크게 효과를 낸다 — 하지만 일단 파서 최적화부터.
통합 프로파일
프로파일 시스템 계층화 (#1716 ~ #1726):
Scanner.scan타이머 (lex 시간 분리 측정)ResolveCache.resolve타이머- graph discover / finalize sub-phase
- emit_output 4단계 sub-phase (emit_prepare / emit_modules / emit_concat / emit_finalize)
- NAPI와 CLI가 동일한 profile 필드를 노출
BREAKING: phaseDurations레거시 이름 정리 (#1719)
부산물로 발견된 버그들:
- lightningcss optional dep를 any로 선언 — tsc 빌드 회복 (#1723)
- Zig 0.15.2 x86_64
@memsetencoder 버그 workaround (#1715) - wasm
clock_time_getWASI stub (#1717)
Lazy Sourcemap
Metro는 HMR 시 소스맵을 파일로 안 내려보낸다. sourceMappingURL만 주고, DevTools가 요청하면 그때 빌드.
#1727 Phase B — lazy sourcemap 인프라:
- Bundler: lazy sourcemap 인프라 +
SourceMapOptions(#1728) - Core (NAPI): lazy sourcemap getter + handle cache (#1729)
- Emitter: HMR per-module code에
//# sourceURL=<mod_id>주석 (#1730) - Bungae: HMR update에 sourceMappingURL만 부착 (#69)
- Bungae: lazy sourcemap 라우트 + build 단위 cache (#68)
HMR breakdown 가시화
성능이 돌아왔으니 UX 디테일 — Metro의 "Hot Reloading" 토스트를 재현.
bungae의 HMR breakdown 로그 개선 (#63 ~ #67):
ZTS profile sync— 새 필드 반영(event as any)캐스트 제거 (WatchRebuildEvent 타입 사용)- HMR breakdown 멀티라인 + graph/emit sub-phase 출력
formatHmrBreakdown헬퍼 추출,SUB_KEYS단일 소스
Linker/Graph 레벨 최적화
특정 라이브러리에서 유독 느린 원인 추적:
ns_export_cache를 Linker 필드로 승격 (thread-safe) (#1739)- link / metadata subpass profile scopes (#1738)
NamespaceAccessIndexshare across namespace imports (#1740)- graph discover parallel/serial split profile (#1741)
side_effects_cache+pkg_type_cache→pkg_info_cache통합 (#1745)package.jsontype필드 캐시 → date-fns 13× → 4× (#1743)
watcher-driven mtime cache
#1732: fs.stat 호출 자체가 HMR 루프의 세금이었다. watch가 쥔 mtime을 이용해 변경 안 된 모듈은 stat 스킵.
emit_concat pre-size
#1731: emit_concat의 module_output 버퍼를 pre-size로 allocation 횟수 감소.
importHelpers (tslib)
런타임 헬퍼를 번들에 인라인하지 않고 tslib에서 import. 여러 번들이 같은 tslib을 공유하면 중복 제거.
Windows CI
#1746: Debug build + tests를 skip. LLVM OOM on GitHub runner. 이 문제는 upstream Zig/LLVM 이슈라 워크어라운드.
4/23 — Mangler 정리, require.context, Expo, lookaround
mangler 메모리 릭 + single unified mangler
GPA 경고 기반 누수. 원인은 canonical_name 계층 역전 — mangler가 semantic에 있는 canonical_name을 소비하는 구조였다. 단일 unified mangler 리팩터로 해결:
mangler 내부에서 canonical 이름을 자체 관리. semantic은 건드리지 않음.
ModuleGraph 재설계 #1779 (Phase 1a~3)
현재 ModuleGraph.modules는 []Module slice. 이 slice가 여러 곳에서 읽히면서 모듈 집합 변경 시점이 암묵적이다. HMR/incremental 빌드에서 문제가 된다.
- #1780 (Phase 1a): ModuleGraph phase-tagged accessor 뼈대 —
modules.items직접 접근 금지, 단계별 허용된 접근자만 - #1781 (Phase 1b):
modules.itemscall site를 accessor 경유로 교체 (prod + test) - #1782 (Phase 2):
[]Moduleslice API 완전 제거 —Module.addDependency→ModuleGraph.linkDependency - #1783 (Phase 3):
ModuleGraph.modules를std.SegmentedList로 교체 — 삽입 시 재할당 없음, pointer 안정성 - #1784: SegmentedList 타입을
ModuleList상수로 추출 - #1785: ModuleGraph invariants 문서화
require.context / require.resolveWeak — RN Expo
require.context — 디렉토리를 런타임에 glob하는 webpack/Metro API. Expo Router가 이걸로 라우트를 만든다.
웹팩의 babel-plugin-transform-require-context는 너무 초기 파이프라인에서 치환해서 tree-shake가 어려워진다. zts는 번들 그래프 레벨에서 처리:
- Phase 1 (#1770):
feat(bundler): require.context AST detection + literal eval—require.context("./pages", true, /\.tsx?$/)의 인자 3개를 리터럴 평가 - Phase 2 (#1772): host plugin hook + graph processing —
onResolveContext훅으로 외부 plugin에 위임 가능 - Phase 2.5 (#1773):
feat(core): NAPI bridge for onResolveContext plugin hook— NAPI로 JS plugin 연결 - Phase 2.6 (#1774):
feat(parser): define table evaluator for require.context (process.env.X)— define 테이블을 같이 평가 - Phase 3 (#1775):
feat(codegen): require.context webpackContext IIFE emit— 런타임 IIFE로 webpack 호환 - 번개 #70:
feat(zts-bundler): require.context 지원 — onResolveContext plugin + Expo Router define entries
zts vs 번개 경계 재점검
4/16에 그었던 경계를 재점검. require.context 자체는 zts에 구현하되, RN 특유의 entry 정책은 번개의 onResolveContext 플러그인에 둔다.
RegExp lookaround — JSC 위임
RegExp lookaround((?<=), (?!)) — 단기엔 JavaScriptCore regex wrapper에 위임, 장기엔 PCRE2 통합. 이슈에 자세히 기록하고 백로그.
Expo virtual-metro-entry 파싱 실패
저녁:
Expo 번들의 virtual entry가 Hermes 파싱에 실패. Expo는 버전마다 babel-preset-expo가 달라진다. zts가 직접 포팅하면 버전 따라잡기 지옥.
당분간 번개 레벨에서 babel-preset-expo 사용 + 이슈화. 장기엔 Expo 플러그인 레이어로 빠진다.
transformer 세부 수정
같은 날 24 커밋 + PR 29건 속에서:
- JSX attribute 이름이 non-identifier 면
string_literal로 lower esm_wrapCodegen init에import_records전파 — require.context IIFE emitstable compiled_cache key via resolved module path
다음 세션으로의 승계
메모리 파일 + 부트스트랩 문구로 다음 세션에 승계. 이 패턴이 이제 정착됐다.
5일간 관통한 것들
4/23 시점에 진행 중인 에픽
"완성"은 아직
3-10의 문구 "Zig로 만든 JS/TS 트랜스파일러는 빠르고, React Native 번들러로 쓸 수 있다"는 4/23 시점에도 여전히 참이다. 다만 4/8~4/23의 16일이 증명한 것은 다음이다:
- "Hermes 구문 통과"와 "실제 RN 앱이 돈다"는 다르다 — 전자는 4/7, 후자는 4/16 전후
- "번들러"의 경계는 시장이 정해준다 — Metro 호환이라는 말은 의외로 많은 표면(resolveRequest/extraNodeModules/assetPlugins/customSerializer/CodePush/Expo)을 요구한다
- 성능은 한 번 측정하고 끝이 아니라 프로파일을 계속 쪼개야 보인다 — HMR 루프는 9일 지나서야 Scanner/Resolve/graph/emit 4단계로 분해됐다
- AST 설계는 2주짜리 프로젝트에서도 한 번 더 갈아엎게 된다 —
old_ast/new_ast분리 → 단일 in-place mutation. 커밋이 쌓일수록 구조 비용이 드러난다 - "구조적이라 스코프 밖"이라는 핑계는 금지 — PR 범위보다 근본 수정을 우선한다. 이게 CLAUDE.md 규칙이 된 이유
3-12 같은 다음 편은 RFC #1672 Phase D2 이후, 트리쉐이킹 에픽 결과, 그리고 "Expo가 실전에서 도는가"에 대한 답이 쌓이면 써질 것 같다. 아직 "완성"이라는 단어를 쓸 때가 아니다.