Babel → ZNTC 이관 가이드
Metro 기반 babel.config.js를 ZNTC로 옮길 때 각 플러그인/프리셋의 대응을 정리합니다.
대응 매트릭스
섹션 제목: “대응 매트릭스”| Babel 설정 | ZNTC 대응 | 비고 |
|---|---|---|
@react-native/babel-preset | platform: "react-native" | JSX/Flow/class props 자동 |
@babel/preset-env (문법 다운레벨) | target: "es2020" 등 | engine 타겟도 가능 (chrome80 등) |
@babel/preset-env { useBuiltIns: "usage", corejs } | runtimePolyfills: "auto" (+ bun add core-js core-js-compat) | 런타임 폴리필 가이드 — useBuiltIns: "entry" → { mode: "entry" } |
@babel/plugin-transform-flow-strip-types | flow: true 또는 RN 프리셋 | .js.flow/@flow pragma 자동 |
@babel/plugin-proposal-decorators { legacy } | experimentalDecorators: true | Stage 3도 별도 지원 |
@babel/plugin-transform-class-properties { loose } | useDefineForClassFields: false | tsconfig와 동기화 |
@babel/plugin-transform-private-methods { loose } | target 자동 다운레벨 | 별도 옵션 불필요 |
@babel/plugin-proposal-optional-chaining | target 자동 다운레벨 | ES2020 내장 |
babel-plugin-root-import | alias: { "~/": "./src" } | tsconfig paths로도 가능 |
react-native-worklets/plugin | 내장 worklet 플러그인 | platform: "react-native"로 자동 |
babel-plugin-lodash | moduleSpecifierMap: { lodash: "lodash/{name}" } | cherry-pick 분해. alias: { lodash: "lodash-es" }도 가능 (ESM tree-shaking 활용) |
babel-plugin-styled-components | compiler.styledComponents | 1st-party transform (아래 섹션) |
@emotion/babel-plugin | compiler.emotion | 1st-party transform (아래 섹션) |
transform-remove-console | drop: ["console"] | |
transform-react-remove-prop-types | pure: ["PropTypes.*"] + DCE | React 19+에선 불필요 |
| 커스텀 Babel 플러그인 | Babel bridge (아래 섹션) | 또는 ZNTC 플러그인 포팅 |
기본 이관 예시
섹션 제목: “기본 이관 예시”Before — babel.config.js
섹션 제목: “Before — babel.config.js”module.exports = { presets: ["module:@react-native/babel-preset"], plugins: [ ["babel-plugin-root-import", { rootPathSuffix: "./src", rootPathPrefix: "~/" }], "@babel/plugin-transform-flow-strip-types", ["@babel/plugin-proposal-decorators", { version: "legacy" }], ["@babel/plugin-transform-class-properties", { loose: true }], ["@babel/plugin-transform-private-methods", { loose: true }], ["react-native-worklets/plugin"], ], env: { production: { plugins: ["transform-remove-console"], }, },};After — zntc.config.ts
섹션 제목: “After — zntc.config.ts”import { defineConfig } from "@zntc/core";
export default defineConfig({ platform: "react-native", target: "es2020", alias: { "~/": "./src" }, experimentalDecorators: true, useDefineForClassFields: false, drop: process.env.NODE_ENV === "production" ? ["console"] : [],});플러그인 배열이 0줄로 줄어듭니다. RN 프리셋 + worklet + Flow는 platform: "react-native"에 전부 포함됩니다.
RegExp 매칭이 필요할 때 — alias array 형태
섹션 제목: “RegExp 매칭이 필요할 때 — alias array 형태”기본 object 형태는 exact + prefix 매칭만 지원합니다. babel-plugin-module-resolver 의 regExp 옵션처럼 정규식이 필요하면 array 형태를 사용합니다 (Vite resolve.alias 호환).
defineConfig({ alias: [ { find: /^@\/(.*)$/, replacement: "./src/$1" }, { find: /^~components\/(.*)$/, replacement: "./src/components/$1" }, ],});매칭 순서대로 첫 번째만 적용. find 가 string 이면 prefix 매칭, RegExp 이면 host runtime 이 매칭 + replacement 로 치환합니다.
주의: array 형태는 build() (async) 만 지원합니다. buildSync() 는 host RegExp 위임이 plugin hook 기반이라 미지원 — Record<string, string> object 형태를 쓰거나 build() 로 전환하세요.
styled-components — compiler.styledComponents
섹션 제목: “styled-components — compiler.styledComponents”babel-plugin-styled-components 의 ZNTC 1st-party 대응. plugin 등록 없이 옵션만 켜면 됩니다.
defineConfig({ compiler: { styledComponents: { displayName: true, // devtools 표시 (default: NODE_ENV !== "production") ssr: true, // 결정론적 componentId hash (default: true) fileName: true, // componentId 에 파일명 포함 (default: true) minify: true, // CSS whitespace minify (default: true) transpileTemplateLiterals: true, // 다운레벨된 템플릿 인식 (default: true) pure: false, // styled.X 부수효과 없음 hint (default: false) namespace: "my-app", // displayName/componentId namespace prefix topLevelImportPaths: ["@my-org/styled"], // vendored fork 인식 cssProp: false, // `<div css={...}>` 를 module-level styled component 로 추출 (default: false) }, },});compiler.styledComponents: true 로 default 옵션을 한 번에 켤 수도 있습니다.
emotion — compiler.emotion
섹션 제목: “emotion — compiler.emotion”@emotion/babel-plugin 의 ZNTC 1st-party 대응.
defineConfig({ compiler: { emotion: { autoLabel: "dev-only", // "always" | "dev-only" | "never" | boolean (default: "dev-only") labelFormat: "[local]", // tokens: [local] / [filename] / [dirname] (default: "[local]") sourceMap: true, // sourceMap 생성 (default: true) importMap: { // fork / vendored emotion 사용 시 import alias "@my-org/styled": { styled: { canonicalImport: ["@emotion/styled", "default"] }, }, }, }, },});jsxImportSource 는 BuildOptions 의 동명 옵션으로 별도 지정합니다 (예: jsxImportSource: "@emotion/react").
Babel bridge — 커스텀 Babel 플러그인 재사용
섹션 제목: “Babel bridge — 커스텀 Babel 플러그인 재사용”내장으로 대체할 수 없는 커스텀 Babel 플러그인(예: 사내 preset, testID 자동 주입, AppRegistry 래핑 등)은 Babel을 통째로 transform 훅에서 호출해 재사용할 수 있습니다.
bun add -D @babel/corezntc.config.ts
섹션 제목: “zntc.config.ts”import { defineConfig } from "@zntc/core";import * as babel from "@babel/core";import mcpPreset from "@ohah/react-native-mcp-server/babel-preset";
export default defineConfig({ platform: "react-native", plugins: [ { name: "babel-bridge", transform: { filter: /\.(jsx?|tsx?)$/, handler(code, id) { const out = babel.transformSync(code, { filename: id, presets: [[mcpPreset, { renderHighlight: true }]], plugins: [ // 여기에 그 외 커스텀 Babel 플러그인 ], babelrc: false, configFile: false, sourceMaps: true, }); if (!out) return null; return { code: out.code ?? code, map: out.map ?? undefined }; }, }, }, ],});핵심 포인트:
babelrc: false, configFile: false— 프로젝트babel.config.js를 재귀적으로 읽지 않도록 명시. ZNTC 설정과 이중 변환 방지filter— 필요한 확장자만.node_modules제외 원하면filter함수에!/node_modules/.test(id)추가sourceMaps: true— 소스맵 체이닝. ZNTC가 이후 단계에서 병합- 반환 형식:
{ code, map? }.null반환 시 ZNTC 기본 파이프라인으로 폴백
성능 고려
섹션 제목: “성능 고려”각 모듈마다 Babel을 한 번 돌리므로 개발 중 dev server 웜업은 느려집니다. 프로덕션 번들은 ZNTC 본체보다 @babel/core 호출 비용이 지배적. 다음 중 하나로 완화:
filter를 좁혀 Babel이 꼭 필요한 파일만 통과 (예:src/**/*.tsx만)- 자주 쓰는 플러그인은 ZNTC 플러그인으로 포팅 (아래 섹션)
- dev는 Babel bridge, prod는 ZNTC 네이티브로 분기
ZNTC 플러그인으로 포팅
섹션 제목: “ZNTC 플러그인으로 포팅”Babel bridge는 간편하지만 느립니다. 성능이 중요하거나 빌드 횟수가 많다면 ZNTC 플러그인 API로 재작성하는 게 정답입니다.
Rollup/Vite 스타일 훅(resolveId, load, transform)으로 커스텀 플러그인 직접 작성:
import { defineConfig } from "@zntc/core";
export default defineConfig({ plugins: [ { name: "inject-testid", transform: { filter: /\.tsx?$/, handler(code, id) { // JSX elements에 testID prop 주입 등 // 자세한 AST 훅은 플러그인 가이드 참조 return null; }, }, }, ],});자세한 플러그인 작성법: 플러그인 가이드, 플러그인 레시피.
자주 묻는 케이스
섹션 제목: “자주 묻는 케이스”Q. babel-plugin-lodash는 꼭 필요한가?
섹션 제목: “Q. babel-plugin-lodash는 꼭 필요한가?”Metro에선 tree-shaking이 약해 import { debounce } from 'lodash' 시 lodash 전체(~70KB)가 번들됨 → 이 플러그인으로 cherry-pick 필수였음.
ZNTC는 ESM tree-shaking이 정상 동작하므로:
lodash-es사용 → 자동 cherry-pick (최적)lodash유지 →alias: { lodash: "lodash-es" }한 줄로 해결lodash유지 + path import 강제 →moduleSpecifierMap: { lodash: "lodash/{name}" }(named specifier 만, alias 없는 경우만 변환. 미충족 시 원본 import 유지)- 즉 플러그인 포팅 불필요
Q. transform-react-remove-prop-types는?
섹션 제목: “Q. transform-react-remove-prop-types는?”React 19+는 PropTypes API 자체를 제거. TypeScript 사용 중이면 PropTypes 자체가 없을 것.
남은 PropTypes 코드 제거가 필요하면:
pure: ["PropTypes.string", "PropTypes.number", /* ... */]- dead code elimination으로 상당 부분 제거. 완벽히는 커스텀 플러그인 필요.
Q. env.production.plugins 분기는?
섹션 제목: “Q. env.production.plugins 분기는?”ZNTC에선 NODE_ENV 기반 분기를 defineConfig 내부에서:
const isProd = process.env.NODE_ENV === "production";export default defineConfig({ drop: isProd ? ["console", "debugger"] : [], plugins: isProd ? [minifyPlugin] : [],});Q. babel overrides (파일별 규칙)는?
섹션 제목: “Q. babel overrides (파일별 규칙)는?”plugins[].transform.filter로 파일 패턴별 변환을 분리:
plugins: [ { name: "a", transform: { filter: /\.tsx$/, handler: ... } }, { name: "b", transform: { filter: /\/legacy\//, handler: ... } },]점진적 이관 전략
섹션 제목: “점진적 이관 전략”한 번에 Babel 전체를 걷어내지 않아도 됩니다:
- Stage 1 —
platform: "react-native"+ alias 기본 설정 + Babel bridge로 기존 플러그인 전부 유지 - Stage 2 — Babel 플러그인을 하나씩 내장 기능으로 치환하거나 ZNTC 플러그인으로 포팅 → bridge에서 제거
- Stage 3 — bridge 자체 제거.
@babel/core의존성 삭제
각 단계가 독립적으로 배포 가능합니다.
- 런타임 폴리필 (core-js) —
useBuiltIns+core-js이관 - 플러그인 가이드
- 플러그인 레시피
- React Native 가이드
- 마이그레이션 가이드 (esbuild/Vite/webpack)