Remotion 코드 분석 — Bundle 시스템과 Webpack/esbuild 통합
bundle()이 React 컴포넌트를 렌더링 가능한 HTML로 변환하는 과정을 소스 코드로 추적
GitHub: remotion-dev/remotion
Remotion의 렌더링은 번들링에서 시작됩니다. React 컴포넌트를 브라우저가 실행할 수 있는 HTML+JS 번들로 만드는 과정을 소스 코드로 추적합니다.
리포지토리 구조
packages/
├── bundler/ # Webpack 기반 번들러
│ ├── src/
│ │ ├── bundle.ts # bundle() 진입점
│ │ ├── webpack-config.ts # Webpack 설정 생성
│ │ ├── webpack-caching.ts # 캐시 전략
│ │ └── index-html.ts # HTML 템플릿 생성
├── core/ # 핵심 런타임
│ ├── src/
│ │ ├── Composition.tsx # <Composition> 컴포넌트
│ │ ├── CompositionManager.tsx # 등록된 Composition 관리
│ │ ├── register-root.ts # registerRoot() 진입점
│ │ ├── use-current-frame.ts # useCurrentFrame() Hook
│ │ └── video-config.ts # fps, width, height 등 설정
├── renderer/ # 로컬 렌더러
│ ├── src/
│ │ ├── render-media.ts # renderMedia() 진입점
│ │ ├── render-frames.ts # 프레임 렌더링 루프
│ │ └── stitch-frames-to-video.ts # FFmpeg 인코딩
├── studio/ # Remotion Studio (개발 서버)
│ ├── src/
│ │ └── start.ts # Studio 시작점
└── lambda/ # AWS Lambda 렌더링
bundle() 함수의 동작 과정
1. 진입점: packages/bundler/src/bundle.ts
// bundle.ts (간략화)
export const bundle = async (options: BundleOptions) => {
const webpackConfig = await webpackConfiguration({
entryPoint: options.entryPoint, // Root.tsx 경로
outDir: options.outDir,
environment: 'production',
});
// Webpack 실행
const stats = await runWebpack(webpackConfig);
// index.html 생성 (Headless Chrome이 로드할 페이지)
await createIndexHtml({
outDir: options.outDir,
baseDir: '/',
});
return options.outDir;
};
핵심은 webpackConfiguration()이 생성하는 Webpack 설정입니다.
2. Webpack 설정: webpack-config.ts
// webpack-config.ts (핵심 부분)
export const webpackConfiguration = (options) => {
return {
entry: options.entryPoint, // 사용자의 Root.tsx
output: {
path: options.outDir,
filename: '[name].bundle.js',
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'esbuild-loader', // ts → js 변환은 esbuild!
options: { loader: 'tsx' },
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
// 이미지, 폰트 등 asset loaders...
],
},
};
};
주목할 점: TypeScript 컴파일에 esbuild-loader를 사용합니다. Webpack의 번들링 능력 + esbuild의 빠른 트랜스파일을 조합한 하이브리드 전략입니다.
3. HTML 템플릿: index-html.ts
// 생성되는 index.html (간략화)
<html>
<head>
<style>* { margin: 0; padding: 0; }</style>
</head>
<body>
<div id="remotion-root"></div>
<script src="/main.bundle.js"></script>
</body>
</html>
이 HTML이 Headless Chrome에서 로드되고, main.bundle.js가 실행되면 React가 #remotion-root에 마운트됩니다.
4. CompositionManager — 메타데이터 수집
// core/src/CompositionManager.tsx
// registerRoot() 실행 시 모든 <Composition>의 메타데이터를 수집
const CompositionManager: React.FC = () => {
const [compositions, setCompositions] = useState<AnyComposition[]>([]);
// <Composition>이 렌더될 때마다 등록됨
const registerComposition = (comp: AnyComposition) => {
setCompositions(prev => [...prev, comp]);
};
// 렌더러가 window.getCompositions()로 이 데이터를 가져감
useEffect(() => {
window.getCompositions = () => compositions;
}, [compositions]);
return <Context.Provider value={{ registerComposition }}>{children}</Context.Provider>;
};
핵심 메커니즘: 번들이 Chrome에서 실행되면, 모든 <Composition>이 CompositionManager에 등록됩니다. 렌더러는 window.getCompositions()를 호출하여 어떤 비디오를 어떤 설정(fps, 해상도, 길이)으로 렌더링할지 결정합니다.
동작 흐름
bundle(entryPoint)가 호출되면 webpackConfiguration()으로 Webpack 설정 생성
esbuild-loader가 TSX → JS 트랜스파일, Webpack이 의존성 그래프를 번들링
index-html.ts가 #remotion-root + main.bundle.js를 포함하는 HTML 생성
registerRoot()가 실행되면 CompositionManager가 모든 <Composition>의 메타데이터를 수집
window.getCompositions()를 통해 렌더러가 fps/width/height/durationInFrames를 조회
장점
- ✓ Webpack + esbuild 하이브리드: 호환성(Webpack)과 속도(esbuild) 모두 확보
- ✓ window.getCompositions() 패턴: 브라우저 ↔ Node.js 간 메타데이터 브릿지가 깔끔
단점
- ✗ Webpack 의존: 최초 번들링이 느릴 수 있음 (Vite 마이그레이션 논의 중)