React JSX Transform 실전 가이드: 빌드 도구별 완벽 정리
2024년 10월 28일
jsxbabeltypescriptesbuildswcvitewebpack
React JSX Transform 실전 가이드: 빌드 도구별 완벽 정리
1. 서론: 같은 목표, 다른 설정
// 모든 빌드 도구가 처리해야 하는 JSX
function App() {
return <div>Hello World</div>;
}
1탄에서 JSX Transform의 원리를 깊이 파헤쳤다면, 이제는 실전이다. Babel, TypeScript, esbuild, SWC, Vite... 각 도구마다 설정 방법이 다르다.
"우리 프로젝트는 어떻게 설정해야 하지?"
모든 빌드 도구의 JSX Transform 설정을 완벽 정리해보자.
2. Babel: 가장 유연한 선택
기본 설정
// .babelrc.json
{
"presets": [
["@babel/preset-react", {
"runtime": "automatic",
"importSource": "react"
}]
]
}
상세 옵션 분석
{
"presets": [
["@babel/preset-react", {
// "automatic" | "classic"
"runtime": "automatic",
// JSX runtime을 import할 패키지
"importSource": "react", // 기본값
// development 모드 설정
"development": process.env.NODE_ENV === "development",
// React.createElement 함수명 (classic 모드)
"pragma": "React.createElement",
// React.Fragment 함수명 (classic 모드)
"pragmaFrag": "React.Fragment",
// propTypes 제거 (production)
"throwIfNamespace": true
}]
]
}
파일별 설정 오버라이드
// classic 모드로 전환
/** @jsxRuntime classic */
import React from 'react';
// custom import source
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
// custom pragma (classic 모드)
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from 'preact';
Babel 플러그인 직접 사용
// babel.config.js
module.exports = {
plugins: [
["@babel/plugin-transform-react-jsx", {
runtime: "automatic",
importSource: "react",
// 추가 옵션들
useBuiltIns: true,
useSpread: true
}]
]
};
3. TypeScript: 타입 안전성과 함께
tsconfig.json 설정
{
"compilerOptions": {
// JSX Transform 모드
"jsx": "react-jsx", // automatic runtime
// "jsx": "react", // classic runtime
// "jsx": "react-jsxdev", // automatic + development
// "jsx": "preserve", // JSX 그대로 유지
// custom import source
"jsxImportSource": "react",
// classic 모드 옵션들
"jsxFactory": "React.createElement",
"jsxFragmentFactory": "React.Fragment"
}
}
TypeScript 버전별 차이
// TypeScript 4.1+: automatic runtime 지원
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx"
}
}
// TypeScript 4.0 이하: classic만 지원
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "h" // custom factory
}
}
타입 정의와 함께 사용
// custom JSX 사용 시 타입 정의
declare namespace JSX {
interface IntrinsicElements {
[elemName: string]: any;
}
interface Element {
type: any;
props: any;
key: string | null;
}
}
// 파일별 설정
/** @jsxImportSource preact */
import type { JSX } from 'preact';
function Component(): JSX.Element {
return <div>Preact with TypeScript!</div>;
}
4. esbuild: 극한의 속도
CLI 옵션
# automatic runtime
esbuild app.jsx --jsx=automatic --jsx-import-source=react
# classic runtime
esbuild app.jsx --jsx=transform --jsx-factory=React.createElement
# JSX 유지
esbuild app.jsx --jsx=preserve
API 설정
// build.js
const esbuild = require('esbuild');
esbuild.build({
entryPoints: ['app.jsx'],
bundle: true,
outfile: 'out.js',
// automatic runtime
jsx: 'automatic',
jsxImportSource: 'react',
// 또는 classic runtime
// jsx: 'transform',
// jsxFactory: 'React.createElement',
// jsxFragment: 'React.Fragment',
});
esbuild 플러그인으로 커스터마이징
// custom-jsx-plugin.js
const customJsxPlugin = {
name: 'custom-jsx',
setup(build) {
build.onLoad({ filter: /\.jsx$/ }, async (args) => {
const source = await fs.promises.readFile(args.path, 'utf8');
// 파일별 pragma 파싱
const pragmaMatch = source.match(/@jsxImportSource\s+(\S+)/);
const importSource = pragmaMatch ? pragmaMatch[1] : 'react';
return {
contents: source,
loader: 'jsx',
resolveDir: path.dirname(args.path),
pluginData: { jsxImportSource: importSource }
};
});
}
};
5. SWC: Rust 기반 차세대 도구
.swcrc 설정
{
"jsc": {
"parser": {
"syntax": "ecmascript",
"jsx": true
},
"transform": {
"react": {
// "automatic" | "classic"
"runtime": "automatic",
// automatic runtime 옵션
"importSource": "react",
// classic runtime 옵션
"pragma": "React.createElement",
"pragmaFrag": "React.Fragment",
// development 모드
"development": false,
// 추가 최적화
"useBuiltins": true,
"refresh": true // React Refresh
}
}
}
}
Next.js에서 SWC 커스터마이징
// next.config.js
module.exports = {
experimental: {
swcPlugins: [
['@swc/plugin-emotion', {
sourceMap: true,
autoLabel: 'dev-only',
jsxImportSource: '@emotion/react'
}]
]
},
compiler: {
emotion: {
sourceMap: true,
autoLabel: 'dev-only',
importSource: '@emotion/react'
}
}
};
6. Vite: 현대적인 개발 경험
vite.config.js 설정
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
// JSX runtime 설정
jsxRuntime: 'automatic', // 'classic' | 'automatic'
// automatic runtime 옵션
jsxImportSource: 'react',
// Babel 설정 (필요시)
babel: {
presets: [
['@babel/preset-react', {
runtime: 'automatic'
}]
],
plugins: [
['@emotion/babel-plugin', {
importSource: '@emotion/react'
}]
]
}
})
],
// esbuild 옵션 (개발 모드)
esbuild: {
jsx: 'automatic',
jsxImportSource: 'react',
jsxFactory: 'React.createElement', // classic 모드
jsxFragment: 'React.Fragment'
}
});
Vite + 다양한 프레임워크
// Preact 설정
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';
export default defineConfig({
plugins: [preact()],
esbuild: {
jsxImportSource: 'preact'
}
});
// Solid.js 설정
import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';
export default defineConfig({
plugins: [solidPlugin()],
esbuild: {
jsx: 'preserve' // Solid는 자체 transform 사용
}
});
7. Webpack: 엔터프라이즈 표준
webpack.config.js with Babel
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-react', {
runtime: 'automatic'
}]
]
}
}
}
]
}
};
webpack with SWC
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'swc-loader',
options: {
jsc: {
transform: {
react: {
runtime: 'automatic',
importSource: 'react'
}
}
}
}
}
}
]
}
};
webpack with esbuild
const { ESBuildMinifyPlugin } = require('esbuild-loader');
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
loader: 'esbuild-loader',
options: {
loader: 'jsx',
jsx: 'automatic',
jsxImportSource: 'react'
}
}
]
},
optimization: {
minimizer: [
new ESBuildMinifyPlugin({
jsx: 'automatic'
})
]
}
};
8. 실전 마이그레이션 시나리오
시나리오 1: CRA 프로젝트 마이그레이션
# 1. React 버전 업그레이드
npm update react react-dom
# 2. react-scripts 업데이트 (5.0+는 automatic 기본)
npm update react-scripts
# 3. ESLint 설정 수정
// .eslintrc.json
{
"extends": ["react-app"],
"rules": {
"react/react-in-jsx-scope": "off"
}
}
시나리오 2: 레거시 프로젝트 점진적 마이그레이션
// 1단계: 빌드 설정은 classic 유지
{
"presets": [
["@babel/preset-react", {
"runtime": "classic"
}]
]
}
// 2단계: 파일별로 automatic 전환
/** @jsxRuntime automatic */
// 이 파일만 automatic 사용
// 3단계: 전체 전환
{
"runtime": "automatic"
}
시나리오 3: 모노레포 환경
// packages/shared/.babelrc.json
{
"presets": [
["@babel/preset-react", {
"runtime": "automatic",
"importSource": "@emotion/react" // 공통 스타일링
}]
]
}
// packages/app-a/.babelrc.json
{
"extends": "../shared/.babelrc.json",
"presets": [
["@babel/preset-react", {
"runtime": "automatic",
"importSource": "react" // 기본 React
}]
]
}
9. 트러블슈팅 가이드
문제 1: Mixed Runtime Error
Error: Automatic JSX runtime requires importing 'jsx' from 'react/jsx-runtime'
but 'React' is already in scope
해결책:
// ❌ 잘못된 코드
import React from 'react'; // automatic에서는 불필요
// ✅ 올바른 코드
// import 제거하거나
import { useState } from 'react'; // 필요한 것만 import
문제 2: JSX Import Source Not Found
Module not found: Can't resolve 'react/jsx-runtime'
해결책:
# React 버전 확인 (17+ 필요)
npm list react
# 업데이트
npm update react react-dom
문제 3: TypeScript 타입 에러
// JSX element type 'Element' is not a constructor function for JSX elements
해결책:
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"types": ["react", "react-dom"]
}
}
10. 성능 측정과 최적화
빌드 시간 비교
// 측정 스크립트
const { execSync } = require('child_process');
const configs = [
{ name: 'Babel Classic', cmd: 'babel src --jsx-runtime classic' },
{ name: 'Babel Automatic', cmd: 'babel src --jsx-runtime automatic' },
{ name: 'esbuild', cmd: 'esbuild src/**/*.jsx --jsx=automatic' },
{ name: 'SWC', cmd: 'swc src -d dist' }
];
configs.forEach(({ name, cmd }) => {
console.time(name);
execSync(cmd);
console.timeEnd(name);
});
실제 측정 결과 (1000개 컴포넌트):
- Babel Classic: 3.24s
- Babel Automatic: 3.18s
- esbuild: 0.12s
- SWC: 0.31s
번들 사이즈 최적화
// webpack-bundle-analyzer로 분석
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html'
})
]
};
11. 결론: 도구는 다르지만 목표는 하나
// 모든 도구가 지향하는 미래
function App() {
return <div>Simple, Clean, Efficient</div>;
}
빌드 도구마다 설정은 다르지만, 모두 같은 목표를 향한다:
- 더 작은 번들 사이즈
- 더 빠른 빌드 속도
- 더 나은 개발 경험
도구 선택 가이드:
- Babel: 가장 유연하고 플러그인 생태계가 풍부
- TypeScript: 타입 안전성이 중요한 프로젝트
- esbuild: 빌드 속도가 최우선
- SWC: Next.js 등 모던 프레임워크 사용
- Vite: 빠른 개발 서버와 HMR이 중요
중요한 건 도구가 아니라, 프로젝트에 맞는 최적의 설정을 찾는 것이다.
"최고의 도구는 우리가 도구의 존재를 잊게 만드는 도구다."