.lite.tsx 파일 하나로 컴포넌트를 짜면, React·Vue·Angular·Svelte·Solid·Qwik·Alpine·Lit·Stencil·React Native·Liquid 등 21개 타깃으로 자동 변환해 주는 컴파일러다. 비결은 Mitosis JSON이라는 중간 표현(IR) — JSX를 일단 프레임워크 중립적인 트리로 바꿔두고, 타깃별 제너레이터(generator)가 그 트리를 각 프레임워크 코드로 "출력"한다. 이건 Babel·SWC 같은 컴파일러가 쓰는 "파싱 → IR → 코드 생성" 패턴을 프런트엔드 컴포넌트에 적용한 사례라, 이 레포 하나로 컴파일러 설계와 여러 프레임워크의 공통점·차이를 동시에 배울 수 있다. (저장소: BuilderIO/mitosis · ★13.8k · Fork 637 · MIT · TypeScript 95.8% · 1,898 commits · core v0.13.2 · TrendShift 20위)이 레포가 무엇을 하는 물건인가.
여러 프레임워크를 쓰는 회사에서 똑같은 버튼·카드를 React용, Vue용, Angular용으로 세 번 따로 만드는 일이 흔하다. Mitosis는 그걸 한 번만 짜면 나머지를 기계가 번역하게 만든다. 단, 런타임 라이브러리가 아니라 빌드 타임 컴파일러다 — 결과물은 Mitosis가 끼어들지 않는 100% 네이티브 React/Vue/… 코드다.
Mitosis(@builder.io/mitosis)는 Builder.io가 만든 컴포넌트 컴파일러다. 개발자는 React와 매우 비슷한 문법(JSX + 훅)으로 컴포넌트를 .lite.tsx 확장자 파일에 작성한다. 그러면 Mitosis가 이를 분석해 Mitosis JSON이라는 프레임워크 중립 트리로 바꾸고, 선택한 타깃마다 별도의 제너레이터가 그 트리를 진짜 React·Vue·Angular·Svelte·Solid·Qwik 코드로 뽑아낸다.
핵심은 "한 번 쓰고, 어디서나 실행(write once, run everywhere)"인데, 웹 컴포넌트(Web Components)처럼 런타임 추상화 레이어를 끼우는 방식과는 정반대다. Mitosis는 컴파일이 끝나면 자기 흔적을 남기지 않는다. 그래서 각 팀은 자기 프레임워크의 표준 코드를 받아 그대로 쓰고 트리쉐이킹·최적화도 정상적으로 누린다. 대표 활용처는 디자인 시스템(여러 프레임워크에 같은 UI를 배포)과 Figma → 코드 동기화다.
.lite.tsx)가 여러 프레임워크 구현으로 "분열"한다는 비유다. npm 패키지명은 @builder.io/mitosis이고, 과거 이름은 jsx-lite였다(레포 토픽에 흔적이 남아 있다).트렌딩 이유 · 경쟁 접근 대비 장점.
2026년 현재 프런트엔드 생태계는 React 천하가 아니다. Vue·Svelte·Solid·Qwik·Angular가 각자 자리를 지키고, 한 회사 안에서도 팀마다 다른 프레임워크를 쓴다. 이 "프레임워크 파편화" 속에서 같은 UI를 여러 번 만드는 비용이 커졌고, Mitosis는 그 통증을 정확히 겨냥한다. 단순 인기가 아니라 ★13.8k · 253개 릴리스 · 1,898 커밋으로 오래 다져진 성숙한 프로젝트라는 점이 신뢰를 준다. 차별점은 넷이다.
| 비교 항목 | 웹 컴포넌트(Web Components) | Mitosis |
|---|---|---|
| 접근 방식 | 런타임 추상화(브라우저 표준 위에 한 겹) | 빌드 타임 컴파일(흔적 0) |
| 출력물 | 커스텀 엘리먼트(프레임워크 밖 존재) | 네이티브 React/Vue/… 코드 |
| 생태계 호환 | SSR·props 전달 등 마찰 잦음 | 각 프레임워크 관용구 그대로 |
| 지원 타깃 | (표준 1개) | 21개(React~Swift까지) |
| 디자인 도구 | — | Figma·Builder.io 연동 |
README가 직접 내세우는 셀링포인트다: "Avoid the pitfalls of web components by compiling to native framework code". 웹 컴포넌트는 SSR·폼 연동·prop 타입 등에서 프레임워크와 자주 부딪친다. Mitosis는 아예 각 프레임워크의 진짜 코드를 뽑아주므로 그런 마찰이 없다. 받은 팀은 "이게 Mitosis로 만들어졌다"는 사실조차 몰라도 된다.
packages/core/src/generators/에 제너레이터가 21개 들어 있다 — react(+preact), vue, angular, svelte, solid, qwik, alpine, lit, marko, stencil, taro, liquid(Shopify), swift, html/webcomponent, react-native, rsc(React Server Components), template, mitosis(자기 자신), builder. 웹을 넘어 Shopify 테마(Liquid)와 iOS(Swift)까지 한 소스에서 나온다.
Builder.io의 Figma 플러그인으로 디자인을 Mitosis 컴포넌트로 뽑고, 그걸 다시 React/Vue/… 패키지로 컴파일해 npm에 배포하는 흐름을 지원한다. 디자인 시스템을 코드와 동기화하려는 조직에 강력하다(레포 토픽: figma·no-code·visual·declarative).
"한 소스"가 모든 차이를 못 메울 때를 대비해 useTarget({ react: ..., vue: ... }) 같은 타깃별 코드 분기를 공식 제공한다. 또 overrides/ 디렉토리로 특정 타깃 파일을 통째로 대체할 수도 있다. "추상화의 누수"를 부정하지 않고 현실적으로 다루는 설계.
원고와 번역본. 작가가 한국어로 소설 한 권(.lite.tsx)을 쓰면, 번역가들이 영어·일본어·프랑스어판을 각각 자연스러운 그 나라 문장으로 옮긴다. 독자(각 프레임워크 팀)는 번역본만 읽으므로 원문이 무엇이었는지 신경 쓸 필요가 없다. useTarget은 "이 문장만은 일본어판에서 이렇게 의역해 달라"는 작가의 각주에 해당한다.
컴파일러 코어 / 입력(파서) / 출력(제너레이터) / 모노레포 운영의 4축으로 본다.
Mitosis는 서버·DB가 있는 웹앱이 아니라 개발자 도구(컴파일러)다. 그래서 "백/프론트/인프라" 대신 컴파일러 파이프라인 + 모노레포 운영 관점으로 스택을 그리는 게 정확하다.
@builder.io/mitosis (packages/core)| 분류 | 라이브러리 | 역할 |
|---|---|---|
| JSX 파싱 | @babel/core, @babel/preset-typescript, plugin-transform-react-jsx | .lite.tsx를 AST로 읽어 Mitosis JSON으로 변환 |
| 타입 처리 | ts-morph, typescript | props/state의 TypeScript 타입을 분석·이식 |
| 다른 입력 파서 | @angular/compiler, svelte+svelte-preprocess | Angular·Svelte 컴포넌트도 입력으로 받음 |
| 코드 출력 포맷 | prettier(2.x), astring | 생성된 코드를 사람이 읽을 수 있게 정렬 |
| 트리/유틸 | neotraverse, lodash, fp-ts, json5, object-hash | IR 트리 순회·함수형 유틸·해시 |
| Builder 연동 | @builder.io/sdk | Builder.io 비주얼 콘텐츠 ↔ Mitosis 변환 |
<div>{x}</div> 같은 글자를 "div 요소 노드 + 그 자식인 표현식 노드" 형태의 객체로 바꾼다. Babel이 JSX를 AST로 읽어주면, Mitosis가 그걸 자기 IR로 옮겨 담는다.packages/core/src/parsers/에 입력 파서가 있다. 기본은 JSX 파서(parsers/jsx)이고, 그 외 angular.ts·svelte/·builder/·context.ts가 있다. 즉 React식 JSX뿐 아니라 Angular·Svelte·Builder.io 콘텐츠도 "원본"으로 삼아 Mitosis JSON으로 끌어올 수 있다. 설정에서 parser를 직접 넣어 커스텀 입력 형식을 만들 수도 있다.
핵심 자산. packages/core/src/generators/ 하위에 타깃별 폴더 21개가 있고, 각각 componentToReact·componentToVue처럼 "Mitosis JSON → 문자열 코드"를 만드는 함수를 export 한다. 이들을 한데 모은 게 targets.ts다.
// packages/core/src/targets.ts — 타깃 레지스트리(요약)
export const targets = {
alpine, angular, customElement, html, mitosis, liquid,
react, reactNative, solid, svelte, swift, template,
webcomponent, vue, stencil, qwik, marko, preact, lit,
rsc, taro,
} as const;
export type Targets = keyof typeof targets;
저장소 전체는 Yarn 4 워크스페이스 + Nx(빌드 캐시·태스크 오케스트레이션)로 묶인 모노레포다. package.json의 workspaces에 packages/*·e2e/*·examples/**가 등록돼 있고, 릴리스는 @changesets/cli로 버전·체인지로그를 관리한다.
| 패키지 | 이름 | 역할 |
|---|---|---|
| core | @builder.io/mitosis | 파서·IR·제너레이터 = 컴파일러 본체 |
| cli | @builder.io/mitosis-cli | mitosis build/compile/new 커맨드 |
| eslint-plugin | @builder.io/eslint-plugin-mitosis | .lite.tsx 작성 규칙 린트(허용 문법 강제) |
| fiddle | (웹 플레이그라운드) | 브라우저에서 입력↔출력을 실시간 비교 |
| starter | create-mitosis | npm create용 프로젝트 스캐폴딩 |
| docs | (Qwik City 사이트) | mitosis.builder.io 문서 |
nx run-many). Changesets는 PR마다 "이 변경이 어느 패키지의 어떤 버전 업인지"를 적어 두었다가 릴리스 때 한꺼번에 버전·체인지로그를 만든다.파싱 → IR → 제너레이트, 그리고 그 사이에 끼는 플러그인.
Mitosis는 거의 모든 컴파일러가 쓰는 "앞단(parse) → 가운데(IR) → 뒷단(generate)" 구조다. 가운데의 프레임워크 중립 IR이 핵심 — 입력이 N개, 출력이 M개여도 N×M개의 변환기를 짤 필요 없이 N+M개만 있으면 된다.
입력 형식이 늘어도 IR만 잘 채우면 21개 출력이 공짜로 따라오고, 새 프레임워크 타깃을 추가할 때도 제너레이터 하나만 더 쓰면 된다. 이게 "한 번 쓰고 어디서나"가 가능한 구조적 이유다.
MitosisComponent 객체다 — 이름·임포트·상태(state)·훅(hooks)·자식 노드 트리(MitosisNode[])·컨텍스트를 담는다. 프레임워크 색이 빠진 "컴포넌트의 본질"만 남긴 형태.IR이 추상적으로 들리면, 실제 타입을 보면 분명해진다. 컴포넌트는 메타데이터 + 노드 트리로 쪼개진다.
// types/mitosis-component.ts (요약) — 컴포넌트 = 메타+상태+훅+트리
interface MitosisComponent {
name: string;
imports: MitosisImport[]; // 어떤 모듈을 import 했나
state: MitosisState; // useStore/useState → 상태 맵
hooks: { onMount, onUpdate, ... }; // 라이프사이클
children: MitosisNode[]; // JSX 트리(렌더 결과)
context: { get, set }; // useContext/setContext
props?, refs?, meta? ...
}
// types/mitosis-node.ts (요약) — 노드 = 이름+속성+바인딩+자식
type MitosisNode = {
'@type': '@builder.io/mitosis/node';
name: string; // 'div', 'Show', 컴포넌트명…
properties: { [k: string]: string }; // 정적 속성(class="x")
bindings: { [k: string]: Binding }; // 동적 바인딩(checked={...})
children: MitosisNode[];
};
여기서 properties(정적)와 bindings(동적)를 나눈 게 영리하다. class="view"는 properties로, checked={props.todo.completed}는 bindings로 들어간다. 제너레이터는 이 구분을 보고 React는 checked={...}, Vue는 :checked="...", Angular는 [checked]="..."처럼 각 프레임워크의 바인딩 문법으로 번역한다.
.lite.tsx레포의 examples/todo에 있는 실제 컴포넌트(요약). React를 닮았지만 상태는 useStore, 조건부는 <Show>로 쓴다 — 이 "Mitosis 사투리"가 모든 타깃으로 번역 가능한 최소 공통 문법이다.
// examples/todo/src/components/todo.lite.tsx (요약)
import { Show, useStore } from '@builder.io/mitosis';
export default function Todo(props) {
const state = useStore({
editing: false,
toggle() { props.todo.completed = !props.todo.completed; },
});
return (
<li>
<input type="checkbox" checked={props.todo.completed}
onClick={() => state.toggle()} />
<label onDblClick={() => { state.editing = true; }}>
{props.todo.text}
</label>
<Show when={state.editing}>
<input class="edit" value={props.todo.text} />
</Show>
</li>
);
}
useStore는 반응형 상태 객체(React의 useState/Vue의 reactive를 모두 커버하는 중립형). <Show when={...}>는 조건부 렌더, <For each={...}>는 반복 렌더. JS의 &&·.map() 대신 명시적 컴포넌트를 쓰는 이유는, 그래야 파서가 "이건 조건/반복이다"를 확실히 알아 Vue v-if·Angular *ngIf 등으로 정확히 번역할 수 있기 때문이다.useState·useStore·onMount·useContext 같은 Mitosis 훅은 실제 함수가 아니다. index.ts에서 declare function useStore...로 선언만 돼 있고, 컴파일 때 각 타깃의 진짜 메커니즘으로 치환되어 사라진다 — React면 useState, Vue면 ref, Svelte면 let 변수. "흔적 0"의 정체가 이것이다.
각 타깃은 componentToX(options) => (component) => string 꼴의 함수다. 같은 IR을 넣으면 항상 같은 코드가 나오는 순수 변환이라, 테스트하기 쉽고 타깃을 독립적으로 늘릴 수 있다. __tests__/의 스냅샷 테스트가 "이 입력 → 이 출력"을 고정해 회귀를 막는다.
파싱 전/후, 생성 전/후에 끼어드는 플러그인 훅이 있다(plugins/). 예: compile-away-components(특정 컴포넌트를 더 단순한 것으로 치환), map-styles(스타일 변환). 코어를 안 건드리고 변환 동작을 확장하는 개방-폐쇄 원칙의 실전 사례다.
Mitosis는 앱 번들에 들어가지 않는다. .lite.tsx는 오직 빌드 입력이고, 배포되는 건 생성된 네이티브 코드뿐이다. 그래서 "Mitosis를 쓰면 번들이 커진다"는 걱정은 사실이 아니다. 대신 제약이 있다 — 아무 JS·아무 npm 라이브러리나 컴포넌트 안에서 자유롭게 쓸 수 없고, 번역 가능한 부분집합(useStore·Show·For 등)을 지켜야 한다.
모노레포 → 컴파일러 코어 → 그 안의 핵심 3폴더 순으로 좁혀 본다.
e2e/ 아래에 React·Vue·Angular·Solid·Svelte·Qwik·Alpine·Stencil 각각의 실제 앱이 따로 있다. 같은 .lite.tsx를 컴파일해 8개 프레임워크에서 돌려보고 동작이 동일한지를 검증하는 장치다. "21개로 번역한다"는 주장을 말이 아니라 실행되는 테스트로 받쳐 주는 부분.
packages/core/src/읽는 순서를 추천하면 ① types/(IR이 무엇인가) → ② parsers/jsx/(입력을 어떻게 IR로 채우나) → ③ generators/react/(IR을 어떻게 코드로 푸나). 이 셋만 따라가면 컴파일러 전체 그림이 잡힌다.
parsers/jsx/ 내부파서가 "통짜 함수" 하나가 아니라 관심사별로 쪼개진 게 학습 포인트다 — 컴포넌트의 각 부분을 따로 읽는다.
| 파일 | 읽는 대상 |
|---|---|
jsx.ts / index.ts | 파서 진입점 · 전체 오케스트레이션 |
element-parser.ts | JSX 엘리먼트 → MitosisNode(속성/바인딩 분리) |
state.ts | useStore/useState → state 맵 |
props.ts · props-types.ts | props와 그 TypeScript 타입 |
hooks/ | onMount·useTarget·useMetadata 등 훅 |
imports.ts · exports.ts | 모듈 import/export 보존 |
signals.ts | 반응형 시그널(useState(.., {reactive:true})) |
이 레포 하나로 "컴파일러 + 멀티 프레임워크 + 모노레포"를 동시에 챙긴다.
가장 값진 배움. 입력 N개·출력 M개를 N×M이 아니라 N+M으로 푸는 "중간 표현(IR)" 발상은 Babel·SWC·LLVM·Svelte까지 관통하는 핵심 아이디어다. Mitosis는 이걸 친숙한 "컴포넌트"라는 소재로 보여줘서 입문용으로 최적이다.
types/mitosis-node.ts의 properties vs bindings 구분을 읽고, "왜 정적/동적을 나눠야 타깃별 바인딩 문법으로 번역되는가"를 한 단락으로 설명해 보기.같은 IR이 React·Vue·Svelte·Solid·Angular 코드로 어떻게 다르게 풀리는지 비교하면, 각 프레임워크의 반응성 모델이 선명해진다. useStore가 React에선 useState, Vue에선 ref, Svelte에선 그냥 변수가 되는 걸 보면 "상태 관리란 결국 같은 문제의 다른 해법"임이 보인다.
왜 .map() 대신 <For>, && 대신 <Show>를 강제할까? 자유로운 JS는 번역 불가능하기 때문이다. eslint-plugin이 이 제약을 강제한다. "표현력을 일부러 줄여서 이식성을 얻는" 설계 트레이드오프를 체험할 수 있다.
// mitosis.config.js — 컴파일 설정의 뼈대(요약)
module.exports = {
files: 'src/**',
targets: ['react', 'vue', 'svelte', 'solid', 'qwik'],
dest: 'output',
options: {
react: { stylesType: 'styled-jsx', stateType: 'hooks' },
vue: { api: 'composition' },
},
};
.lite.tsx를 만들어 어떤 경고가 뜨는지 확인.core·cli·docs·e2e가 한 저장소에서 어떻게 의존하고, Nx가 nx run-many로 바뀐 패키지만 빌드/테스트하며, Changesets가 릴리스를 자동화하는지를 실물로 본다. 실제 OSS 운영을 배우는 좋은 교본이다.
package.json의 ci:build/ci:test 스크립트를 읽고 Nx 타깃 그래프가 어떻게 그려지는지 추적.__tests__/의 스냅샷은 "이 입력 → 정확히 이 React 코드"를 박제한다. 제너레이터를 고칠 때 의도치 않은 출력 변화를 즉시 잡아내는 컴파일러 회귀 테스트의 정석이다.
서버·DB 없음 — Node 환경이면 끝. 빌드 타임 도구다.
| 항목 | 요구사항 | 메모 |
|---|---|---|
| 런타임 | Node.js ≥ 16 | 루트 package.json의 engines 기준 |
| 패키지 매니저 | (기여 시) Yarn 4.1.1 | packageManager 고정. 사용만 할 땐 npm도 OK |
| 언어 | TypeScript 5.x 권장 | props/state 타입이 그대로 이식됨 |
| 대상 프레임워크 | 타깃별 빌드 환경 | 출력 코드는 각 프레임워크 표준 프로젝트에서 빌드 |
| 네트워크 | 설치 시에만 | 컴파일 자체는 로컬에서 동작 |
# 대화형 스캐폴딩(starter 패키지가 생성)
npm create @builder.io/mitosis@latest
# 생성된 폴더에서 — 컴포넌트를 모든 타깃으로 컴파일
npm run build # 내부적으로 mitosis build 실행
npm i -D @builder.io/mitosis-cli @builder.io/mitosis
# mitosis.config.js를 만든 뒤
npx mitosis build # config의 targets로 일괄 컴파일
npx mitosis compile -t=vue ./Button.lite.tsx # 단일 파일 → Vue
CLI 커맨드는 packages/cli/src/commands/에 build.ts(설정 기반 일괄 빌드)·compile.ts(단일 파일 변환)·new.ts(스캐폴딩) 셋으로 단순하게 나뉜다. 브라우저에서 빠르게 감을 잡고 싶으면 설치 없이 플레이그라운드를 쓰면 된다.
읽기만 말고, 난이도순으로 손을 움직여 보자.
mitosis.builder.io/playground에 버튼+카운터 컴포넌트(useStore 상태 1개, <Show> 1개)를 작성하고 React·Vue·Svelte·Solid 출력 탭을 비교한다. "같은 IR이 어떻게 다른 코드가 되나"를 눈으로 확인.
npm create @builder.io/mitosis로 프로젝트를 만들고 Button·Badge·Card 세 컴포넌트를 .lite.tsx로 작성한 뒤 targets: ['react','vue']로 컴파일한다. 나온 React/Vue 코드를 각 프레임워크 샘플 앱에 붙여 실제로 렌더되는지 확인.
한 컴포넌트 안에서 useTarget({ react: ..., vue: ..., default: ... })로 타깃별로 다른 클래스명/속성을 출력하게 만들어 본다. "추상화의 누수"를 공식 장치로 다루는 감각을 익히는 단계.
레포를 클론해 packages/core/src/generators/react/를 읽고, __tests__/ 스냅샷 하나를 골라 "이 IR이 왜 이 React 코드가 되는지" 한 줄씩 매핑한다. yarn test:update로 스냅샷이 갱신되는 흐름도 체험.
generators/template(가장 단순한 골격)을 본떠 아주 작은 커스텀 타깃을 만들거나, plugins/ 훅으로 "모든 class에 접두사를 붙이는" 플러그인을 작성한다. IR을 직접 순회·변환해 보며 컴파일러 뒷단을 손에 익히는 최종 단계.
4주 코스 — 컴파일러 개념과 프레임워크 비교를 번갈아 다진다.
| 주차 | 주제 | 핵심 활동 |
|---|---|---|
| 1주차 | JSX·AST·컴파일러 기초 | Babel이 JSX를 AST로 바꾸는 과정 이해 · astexplorer.net 실습 · parse→IR→generate 개념 잡기(Lv1) |
| 2주차 | 프레임워크 반응성 비교 | React hooks · Vue reactivity · Svelte runes/store · Solid signals를 같은 예제로 비교. useStore가 각각 무엇이 되는지 매핑(Lv2) |
| 3주차 | Mitosis IR & 제너레이터 | types/→parsers/jsx/→generators/react/ 코드 읽기 · 스냅샷 테스트 추적(Lv3·Lv4) |
| 4주차 | 확장 & 운영 | 플러그인/커스텀 타깃 작성(Lv5) · Nx/Changesets 모노레포 운영 · eslint-plugin 규칙 분석 |
먼저 "컴파일러란 무엇인가"(1주)와 "프레임워크들이 같은 문제를 어떻게 다르게 푸나"(2주)를 알아야, 3~4주차에 Mitosis 코드가 "왜 이렇게 생겼는지"가 비로소 납득된다. 도구의 사용법이 아니라 그 도구가 풀고 있는 문제부터 보는 순서다.
이 레포를 읽다 마주치는 용어들.
useStore·<Show>·<For> 같은 "번역 가능한 부분집합"만 쓴다. 컴파일 입력 전용 — 앱에 직접 배포되지 않는다.name(태그/컴포넌트명), properties(정적 속성), bindings(동적 바인딩), children으로 구성. 정적/동적 분리가 타깃별 바인딩 번역의 토대다.componentToReact 등)와 그 대상. targets.ts에 21개가 등록돼 있다.config.parser로 커스텀 가능.useState, Vue ref/reactive, Svelte 변수 등으로 치환되고 자기 자신은 사라진다(compile-away).&&·.map() 대신 명시적 컴포넌트를 써야 Vue v-if/v-for, Angular *ngIf/*ngFor 등으로 정확히 번역된다.useTarget({ react:..., vue:..., default:... }) 형태. 한 소스로 못 메우는 차이를 현실적으로 처리.원본과 더 깊이 파고들 자료.
· GitHub 저장소: github.com/BuilderIO/mitosis
· 공식 문서: mitosis.builder.io/docs
· 플레이그라운드: mitosis.builder.io/playground
· npm 패키지: @builder.io/mitosis, @builder.io/mitosis-cli
· IR 타입: packages/core/src/types/mitosis-component.ts, mitosis-node.ts
· 타깃 레지스트리: packages/core/src/targets.ts
· 기본 파서: packages/core/src/parsers/jsx/ · 제너레이터: packages/core/src/generators/react/
· 실제 예제: examples/todo/src/components/todo.lite.tsx
· Figma 연동: mitosis.builder.io/docs/figma
· 컴파일러 개념 실습: astexplorer.net (JSX를 AST로 보는 도구)
· Discord 커뮤니티 / 기여 가이드: 레포 CONTRIBUTING.md