3-13. Metro 경계 확정과 근본 수정 원칙
기간: 2026년 4월 15일 ~ 4월 18일 (4일) 커밋: zts 약 79개 핵심: Symbol Table SoA 이행, Bun 스타일 crash report, watchFolders, "zts vs 번개" 역할 분리, namespace re-export 재귀 getter 근본 수정, 친절한 에러(miette급) 방향, CLAUDE.md ↔ docs 참조 역전, "근본 수정" CLAUDE 규칙 명문화, Zig 0.16 업그레이드 검토, Svelte 번들 트리쉐이킹 에픽 시작
이 편의 위치
3-12에서 function_params 정규화와 ESTree 어댑터 채택이 끝나자, AST 안정화의 다음 단계가 떠올랐다 — 심볼 테이블 구조. 그리고 동시에 번들러의 외부 접점이 커질 대로 커지면서, 어디까지가 zts의 책임이고 어디까지가 번개(RN 얇은 래퍼)의 책임인가라는 경계 문제가 바닥부터 올라왔다.
이 4일은 그 질문에 답하는 구간이다.
4/15 — Symbol Table AoS → SoA
"API 시그니처 유지"라는 관행 의심
새벽:
"API 시그니처 유지"라는 내부 관행 자체를 의심한다. 외부 사용자가 없는 내부 API에 어제의 시그니처를 지키는 건 종종 잘못된 리팩터링 중단의 이유가 된다. Phase 3까지 밀고 가기로.
AoS vs SoA
아침:
심볼 테이블 설계 결정.
- AoS (Array of Structs, 현재 zts) —
[]Symbol, 각 Symbol이 flags/name/scope_id 등을 한 덩어리로 보유 - SoA (Structure of Arrays, oxc 방식) —
flags: []u32,names: []StringIndex,scope_ids: []u32등을 별도 배열로 분리
SoA의 이점:
- 캐시 지역성 — mangler가 flags만 돌 때 name을 같은 캐시라인에 끌고 올 필요 없음
- estree 인덱스화 — 각 배열이 자연스럽게 인덱스 테이블이 됨
- 장기 유지보수 — 필드 추가/제거가 한 배열에만 영향
Phase 4c-1 ~ 4c-4 PR 시리즈로 이행 시작.
cjs_wrap 분리 + single source of truth
CJS wrapping 로직을 cjs_wrap.zig로 분리. transformer/linker/codegen 세 곳에 흩어진 CJS interop 규칙을 한 곳에서 결정.
스트링 필드 물리 제거 논쟁
"stringly-typed" 필드를 떼어내면 140곳의 역탐색 비용이 생긴다. 경쟁 구현 조사 후 판단.
워크플로우 — 시리즈 PR은 직렬화
속도 우선. 로컬 통과 시 선머지, CI는 후행.
Crash Report — Bun 스타일
오후에 SIGBUS 크래시 처리 정책:
Zig의 panic handler를 붙잡아서 Bun 스타일 crash report로 변환:
사용자에게 깃헙 이슈 URL을 바로 제시. feat(diagnostics): Bun 스타일 crash report — panic handler + 신고 안내.
watchFolders 실작업
저녁:
Metro 호환 기능 중 watchFolders 옵션 직접 구현. 이 결정이 다음날의 Metro API 감사로 이어진다.
WASM 공개 시점
WASM 공개 시점 재검토. AST API 안정화와 세트.
4/16 — "zts와 번개는 어디까지 해야 하는가"
이 편 전체에서 가장 큰 결정 하나가 이날 오후에 있었다.
Metro API 표면적 감사
이 목록을 보면서 당연한 질문이 따라왔다:
플러그인 훅으로 외화하는가
웹과 RN이 같은 번들러를 공유하면서, 옵션 네이밍만 "assetPlugins"로 공유해선 안 된다. 각 플랫폼의 훅으로 내려야 한다.
에셋·OTA·CJS 변환
에셋 포맷 호환성, CodePush(OTA) 호환, 이미지 변환 경로를 모두 이날 정리.
결정 — 번개는 "RN만" 지원하는 얇은 레이어
저녁 7시:
경계가 확정됐다:
자연스럽게 zts 쪽에 훅들이 추가되고, 번개는 그 훅을 쓰는 얇은 래퍼로 수렴했다.
namespace re-export 재귀 getter 회귀
밤 10시:
namespace re-export가 자기 자신을 참조하는 getter를 만들어내는 회귀. 사용자는 번개 dev 서버를 직접 띄우고 생성된 번들을 직접 읽으면서 문제를 지목했다.
4/17 — 근본 수정, 친절한 에러, 24바이트 제약 재평가
"이게 정말 우리만의 문제인가?"
하루의 결정적 질문:
확인 결과 — esbuild/rolldown도 동일한 입력에서 동일한 재귀 getter를 만든다. 즉 원본 코드가 애초에 잘못된 경우. 레퍼런스가 우회했던 방식은 에러 리포팅. zts도 파싱 단계에서 에러를 던지는 쪽으로 전환.
rolldown 쪽에서 찍히는 동작과 동등하게 맞추는 선에서 정리.
miette급 에러
목표를 rust의 miette 수준 — 에러 코드 체계(ZTS0xxx) + 소스 포인터 + help + 관련 링크 — 로 잡음:
이 수준으로 가는 작업은 장기 에픽으로 등록.
CLAUDE.md ↔ docs 참조 역전
기존 구조: CLAUDE.md가 진실의 근원. docs는 부차적. 새 구조: docs가 진실의 근원. CLAUDE.md는 얇은 인덱스로 docs를 가리킨다.
장기적으로 docs는 웹사이트로 공개될 후보이고, CLAUDE.md는 세션 부트스트랩용이므로 중복을 유지할 이유가 없다.
24바이트 고정 AST 재평가
오후에 3-9/3-10의 자랑거리였던 "24바이트 고정 AST"가 도마에 올랐다:
24바이트 고정은 캐시 라인 최적화에는 유리하지만, 일부 노드는 extra_data 포인터로 우회해야 하고(메모리 추적 비용), AST 안정화/ESTree 어댑터 측면에선 불리하다. oxc 방식의 per-kind 가변 페이로드로 갈 여지 확인.
이성 회복
몇 시간 뒤:
과투자 회피. 24바이트 제약 완전 폐기는 보류. 대신 필요한 노드만 payload_kind로 확장 여지 남김.
"근본 수정" CLAUDE 규칙 명문화
하루의 마무리:
CLAUDE.md 개발 규칙에 명문화:
근본 수정 원칙. 밴드에이드로 현상을 숨기지 말고, 원인을 구조로 고친다. 테스트 실패 시: 기댓값을 구현에 맞추지 말고, 구현을 사양에 맞춘다. 회귀 발생 시: 회피 플래그가 아니라 구조적 수정. PR 범위를 벗어나는 구조적 문제가 보이면: 별도 PR로라도 근본을 고친다.
const enum / private field / optional chain 다운레벨
이 날 연쇄적으로 고쳐진 ES5 다운레벨링 버그들:
- const enum 재설계 — 표현식 평가기, shadowing, computed key 지원
parameter property this.x = xES5 class lowering- private field compound assignment (
#x += 1) (#1468) - optional chain private field read (
obj?.#x) (#1492) - destructuring assignment target 허용 (#1485)
#x **= n를 Math.pow로 변환 (#1486)- generator 내부 unlabeled break/continue state machine 변환
- variable declarator의 yield* delegate op 5 누락
4/18 — Zig 0.16, Svelte 트리쉐이킹 에픽
Zig 0.16 + async/await 검토
새벽:
Zig 0.16 업그레이드는 백로그 이슈로. async/await은 Producer-Consumer 파이프라인에 일부 적용 가능하지만 ROI 확인 필요. 구현은 추후.
package.json exports 우선순위
package.json의 exports 해석 우선순위. axios 같은 패키지가 import 실패. 롤다운·rspack 중 rspack 구현 기준으로 맞춤.
tree_shaker 리팩토링
이날 대규모 tree_shaker 구조 개선:
- BFS를 fixpoint 내부로 통합 — 기존엔 outer BFS + inner fixpoint의 이중 루프. 불필요한 재방문 발생
StmtInfo구축을 fixpoint 전으로 이동 — 한 번만 buildreference_count신호를 완전 제거 — stmt-level references로 대체 (다음 편 #1634 RFC의 복선)
tsconfig paths 재설계
- paths/baseUrl + CLI
--alias병합 지원 - 자동 발견 + paths wildcard 경고
- paths를 TS 공식 스펙에 맞게 재설계 — wildcard anywhere + 다중 후보
decorator 후속
- Stage 3 decorator + ES5 private backing WeakMap lowering
@decorator export [default] class보존--target=es5재방문 활성화 (decorator가 emit한 코드도 ES5로 다시 내려감)
minify_syntax
- boolean 리터럴
!0/!1축약 - 자동 define로 optional chaining + globalThis root 매칭
- E2E dangling 복구
에러 UX와 성능을 같은 PR에 넣을 것인가
에러 UX와 성능은 같은 PR로 묶지 않는다. 근본 수정하면 애초에 B안이 필요 없다.
Svelte 트리쉐이킹 에픽 시작
저녁:
CI 댓글에서 Svelte 번들이 esbuild/rolldown보다 크다는 것을 확인. 여기서 트리쉐이킹 개선 에픽이 시작된다. 롤다운을 직접 레퍼런스로 클론:
3-9에서 구현한 StmtInfo에 중복이 많다는 인식. 같은 정보를 stmt_referenced·reference_count·mangler liveness가 각자 들고 있다. 다음 편 RFC #1634로 직결된다.
4일간 관통한 것들
이어서 — 3-14에서
4/18 저녁 "statement-level symbol graph 완벽히 구현되면 필요 없다는거 아니예요?"가 다음 편의 주제를 정했다.
- References 배열화 RFC #1634 — stmt_info의 관계 정보를 References 배열 하나로 유도
- RFC #1672 — oxc 스타일 AST 재설계 — old_ast/new_ast 분리 → 단일 in-place mutation
- transformer epic + debug infra
- HMR 성능 분해 — detect / emit 단위로 프로파일
- require.context — Expo Router를 위한 RN 특유 API
- Expo virtual-metro-entry 파싱 실패 대응
다음 편 3-14. RFC 시리즈 — References·AST 재설계·HMR 프로파일로 이어진다.