3-17. 0.1.0을 향해 — 플러그인 통일·대규모 파일 분리·배포 파이프라인, 그리고 Codex 회고
기간: 2026년 5월 8일 ~ 5월 12일 (5일, 5/12는 오전까지) 커밋: zntc 약 392개 (5/8 40 · 5/9 161 · 5/10 89 · 5/11 88 · 5/12 14) — 5/9는 단일 날짜 최다 커밋(161개) — 종전 최다 3/19(138)을 넘김 핵심: plugin 시스템 통일 (generic
unwrapNapi, comptime-generic per-hook 어댑터,HookContextout-param,buildSyncsync JS plugin), 대규모 파일 분리 리팩터 (~200 커밋 — codegen/transformer/graph/emitter/cli + 테스트 스위트를 수백 개 작은 모듈로), TS Project References (composite·tsc -b·isolatedDeclarations), test262 OS 불일치 디버그 (await regexp — macOS만 fail), npm 배포 파이프라인 (@changesets/cli·release.ts·publint·sherif·OIDC·9-platform prebuilt NAPI 매트릭스·11-패키지 lockstep), react-refresh 에픽 ($RefreshSig$·component registration), Hermes named-capture regex downlevel (#1063), MetafileAnalyzer (Treemap + import 그래프), codegen sourcemap names 배열, 문서 사이트 대규모 보강 (설치 탭·analyze 페이지·electron/RN init 가이드·"합쇼체" 통일·로드맵·벤치마크 재측정), e2e compat matrix (39 libs / 9 scenarios), JSX runtime helper scope isolation (#3068), 그리고 0.1.0 publish prep (배포는 아직) 진행 중 — "publish 준비"까지가 이 편의 끝이다. 실제 npm 배포는 아직 안 했다.
이 편의 위치
3-16은 5/1~5/7 — binding-lite fast path, 메모리 오염 근본 수정, stack-overflow hardening, RN codegen 매트릭스, @zntc/* 모노레포 분리, 번개 흡수, 그리고 5/7 15:40 KST의 이름 변경 — 으로 끝났다. 이 편은 그 뒤 5일이다. "프로덕션 레벨 배포에 부족한 게 뭐예요?"(4/28)에서 시작된 흐름이 0.1.0 배포 파이프라인 세팅까지 온다.
이 5일의 성격은 앞 편들과 조금 다르다 — 새 기능을 만드는 비중이 줄고, 배포 가능한 상태로 만드는 비중이 커진다. 코어 파일을 잘게 쪼개고, 11개 패키지의 버전 정책을 정하고, 9개 플랫폼 prebuilt를 CI 매트릭스에 넣고, 문서를 다듬는다. 그리고 마지막에, 이 17일을 관통한 Codex 병행 효과를 정리한다.
5/8 — plugin 시스템 통일, 그리고 코어 파일 라인 수
unwrapNapi(T) — 어댑터 한 줄로
플러그인 NAPI 경계가 hook마다 보일러플레이트로 두꺼웠다. 이날 그걸 정리한다 — generic unwrapNapi(T) 헬퍼, comptime-generic per-hook NapiPlugin/NapiSyncPlugin 어댑터, TLS 기반 plugin failure/sourcemap 채널을 HookContext out-param으로 교체, async/sync dispatcher를 generator로 통일. buildSync가 sync JS plugin을 지원하게(이전엔 미지원 정책이었던 걸 "언젠간 해야 하는 거면 다 해주세요"로):
semantic predeclare/hoist 수정(catch-block 함수 재선언, computed ctor/proto key, nested-block 함수 kind)도 이날. RN dev-server 터미널 UI를 번개 것과 정렬 + 콘솔 로그 v 토글.
"코어 파일들이 너무 라인수가 많은것 같은데"
여기서 5/8~5/11을 관통하는 대규모 파일 분리 리팩터가 시작된다. 먼저 테스트부터 — test(core): split ... 가 30개 넘게. 그다음 코어 코드.
test262 OS 불일치
await + regexp가 들어간 test262 케이스가 macOS에서만 fail하고 Linux/메인 CI는 통과. 자동 wakeup으로 [DBG-OS] 라인을 grep해가며 추적 — macOS/Linux의 test262 metadata parse 차이가 분기점(debug/test262-await-regexp-os-discrepancy 브랜치). 3-3에서 달성한 Test262 100%가 OS별로 미묘하게 갈리고 있었다.
5/9 — 최다 커밋의 날 (161개)
두 트랙이 동시에
이날 161개 커밋은 두 개의 큰 트랙이 겹친 결과다.
(1) 모노레포 publish-readiness — @changesets/cli + release.ts(dry-run-first), publint + sherif CI, publish-smoke + publish-install-test, prepublishOnly + PUBLISH.md, source-exports condition, RN runtime .js→.cjs, @zntc/rspack-loader 신규 패키지. 그리고 TS Project References 에픽 — composite, tsc -b, solution-style root tsconfig, isolatedDeclarations, web/RN/vite-plugin/wasm/init를 core/server에 wire, IDE.md 가이드.
(2) 대규모 파일 분리 — refactor: split ... 가 ~100개. codegen / transformer / graph / napi / es2015_class를 여러 helper 모듈로. runtime-helper 수정(es5-rn __extends$1 is not defined, canonical_name 보존, symbol-bound helper refs #2869), .js/.jsx를 ESM으로 파싱(esbuild/swc/rollup 정렬 → 이후 CLI/transpile 경로로 좁힘), .cjs→script/.cts→module, for-of/for-loop _loop async-marker 전파.
rspack builtin:zntc-loader?
@zntc/rspack-loader 신규 패키지 — rspack의 builtin:swc-loader 같은 자리에 들어갈 zntc 로더. 메모리 누수 #2869/#2868도 이날 근본 수정(symbol-bound가 다른 번들러들도 다 그렇게 하니 그게 낫지 않을까요?).
5/10 — 9-platform prebuilt, react-refresh, 배포 파이프라인
NAPI 플랫폼 매트릭스
9개 플랫폼 — darwin x64/arm64, linux x64/arm64 (gnu + musl), win32 x64/arm64/ia32. loader가 libc(gnu vs musl) 감지, zig-out/lib/zntc.node 단일화, win32-ia32 smoke(Node 22 LTS), release.yml 매트릭스 NAPI build + npm publish job. @zntc/core dual ESM/CJS exports.
배포 파이프라인 — OIDC
GitHub Actions에서 OIDC short-lived token으로 npm publish, rc 버전 대응. 세팅만 — "배포는 하지말구". 11개 패키지의 버전 정책도 정했다:
→ lockstep(한 핫픽스에도 11개 버전이 같이 올라감) 정책 + lockstep 강제.
react-refresh 에픽
$RefreshSig$ hook signature opt-in(reactRefreshHookSignatures), const Foo = () => ... 컴포넌트 등록, reactRefresh를 TranspileOptions에 노출, Vite와 동일한 경로 필터. Hermes named-capture-group regex downlevel(#1063), babel wrapRegExp parity, bundler lazy-barrel/wrapper-barrel 분리, mangler cross-module Phase A shadow guard, vite-plugin-zntc→@zntc/vite-plugin도. mtime_map upsert 누수(#2868), forwardClientLogs deep-clone 누수(#2885) — RN dev 메모리 누수 진단의 결과:
5/11 — 0.1.0 prep, MetafileAnalyzer, 문서 대보강
"이제 슬슬 배포하려고 하는데"
이날은 배포 직전 전수 점검의 날이다.
chore(release): 0.1.0 publish 준비 — 메타데이터/문서/배너, RELEASE_STRATEGY.md. OutputFile을 esbuild 호환 모양으로(contents Uint8Array + lazy text getter). jsx: 'preserve' 모드 + return-ASI hazard 수정(#3056). vite-plugin virtual module ID + query-param sub-import(#3022). HTML EJS 스타일 ZNTC_* env-var 토큰 치환(XSS 방어 — html-env EJS token replacement 테스트 FAIL을 TDD로 잡음). mangler reserved-name 수정(jotai/svelte/three — #2965/2958/2971/2959). writer-edge over-fire 회귀(#3012). codegen arrow-body leftmost-{ paren wrap(#2964), destructuring shorthand longhand expand(#2977). resolver 디렉터리 레벨 realpath 캐시.
MetafileAnalyzer
MetafileAnalyzer — esbuild metafile을 Treemap + import-graph로 시각화하는 analyze 페이지. codegen sourcemap에 name_index(names 배열, oxc 스타일 explicit mapping)도.
문서 사이트 대보강 + 로드맵 + 합쇼체
설치 페이지를 pnpm/bun/npm 탭으로, electron/RN/RN-expo init 가이드, native-transforms·plugins dev guide, "한다"체와 "합니다"체 혼재를 합쇼체로 통일, 로드맵 페이지, 벤치마크 재측정. @zntc/init vite/rspack/web 오버레이 모드.
대형 번들 프로파일링 + 워커풀
resolve.realpath cache, mangling 병렬화, scan.worker per-file overhead — "지금도 다른 번들러보다 훨씬 빠른데" 그 위에서 또 한 겹. (워커풀 재설계는 시도 후 일부 롤백.) axios FAIL CI 회귀도 추적.
5/12 — 출시 직전 최종 점검 (오전까지)
e2e compat matrix — 39개 라이브러리 / 9개 시나리오 / real-app shape, 중앙 포트 할당(tests/e2e/tests/ports.ts). JSX runtime helper scope isolation #3068 — synthetic-import bypass를 없애고 transformer가 정식 AST 노드를 emit하도록(사용자 식별자 충돌 회피). dev-server proxy/middleware의 webpack 갭을 문서화 + 옵션 추가. 사용자 페이지에서 PR#/D-ID/perf-history/구현 비교 제거(공개용으로 정리). HTTPS dev server / Web Worker 사용자 가이드.
이 시점에서 zntc는 — 트랜스파일 ~2ms, 번들 ~3ms(소형), rolldown 대비 ~19×, 340+ 문서 페이지, 9 플랫폼 prebuilt, @zntc/{core,web,server,react-native,vite-plugin,wasm,rspack-loader,init,shared} + 9 platform 서브패키지, RN 데브서버(Metro 호환) + Flow + 워클렛 + styled/emotion 네이티브 트랜스포머 + core-js 자동 폴리필 — 0.1.0 배포 직전이다. 아직 npm에 올리진 않았다.
Codex 병행 효과 회고 — 두 AI, 하나의 저장소
4/26부터 5/12까지, 이 17일은 Claude Code(zts/zntc 폴더)와 OpenAI Codex CLI(zts-codex 폴더)를 병행한 기간이다. 처음 받았던 질문 — "메모리가 zts로 가도 이어지는거?"(3-1, 3/18) — 의 17일 뒤 버전은 "상위폴더 zts에 똑같이 있는데 여기서 B 작업해주실래요?"(5/1)다.
구조: 두 효과는 별도 클론, 같은 origin remote(ohah/zts → 5/7부터 ohah/zntc). 사용자는 GitHub 이슈 URL과 PR URL을 양쪽에 그대로 던졌고, Codex가 만든 PR도 같은 저장소·같은 라벨·같은 assignee(ohah)로 올라갔다 — 하나의 이슈/PR 풀을 공유. Codex 세션은 거의 매번 작업 시작 시 git switch main && git pull로 상대방 결과를 흡수했고, "제가 그사이에 메인 머지 한거 있는데 그거 확인 한번 해주세요"·"메인기준으로 리베이스 하세요" 식으로 사용자가 수동 동기화했다.
역할 분담의 무게중심 (Codex 쪽):
- 번들 크기/속도 회귀 전수조사 + root cause 수정 — mobx/d3/dotenv/toolkit/lru-cache/lodash-es가 "왜 rolldown보다 큰가/느린가" → numeric const folding, lazy barrel 로딩
- Vite 스타일 app builder parity —
zts dev/build/preview, HTML entry,.env, PostCSS/Tailwind, dev CSS HMR, 에러 오버레이 - binding-lite / semantic-light 성능 아키텍처 POC + 회귀 fixture 대량 추가
- core-js 자동 폴리필
- zts → zntc 리네임
- 코어/테스트 파일 라인 수 줄이는 대규모 리팩터(5/8~5/11)
- CI flaky 수정 + CI 병렬화 + 벤치마크/문서 사이트 정비
5/11~5/12 zntc 본체 커밋 메시지(refactor: split ... tests, perf(fs), perf(resolver), feat(core): OutputFile shape esbuild 호환, chore(release): 0.1.0 publish 준비)가 이 Codex 대화 흐름과 정확히 겹친다 — 이 시기 본체에 머지된 상당 부분이 Codex 산출물이다.
Codex에 대한 교정 — Codex 로그에 "코덱스가 못함" 같은 직접 비교 발언은 없지만, 운영상의 마찰은 자주 보인다: 워크트리 자동 생성 거부("그냥 워크트리 만들지 말고 그냥 메인기준으로"), 메인 기준 강제 반복, "무조건 루트커즈 수정", 한글 주석 규칙 위반 지적, 같은 전수조사를 "할 때마다 계속 나오네요"라며 여러 번 시킴, <turn_aborted>(턴 강제 중단)가 4/30·5/2·5/3·5/5·5/7·5/8·5/10·5/11 등 거의 매일, "왜이리 오래걸려여"·"PR 너무 많은거 아니야?" 같은 속도/PR 분할 불만. 컨텍스트 연속성은 "매 작업마다 이슈 #2352에 진행상황 기록"으로 풀었다 — 3-14의 "메모리 파일 + 부트스트랩 문구" 패턴의 Codex 버전.
왜 둘을 썼나 — 이건 대화 로그에 직접 답이 없다. 추론하자면: (1) 처리량 — 같은 17일에 ~1,300개 커밋(5/9 하루 161개)은 한 AI/한 워크스페이스로는 어려운 규모다. (2) 작업 격리 — "이 폴더에서 다른 작업도 할 거라서" 처럼, 큰 리팩터(파일 분리)와 기능 작업(RN codegen, 패키지 분리)을 물리적으로 다른 디렉터리에 두면 머지 충돌과 컨텍스트 오염이 준다. (3) 비교 자체 — 같은 이슈를 양쪽에 던져보고 어느 쪽 접근이 나은지 보는 용도(5/9 우리꺼 넣었을때랑 swc 넣엇을때랑 벤치마크도 비교해줘처럼, "비교"가 이 사람의 기본 의사결정 도구다).
5일간(+17일간) 관통한 것들
"완성"은 아직 — 그러나 "배포 직전"
3-14의 마지막에 썼던 다섯 문장 — "Hermes 구문 통과와 실제 RN 앱이 도는 건 다르다", "번들러의 경계는 시장이 정해준다", "성능은 한 번 측정하고 끝이 아니다", "AST 설계는 2주짜리에서도 한 번 더 갈아엎는다", "구조적이라 스코프 밖이라는 핑계는 금지" — 에, 4/24~5/12의 19일이 다섯을 더한다:
- "학습 프로젝트"라는 자기소개는 어느 시점에 슬그머니 바뀐다 — 4/28 "프로덕션 레벨 배포 하려면 부족한 게 뭐예요?"가 그 분기점. 코드가 충분히 쌓이면 "공부용"이라는 말이 안 어울리는 순간이 온다.
- 이름은 코드보다 늦게, 그러나 갑자기 정해진다 —
@zts가 npm에 선점돼 있다는 걸 배포 준비하다 발견. 한 저녁의 브레인스토밍 끝에zntc, 그리고 687 파일 한 커밋. - 모노레포 분리는 "배포 가능한 모양"의 일부다 — 단일 저장소로는 "vite에선 코어만, 자체 실행은 코어+서버" 같은 설치 시나리오를 표현할 수 없다. 패키지 경계는 사용자의 설치 명령에서 역산된다.
- 두 AI를 동시에 쓰면 처리량은 늘지만 동기화 비용이 생긴다 —
git switch main && git pull, "메인 머지 한 거 확인해주세요",<turn_aborted>— 17일 내내 수동 동기화. 그래도 5/9 하루 161 커밋은 그 비용을 치를 만했다. - "배포 직전"과 "배포"는 또 다르다 — "세팅만 다 해주세요 실 배포는 하지말고"가 5/10~5/11 내내 반복됐다. 0.1.0 publish prep까지 와도, 실제로
npm publish를 누르는 건 아직 안 한 일이다.
다음 편은 npm publish가 눌리고, 첫 사용자가 이슈를 열고, "RN+Flow 되는 빠른 번들러"라는 자기 인식이 시장에서 검증(혹은 반박)되면 써질 것 같다. 아직 그 시점은 오지 않았다.