3-15. "프로덕션이라는 말" — config 시스템·CJS DCE·styled/emotion 트랜스포머
기간: 2026년 4월 24일 ~ 4월 30일 (7일) 커밋: zts 약 543개 (4/24 57 · 4/25 102 · 4/26 76 · 4/27 34 · 4/28 91 · 4/29 90 · 4/30 93) 핵심: type-only import elision 근본 수정 Phase D, oxc 스타일 AST cosmetic refactor #1802 (
--strict-cosmeticCI 게이트), Astro Starlight 문서 사이트 전면 개편, manualChunks 에픽 #1027 (Rollup parity 64%→93%), FileSystem 추상화 + WASM 번들러 #1885, runtime-helper virtual module #1961 + ES5 헬퍼 null-prefix 근본 수정, config 시스템 (zts.config.*/.env/--mode/extends/ workspace), CJS DCE 에픽, styled-components·emotion 트랜스포머 정식 스펙 parity, "프로덕션 레벨 배포에 부족한 것" 첫 점검, 그리고 OpenAI Codex 등판 진행 중 — 이 편이 담는 대부분은 여전히 에픽으로 열려 있다. 머지된 phase와 리뷰 중인 phase가 섞여 있다.
이 편의 위치
3-14는 4/19~4/23의 RFC 시리즈 — References 통합, oxc 스타일 AST 재설계(D1 리버트 후 debug infra 위에서 재진입), HMR 프로파일 계층화, ModuleGraph accessor, require.context, Expo — 로 끝났다. 그 마지막에 "4/23 시점에 진행 중인 에픽" 표가 길게 붙어 있었다. 이 편은 그 표의 항목들이 하나씩 닫히고, 동시에 "학습 프로젝트"라는 자기소개를 슬그머니 내려놓고 "프로덕션 레벨 배포 준비"라는 단어를 처음 입에 올린 7일이다.
그리고 이 7일 안에 또 하나의 변화가 있다 — OpenAI Codex CLI를 병행 투입하기 시작했다. 처음엔 같은 zts 폴더에서, 4/29부터는 zts-codex라는 별도 클론에서. 두 AI가 같은 GitHub 저장소·같은 이슈/PR 풀을 공유하며 갈라져 작업하는 구도가 이때 만들어졌다. 자세한 회고는 3-17 끝에 둔다.
4/24 — type-only import 근본 수정, AST cosmetic refactor, 문서 사이트 갈아엎기
require가 없다
이날 오전, RN 앱이 또 깨진다:
원인은 type-only import elision이 불완전해서 import type만 남은 모듈 참조가 런타임 require()로 새어 나간 것. 4/23 RFC #1672 시리즈 때부터 끌고 온 project_type_only_import_elision.md의 "구현 설계" 섹션을 꺼내 축 A → 축 B 순서로:
이 "근본으로 / 무조건 근본 / band-aid는 싫다 / 다른 번들러는 어떻게 하는데"의 4박자가 이 편 7일을 (그리고 이후 전 기간을) 관통한다. type-only import elision은 16개 바인딩 캡을 제거하고 unresolved는 soft-fail하는 쪽으로 Phase D까지 진행(#2466 등은 5/3에 닫힌다).
AST cosmetic refactor #1802 — 그리고 --strict-cosmetic
3-14의 RFC #1672(oxc 스타일 단일 in-place AST)는 D1 리버트 이후 "debug infra 위에서 재진입"이 정답이었다. 그 재진입의 첫 단계가 cosmetic refactor — 동작을 바꾸지 않고 노드 구조만 oxc에 맞춰 정리하는 카테고리 A/B/B2/C/special. --strict-cosmetic CI 게이트를 만들어 "이번 PR은 cosmetic만"임을 기계로 강제했다. Tag↔variant 정합성 정적 감사(#1797), flow_match_arm 태그 분리(#1822)도 이 흐름.
"진짜 Full AST로 재작성" — 그리고 백로그
같은 날 더 큰 질문이 떠올랐다. std.zig.Ast.parse 같은 네이티브 파서 수준으로 zts AST 자체를 다시 짤 것인가?
이득(parse-once, identity preservation 끝까지)은 분명했지만 비용이 더 컸다. cosmetic refactor + Phase C/D로 충분히 수렴할 수 있다고 보고 백로그. (3-14의 "AST 설계는 2주짜리 프로젝트에서도 한 번 더 갈아엎게 된다"는 교훈이 또 적용됐다 — 이번엔 "더 갈아엎지 않기로" 결정했다는 게 차이.)
문서 사이트를 갈아엎다
Astro/Starlight 기반으로 랜딩 페이지를 Zig 오렌지 팔레트 + 스플래시 레이아웃 + 8그룹 사이드바로 재설계. 이날 저녁 내내 히어로 정렬·다크모드·레이어 깨짐(* { margin: 0 }이 범인) 보고가 반복됐다:
문서 사이트는 이후 5/11까지 계속 보강된다 — 이 프로젝트가 "공개"를 진지하게 생각하기 시작했다는 가장 가시적인 신호다.
require.context Phase 4(#1579 — graph dep + tree-shake root 연결), for-of+let 클로저 캡처 → _loop 추출(#1807), IIFE+external+globals(#1824)도 같은 날.
4/25 — manualChunks parity, WASM 번들러, 스모크 테스트 폭격
manualChunks 에픽 #1027
Rollup/rolldown의 manualChunks(어떤 모듈을 어느 청크에 넣을지 사용자가 지정) — 부분 구현 상태였다. Phase 1(substring 매칭), Phase 2(함수 resolver), NAPI 브리지, meta API까지 채워 Rollup parity를 **64% → 93%**로. inlineDynamicImports도 같이.
FileSystem 추상화 + WASM 번들러 #1885
"Playground 번들러 모드" — 브라우저에서 zts 번들러를 돌리려면 파일시스템이 추상화돼야 한다.
RealFS / VirtualFS(host-JS 콜백) / ResolvedModule 유니온, wasm-bundler 빌드 타깃, Astro COOP/COEP, playground 번들러 페이지. 트랜스파일 wasm과 번들러 wasm을 별도 페이지로 분리:
스모크 테스트 폭격 + symlink farm
스모크 테스트 라이브러리 대량 추가 + pnpm/bun symlink farm 해석(#1921). async-generator/__asyncGenerator(#1911), async this binding(#1909), yield* iterable wrap(#1910), if-await ES5 generator state-machine(#1887) — ES5 다운레벨의 빈칸도 메웠다. bundle-perf regression-guard 인프라 + baseline도 이날.
4/26 — Codex 등판, runtime-helper virtual module, ES5 헬퍼 null-prefix 근본 수정
"당신은 버그 헌터예요"
이날 처음으로 OpenAI Codex CLI에 작업이 떨어진다 — 처음엔 같은 zts 폴더에서:
Codex의 무게중심은 처음부터 분명했다 — 번들 크기/속도 회귀 전수조사 + root cause 수정, 그리고 Svelte/effect/date-fns 같은 라이브러리에서 "왜 우리가 rolldown보다 큰가". (4/27 Codex: purity + statement DCE 계획 세워주세요 / 세우신 계획이 루트커즈 수정이예요?)
ES5 헬퍼 null-prefix — 모든 헬퍼를 바꿔야 하나
__commonJS, __create, __defProp 같은 런타임 헬퍼가 splitting 모드에서 제대로 분배되지 않거나 이름이 충돌하는 문제. oxc는 헬퍼를 virtual module로 빼서 그래프에 정식 노드로 넣는다:
runtime-helper-virtual-module 에픽 #1961 — loader 인프라, 14개 누락 헬퍼 매핑, splitting 모드 분배, single-bundle 활성화, transformer pre-pass + builtin plugin. 동시에 ES5 class/super 다운레벨의 정확성을 대대적으로 쓸었다 — NewTarget 캡처, derived-ctor super receiver _this(#2022), wrapped-super lowering(#2030), optional-chain super(#2034), parameter-property 순서. CJS interop도(ESM external을 require로 보존 #1962, __toESM transitive default re-export 회귀 #812).
"CLI가 왜 느려졌지?"
결론: NAPI 경로는 미리 빌드된 네이티브 바이너리라 빠르고, CLI는 매번 컴파일해서 느렸던 것 — 측정 환경의 문제였지 코드 회귀가 아니었다. (이 "추론 말고 실측"은 3-14에서도 반복됐던 패턴.) innerGraph 같은 개념을 다시 설명받고, axios CI fail 재발 원인을 추적했다.
4/27 — 회귀 추적, /simplify 정리
비교적 한산한 날(34 커밋). 직접 올린 PR을 /simplify로 일괄 정리하고, smoke FAIL이 다시 난 패키지들(semver@es5, neverthrow, minimatch, cheerio, kysely)의 회귀 원인을 추적했다:
bundler tree-shaking/DCE refinement(순수 class expression, post-declaration assignment, re-export-star reachability, post-transform analysis refresh, transform-source를 캐시 키에), HMR 메모리 ownership 수리, Expo RN dev rewrites 안정화도 이날.
4/28 — config 시스템, CJS DCE, 그리고 "프로덕션 레벨 배포에 부족한 것"
config 시스템 에픽
zts.config.{ts,mjs,js,cjs,json} 자동 발견, .ts/.js self-compile loader, --mode + zts.config.{mode}.*, .env 자동 로드 + import.meta.env 정적 치환, extends + 사이클 탐지, Levenshtein "did you mean?" 오타 탐지, 함수형 config, zts.workspace.ts(Vitest 스타일 모노레포), config/.env 변경 시 watch 자동 재시작.
CLI ↔ BuildOptions ↔ WASM 스키마 동기화, TypeScript 5.9.3→6.0.3, oxlint 경고 0개도 같은 흐름.
CJS DCE 에픽 — "장기적으론 다 해야하는거죠?"
CJS 출력의 dead code elimination — Object.defineProperty getter/value export DCE, __esModule 마커 가지치기, dead-write/dead-store 가지치기, 새 Object.assign/Object.freeze를 pure로 취급. 여기서 매번 나온 질문이:
"일부만 바꾸면 이득 없다 → 전수로 해라"는 이 무렵 자주 나온 판단 기준. 4/29~4/30에 더 밀어붙인다.
"프로덕션 레벨 배포 하려면 부족한 게 뭐예요?"
이 편에서 가장 의미 있는 한 문장:
3-1의 "공부용이긴 하지만 그래도 프로덕션 레벨을 고민하면서"가 36일 만에 "이제 프로덕션 레벨 배포"가 됐다. 동시에 묵은 이슈를 닫는다:
3-14의 진행 중 에픽 표에 있던 HMR #1747이 여기서 닫힌다.
4/29 — 옵션 갭 메우기, IIFE 디폴트 제거, 그리고 zts-codex 분리
"다른 번들러 대비 딸리는 옵션이 뭐야?"
base64 loader, onLoad loader override(#2157), sourcemap 3-mode(inline/external/linked, #2217), Vite 스타일 alias 배열, preserveSymlinks, single-file dynamic-import 자동 lazy-wrap(#2209/#2211), 사이클 const/let/class → var 강등(ESM live binding, #2198), string-enum reverse-mapping 제거(#2192), 플러그인 lifecycle hook buildStart/buildEnd/closeBundle(#2156).
IIFE 디폴트 포맷 제거
"의도된 정책이 아닌 fallback은 무조건 없앤다" — 이 원칙이 4/30 styled-components/emotion의 SKIP fallback 제거로 그대로 이어진다.
zts-codex 별도 클론
이날부터 Codex 세션의 작업 디렉터리가 ~/Documents/workspace/zts에서 ~/Documents/workspace/zts-codex(별도 클론, 같은 origin remote)로 옮겨진다. 같은 시각대에 Claude는 zts에서, Codex는 zts-codex에서 — 둘이 같은 GitHub 이슈/PR을 던져받고, 작업 시작 시마다 git switch main && git pull로 상대방 결과를 흡수하는 구도. 이날 Codex 쪽 흐름은 번들 크기 회귀 전수조사(mobx/d3/lru-cache/dotenv/toolkit/semver/dayjs)와, 한발 더 나가 ZTS를 standalone Vite-style app builder로 포지셔닝(zts dev/build/preview, HTML entry, .env, publicDir, base, Tailwind/PostCSS, dev CSS HMR):
4/30 — styled-components·emotion 트랜스포머, "CSS 파서가 없어서"
SKIP fallback을 근본으로
emotion / styled-components 트랜스포머 정식 스펙
이날 zts는 Babel 플러그인 없이 자체 트랜스포머로 styled-components와 emotion을 처리한다 — RN/네이티브 지원의 핵심 차별점이다.
- emotion:
autoLabel(never/always/dev-only),@emotion/styleddefault 탐지,ClassNamesrender-prop, css prop object/array, keyframes/Global/injectGlobal autoLabel, importMap, labelFormat/sanitizeLabelPart, sourceMap 인라인 코멘트, styled chain walker(withComponent/attrs). - styled-components: componentId 해시,
withConfigMERGE/preserve, cssProp Steps 1~6(intrinsic tag → custom component → object form → auto-inject + program-level hoist), minify/ssr/pure/namespace/fileName/meaninglessFileNames/topLevelImportPaths 옵션, picomatch 호환 glob 유틸.
"바벨이 되는데 우리만 안 되면 문제" — 이게 parity의 기준선이다. Buffer overflow 차단 (256B 스택 버퍼) 같은 임시 방어도 "fallback 없이 완벽하게 못 하나요?"로 도전받았고(5/3에 ArrayList 전환으로 해결), RN 쪽 실패 케이스는 "보편적인 루트커즈 해결은 아닌거네요? 점진적으로 다 해결해주시죠"로 백로그됐다.
도그푸드 — 외부 IP로 예제앱 접속
ES5 다운레벨 정정도 이날 또 한 차례 쓸었다. Codex 쪽은 24-byte compact node vs ESTree AST 토론(처음부터 이걸 고려했으면 성능에도 문제없고 잘 만들어졌을 수 있을까? / 록다운이나 swc는?)과 메인 기준 문법 변환 전수조사(또 한번 해주세요 할때마다 계속 나오네요 / 무조건 루트커즈 수정을 해주세요)를 돌렸다.
7일간 관통한 것들
4/30 시점에 진행 중인 에픽
다음 편
3-16은 5/15/7 — esbuild 스타일 0.84 매트릭스, Flow 파서를 "ts처럼" 만들기, core-js 자동 폴리필, tsconfigRaw 통합, binding-lite 트랜스파일 fast path, 메모리 오염 근본 수정(#raw-require / 0xAA slice use-after-free), stack-overflow hardening, RN codegen rn-0.78@zntc/* 모노레포 패키지 분리, BoringSSL dev TLS, 번개(bungae) 흡수 + 폐기, 그리고 프로젝트 이름이 zts에서 zntc로 바뀐 날을 담는다.