React JSX Transform 실전 가이드
Introduce
// 모든 빌드 도구가 처리해야 하는 JSX
function App() {
return <div>Hello World</div>;
}
npx create-next-app
이나 npm create vite
같은 CLI 명령어 하나면, Babel, SWC, TypeScript의 복잡한 설정이 우리도 모르는 새에 완료된다. JSX Transform 설정 역시 마찬가지이다.
"이걸 굳이 내가 직접 알 필요가 있을까?"
물론 그렇게 생각할 수 있겠지만, 현실은 그리 녹록치 않다. 자동 설정이라는 편안한 조수석에서 내려, 직접 운전대를 잡고 문제를 해결해야 하는 순간은 반드시 찾아온다는 것이다.
예를 들어 이런 상황들.
- 문제 해결 능력 (디버깅) : 원인 불명의 빌드 에러가 터미널에 _jsx is not defined나 Cannot resolve 'react/jsx-runtime' 같은 메시지를 뿜어낼 때, 어디부터 확인해야 할까?
- 자유로운 확장성 : 프로젝트에 Emotion이나 Relay처럼 특별한 Babel 플러그인이 필요한 라이브러리를 새로 도입해야 한다면?
- 최적화 : CRA(Create React App)를 eject 하거나 Webpack 설정을 직접 수정하여 빌드 속도를 극한까지 끌어올려야 하는 상황이라면?
- 하위 호환성 : 아직 Classic Runtime을 사용하는 오래된 사내 라이브러리나 레거시 프로젝트를 갑자기 유지보수하게 되었다면?
CLI는 우리를 목적지까지 빠르고 편안하게 데려다주는 자율주행 자동차와 같다. 하지만 엔진 소리가 이상하거나, 예상치 못한 경고등이 켜졌을 때 보닛을 열어 문제를 진단할 수 있는 엔지니어와 그렇지 못한 엔지니어의 차이는 명확하지 않을까?
⚠️ 주의: 이 글은 2024년 11월 기준으로 작성되었습니다.
웹 개발 생태계, 특히 빌드 도구는 매우 빠르게 변화합니다. 이 글을 참고하시되, 실제 프로젝트에 적용하기 전에는 반드시 각 도구의 최신 공식 문서를 함께 확인하시는 것을 권장합니다.
트랜스파일러(Transpiler)와 번들러(Bundler)
이 글에 등장하는 도구들은 크게 두 종류로 나뉜다. 대부분의 개발 환경은 번들러(Webpack, Vite)를 사용하며, 이 번들러가 내부적으로 트랜스파일러(Babel, SWC, esbuild)를 호출하여 JSX 변환 같은 작업을 처리한다. 이 구조를 이해하면 각 설정이 어떤 역할을 하는지 파악하기 쉽다.
1. Babel: 가장 유연한 선택
Babel은 최신 JavaScript 문법을 구형 브라우저에서도 동작하도록 변환해주는, 가장 성숙하고 널리 쓰이는 트랜스파일러이다. 방대한 플러그인 생태계를 통해 JSX 변환 외에도 무궁무진한 코드 변환이 가능하다.
기본 설정
// .babelrc.json
{
"presets": [
["@babel/preset-react", {
"runtime": "automatic",
"importSource": "react"
}]
]
}
상세 옵션 분석
{
"presets": [
["@babel/preset-react", {
// "automatic" | "classic"
"runtime": "automatic",
// JSX runtime을 import할 패키지 (기본값: "react")
"importSource": "react",
// development 모드 설정 (true 시 __source, __self prop 추가)
"development": process.env.NODE_ENV === "development",
// React.createElement 함수명 (classic 모드 전용)
"pragma": "React.createElement",
// React.Fragment 함수명 (classic 모드 전용)
"pragmaFrag": "React.Fragment",
// XML 네임스페이스 문법 사용 시 에러 발생 (React 미지원)
"throwIfNamespace": true
}]
]
}
파일별 설정 오버라이드
/** @jsxRuntime classic */
또는 /** @jsxImportSource @emotion/react */
같은 주석을 파일 상단에 추가하여 전역 설정을 덮어쓸 수 있다.
2. TypeScript: 타입 안전성과 함께
TypeScript는 JavaScript에 정적 타입 시스템을 추가한 슈퍼셋(Superset) 언어이다. 자체 컴파일러(tsc)를 통해 타입 검사와 코드 변환(JSX 포함)을 동시에 수행할 수 있다.
TypeScript는 tsconfig.json 파일을 통해 JSX 처리 방식을 정교하게 제어한다.
tsconfig.json의 주요 jsx 옵션
{
"compilerOptions": {
// "preserve": JSX를 변환하지 않고 그대로 둡니다.
// Babel, SWC 등 다른 트랜스파일러가 JSX를 처리할 때 사용합니다.
// 가장 흔하고 권장되는 설정 중 하나입니다.
"jsx": "preserve",
// "react-jsx": Automatic Runtime을 사용하여 JSX를 변환합니다. (프로덕션용)
// "react-jsxdev": Automatic Runtime을 사용하며, 디버깅을 위한
// __source, __self prop을 추가합니다. (개발용)
"jsx": "react-jsxdev",
// Automatic Runtime 사용 시, JSX 함수를 가져올 소스를 지정합니다.
// (예: "preact", "@emotion/react" 등)
"jsxImportSource": "react"
}
}
실전에서의 선택: preserve vs react-jsx
-
"jsx": "preserve"
: 현대적인 프레임워크(Next.js, Vite 등)에서 가장 흔하게 볼 수 있는 설정이다. TypeScript는 JSX의 타입 검사만 수행하고 실제 변환은 더 빠른 SWC나 esbuild에게 맡기는 방식이다. 이를 통해 타입 안전성과 빌드 속도를 모두 잡을 수 있다. -
"jsx": "react-jsx" / "react-jsxdev"
: 오직 tsc(TypeScript 컴파일러)만을 사용하여 코드를 변환하는 간단한 환경에서 유용하다.
대부분의 경우, 어플리케이션 개발에서는 프레임워크가 제공하는 기본 설정(preserve)을 그대로 따르는 것을 추천한다.
3. esbuild: 극한의 속도
esbuild는 Go 언어로 작성되어 압도적으로 빠른 속도를 자랑하는 차세대 트랜스파일러 겸 번들러이다. 복잡한 설정 없이도 매우 빠른 성능을 보여주어 Vite 등 현대적인 도구의 핵심 엔진으로 사용된다.
CLI 옵션
# automatic runtime
esbuild app.jsx --jsx=automatic --jsx-import-source=react
# classic runtime
esbuild app.jsx --jsx=transform --jsx-factory=React.createElement
API 설정
// build.js
require('esbuild').build({
entryPoints: ['app.jsx'],
bundle: true,
outfile: 'out.js',
jsx: 'automatic',
jsxImportSource: 'react',
});
5. SWC: Rust 기반 차세대 도구
SWC (Speedy Web Compiler)는 Rust 언어로 작성되어 Babel을 대체할 수 있는 빠른 속도를 목표로 하는 트랜스파일러이다. Next.js의 기본 트랜스파일러로 채택되면서 널리 알려지게 되었다.
.swcrc 설정
{
"jsc": {
"parser": { "syntax": "typescript", "jsx": true },
"transform": {
"react": {
"runtime": "automatic",
"importSource": "react",
// true 시 React Refresh 자동 활성화 및 개발 정보 주입
"refresh": process.env.NODE_ENV === "development"
}
}
}
}
Next.js에서 SWC 커스터마이징
Next.js는 내장된 SWC 최적화를 compiler 옵션을 통해 제어하는 것을 권장한다.
// next.config.js
module.exports = {
compiler: {
// Emotion.js 최적화를 위한 내장 기능 (권장)
emotion: {
sourceMap: true,
autoLabel: 'dev-only',
importSource: '@emotion/react'
}
},
// 그 외 커스텀 WASM 플러그인 사용 시 (실험적 기능)
experimental: {
swcPlugins: [
['my-custom-swc-plugin', { /* plugin options */ }]
]
}
};
6. Vite: 현대적인 개발 경험
Vite는 네이티브 ESM을 기반으로 한 매우 빠른 개발 서버와 Rollup 기반의 최적화된 프로덕션 빌드를 제공하는 차세대 프론트엔드 빌드 도구이다. 개발 시에는 esbuild를 사용하여 즉각적인 HMR을 구현한다.
JSX 설정은 @vitejs/plugin-react
를 통해 통합 관리된다.
vite.config.js 설정
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
jsxRuntime: 'automatic', // 'classic'
jsxImportSource: 'react', // 기본값
// 필요 시 Babel 플러그인 체인 추가 가능
babel: {
plugins: ['@emotion/babel-plugin']
}
})
]
});
7. Webpack: 엔터프라이즈 표준
Webpack은 유연한 모듈 번들러이다. 방대한 로더와 플러그인 생태계를 통해 거의 모든 종류의 웹 애플리케이션 빌드 파이프라인을 구축할 수 있으며, 오랜 기간 표준으로 자리 잡아왔다.
Webpack은 로더(loader)를 통해 트랜스파일러를 선택적으로 사용할 수 있다.
로더 선택 가이드
- babel-loader : 가장 성숙하고 방대한 플러그인 생태계를 활용해야 할 때 최적의 선택이다. 유연성이 가장 높다.
- swc-loader / esbuild-loader : 기존 babel-loader 대비 월등히 빠른 빌드 속도를 원할 때 사용한다. Babel 만큼의 플러그인 호환성은 없지만, 속도 향상 효과가 매우 크다.
webpack.config.js with Babel
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'swc-loader',
options: {
jsc: {
transform: {
react: {
runtime: 'automatic',
importSource: 'react'
}
}
}
}
}
}
]
}
};
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 | 느림 | 최상 | 높은 커스터마이징이 필요하거나, 레거시 지원이 중요한 환경 |
SWC | 빠름 | 좋음 | Next.js, Webpack/Vite 등에서 속도와 유연성의 균형을 잡을 때 |
esbuild | 가장 빠름 | 보통 | Vite 개발 서버, 빠른 프로토타이핑, 번들링 속도가 최우선일 때 |
결국 가장 중요한 것은 "어떤 도구가 최고인가"가 아니라, "우리 프로젝트의 요구사항에 가장 잘 맞는 도구와 설정은 무엇인가"를 고민하는 것이다.