3-11. Metro 동등성 — RN HMR 재설계
기간: 2026년 4월 8일 ~ 4월 10일 (3일) 커밋: zts 약 76개, 번개 연동 포함 100+ 핵심: 롤다운 기반 레퍼런스 번들러와의 라인 대조 방식 확립, dev 엔진 프로덕션 파이프라인 통합, 플러그인을 stdio → bun:ffi → C NAPI로 전환
주의 — 이 편도 진행 중인 작업의 일부다. 3-10(~4/7)에서 나왔던 "Hermes 통과·esbuild 1.24×"의 숫자가 "실제 RN 앱에서 HMR이 도는가"라는 실전 기준으로 옮겨 가는 구간이다.
3-10의 숫자는 기준선이지 결산이 아니었다
3-10까지의 20일간 **Test262 100%, kangax ES5 99%, Hermes 구문 통과, esbuild 1.24×**라는 숫자가 나왔다. 그 시점에 결산하고 싶은 유혹이 있었지만, 번개(bungae) — RN용 얇은 어댑터 — 위에 zts를 얹어 실제 RN 앱의 HMR을 돌리는 순간, 다음 전선이 열렸다.
4/8 — "레퍼런스는 어떻게 하고있는데요?"
첫날 새벽 4:22의 첫 발화.
이 한 마디가 이후 16일을 관통한다. 여기서 "레퍼런스"는 번개(bungae)에 포함되어 있던 롤다운 기반 레퍼런스 번들러를 가리킨다. 이후 모든 HMR·워클렛·재귀 getter 문제를 "레퍼런스 결과물과 라인 단위로 대조하며" 좁혀 들어가는 작업 패턴이 이 구간의 기본이 됐다.
조용한 실패 — 에러는 없는데 HMR은 안 됨
원인은 window.__REACT_REFRESH_RUNTIME__ 주입 시점이 번들러 prelude가 아니라 엔트리 이후였던 것. 해결은 "레퍼런스 prelude 방식" — refresh 런타임을 prelude에 선주입.
dev 엔진을 프로덕션 파이프라인에 합치다
같은 날 구조적 변화가 있었다. 이전까지는 emitDevBundle()이라는 별도 경로가 있었고, 프로덕션은 emitWithTreeShaking()이었다. 두 경로가 갈라져 있다 보니 dev에서 되던 게 프로덕션에서 안 되거나 그 반대가 쉬웠다.
플레이그라운드 모바일 반응형
부산물로 문서 사이트까지 건드렸다:
좌우 분할이 모바일에서 뭉개지던 문제 + 코드블록/표 스타일 개선 + ROADMAP.md 구현 상태 최신화.
벤치마크 회귀
같은 저녁:
모듈 수가 커질수록 롤다운과 zts가 역전되는 구간이 있었다. 아키텍처 전면 개편은 보류하고, 파서 재실행 최소화 캐시레이어만 우선 채택 — 며칠 뒤의 compiled_cache 설계로 이어진다.
프로덕션 레벨 로드맵 점검
밤:
문서-구현 갭 파악. Stage 3 decorator, mangleProps, Module Concatenation, core-js 옵션 등이 백로그로 적재됐다.
4/9 — Metro 동등성의 잔여 디테일
하루 종일 "Metro는 되는데 우리는 안 된다"의 반복.
HMR 실패의 진짜 원인은 모노레포 경로 해석. Metro와 zts의 모듈 ID 발급이 모노레포 루트 기준에서 어긋났다. 레퍼런스 기반으로 재설계:
첫 번째 "된다"
동일 내용 저장에서도 풀 리로드가 터지는 엣지케이스. graph_changed 감지를 경로 SET 비교로 개선.
남은 세 가지 시각 이슈
호환성의 기준선 = Metro 동등성. 그리고:
사용자가 직접 RN의 NativeModules 경로로 찍어가며 은닉 API를 찾아낸 순간. 원인은 globalThis.__turboModuleProxy 경로가 아니라 **NativeModules.DevLoadingView.hide()**가 RN Fabric에서의 올바른 호출.
소스맵 2종의 비대칭
저녁:
소스맵이 두 군데서 쓰인다 — DevTools 소스 탭(파일 매핑)과 콘솔 라인(error stack frame). 소스 탭은 OK인데 콘솔은 한 줄씩 밀림. 다음 날 새벽까지 이어지는 이슈.
4/10 — 플러그인 스택 전면 교체
3-10 시점까지 zts 플러그인은 stdio 기반 subprocess였다. 각 transform 요청마다 프로세스를 띄우고 JSON을 주고받는 구조. 대규모 번들에서 확실한 병목.
벤치마크 자체에 대한 의심도 같이:
7ms는 캐시 결과였다. "말이 안 되는 수치"를 본능적으로 걸러내는 감각. 캐시 비우고 실측 재지시.
bun:ffi를 잠깐 → NAPI로 최종
오후 1시부터:
@zts/ffi로 분리하고 core를 리네이밍. 하지만 몇 분 뒤 다시 뒤집는다:
이유는 단순했다 — Node·Deno·Bun 모두 커버하려면 NAPI가 필요하다. bun:ffi는 Bun 전용. @zts/ffi → @zts/core 리네이밍과 함께 C NAPI 바인딩으로 전면 전환. zig build napi가 기본 빌드 타겟에 추가됐다.
플러그인 API 스타일 결정
같은 날 플러그인 API 형태도 확정됐다:
esbuild 스타일로 전면 통일:
onResolve({ filter, namespace }, handler)onLoad({ filter, namespace }, handler)onStart/onEnd라이프사이클 훅
여기에 **Vite/Rollup 어댑터(Phase 4)**를 얹어 플러그인 생태계를 흡수한다. Vite 6+의 번들러는 롤다운이므로 비교군도 거기에 맞췄다:
import.meta.glob
같은 날 Vite 호환의 실용적 필수:
eager: true / import: '*' 옵션 지원, 실제 Vite 실전 테스트 5개 추가. Vite/Rollup 어댑터의 첫 번째 검증.
Serve 모드 런타임 고민
dev server까지 Bun 런타임 채택 논의. 당장은 보류 — Node 호환을 우선.
회귀와 빌드 환경 정비
저녁:
zts와 번개 두 쪽 모두 FFI → NAPI로 전환. 외부 개발자의 postinstall 실패는 mise로 해결(mise.toml에 zig 설치 명시 — 4/11 새벽 작업으로 이어진다).
3일간 관통한 것들
이어서 — 3-12에서
4/10 저녁 "플러그인 스택 전면 교체"가 끝나자 새로운 문제 목록이 따라왔다:
- NAPI 전환 이후의 회귀 (4/11 새벽
"napi로 바꾸기전엔 됐었는데 왜 됐던거야 그럼?") - Reanimated worklet이 파싱 실패 (";'" expected)
- TC39 Stage 3 decorator — esbuild 식이 아니라 TS 식으로 재결정
- "코드젠이 정확히 어떻게 하고 어떻게 쓰는건지 설명좀" — AST 안정화 토론의 시작점
다음 편 3-12. 호환성 겹층 — 워커렛·Stage 3 decorator·ESTree 어댑터로 이어진다.