JSX 동작 원리 — 바벨이 JSX를 변환하는 과정
우리가 매일 쓰는 JSX는 정말 JavaScript일까요? 브라우저가 이해할 수 없는 이 문법은 어떤 과정을 거쳐 실행 가능한 코드가 되는 걸까요?
개념 정의
JSX(JavaScript XML)는 JavaScript 안에서 HTML과 유사한 마크업을 작성할 수 있게 해주는 문법 확장(syntax extension) 입니다. 브라우저는 JSX를 직접 이해하지 못하므로, Babel이나 SWC 같은 트랜스파일러가 이를 일반 JavaScript 함수 호출로 변환합니다.
왜 필요한가
HTML과 JavaScript를 분리하는 대신, UI 로직과 마크업을 하나의 단위로 관리 하기 위해 JSX가 탄생했습니다.
- 템플릿 문자열보다 가독성이 높습니다
- 컴파일 타임에 구문 오류를 잡아낼 수 있습니다
- JavaScript의 모든 표현식을 중괄호
{}안에서 사용할 수 있습니다
// JSX 없이 — 가독성이 떨어집니다
React.createElement('div', { className: 'card' },
React.createElement('h2', null, title),
React.createElement('p', null, description)
);
// JSX로 — 한눈에 구조가 보입니다
<div className="card">
<h2>{title}</h2>
<p>{description}</p>
</div>
내부 동작 — 트랜스파일 과정
Classic 런타임 (React 17 이전)
React 17 이전에는 JSX가 React.createElement 호출로 변환되었습니다.
// 변환 전
const element = <h1 className="greeting">Hello</h1>;
// 변환 후
const element = React.createElement('h1', { className: 'greeting' }, 'Hello');
이 방식의 문제점은 모든 JSX 파일에서 import React from 'react'가 필요 하다는 것이었습니다. JSX를 직접 사용하지 않는 것처럼 보여도 변환 결과가 React.createElement를 참조하기 때문입니다.
Automatic 런타임 (React 17+)
React 17부터 도입된 자동 런타임은 이 문제를 해결합니다.
// 변환 전
const element = <h1 className="greeting">Hello</h1>;
// 변환 후 (자동 런타임)
import { jsx as _jsx } from 'react/jsx-runtime';
const element = _jsx('h1', { className: 'greeting', children: 'Hello' });
핵심 차이점을 정리하면 다음과 같습니다.
react/jsx-runtime모듈에서jsx함수를 자동으로 import 합니다- 개발자가
import React를 작성할 필요가 없습니다 children이 props 객체에 포함됩니다- 약간의 번들 크기 개선 효과가 있습니다
Babel 설정
// .babelrc 또는 babel.config.json
{
"presets": [
["@babel/preset-react", {
"runtime": "automatic" // 'classic'이 기본값이었으나, 최신 프로젝트는 'automatic' 사용
}]
]
}
Vite를 사용한다면 @vitejs/plugin-react가 내부적으로 이 설정을 자동으로 처리합니다.
SWC — Babel의 대안
SWC는 Rust로 작성된 트랜스파일러로, Babel보다 20~70배 빠른 변환 속도 를 자랑합니다.
// .swcrc
{
"jsc": {
"transform": {
"react": {
"runtime": "automatic"
}
}
}
}
Next.js는 기본적으로 SWC를 사용하고, Vite에서도 @vitejs/plugin-react-swc 플러그인으로 전환할 수 있습니다.
React.createElement가 반환하는 것
JSX가 변환된 함수 호출의 결과는 React Element 라 불리는 일반 JavaScript 객체입니다.
// React.createElement('h1', { className: 'greeting' }, 'Hello')의 반환값
{
type: 'h1',
props: {
className: 'greeting',
children: 'Hello'
},
key: null,
ref: null,
// ... 내부 필드들
}
이 객체가 Virtual DOM의 노드 역할을 합니다. React는 이 객체들의 트리를 비교(Reconciliation)하여 실제 DOM에 최소한의 변경만 적용합니다.
Fragment
여러 요소를 반환해야 할 때, 불필요한 래퍼 DOM 노드 없이 그룹화하는 방법입니다.
// 방법 1: <React.Fragment>
import { Fragment } from 'react';
function Columns() {
return (
<Fragment>
<td>첫 번째</td>
<td>두 번째</td>
</Fragment>
);
}
// 방법 2: 단축 문법 <>...</>
function Columns() {
return (
<>
<td>첫 번째</td>
<td>두 번째</td>
</>
);
}
단축 문법 <>...</>는 key를 전달할 수 없습니다. 리스트 렌더링 시 key가 필요하면 <Fragment key={id}>를 사용해야 합니다.
// Fragment에 key가 필요한 경우
{items.map(item => (
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</Fragment>
))}
주의할 점
표현식 vs 문(Statement)
JSX 중괄호 안에는 표현식만 들어갈 수 있습니다. JSX가 함수 호출로 변환되기 때문입니다. 함수의 인자 위치에는 값을 반환하는 표현식만 올 수 있고, if나 for 같은 문(statement)은 값을 반환하지 않으므로 컴파일 에러가 발생합니다.
// ✅ 표현식 — 사용 가능
{isLoggedIn && <Welcome />}
{count > 0 ? <List /> : <Empty />}
{items.map(item => <Item key={item.id} />)}
// ❌ 문 — 사용 불가
{if (isLoggedIn) { return <Welcome /> }} // SyntaxError
{for (let i = 0; i < 5; i++) { ... }} // SyntaxError
HTML 속성명 차이
JSX는 JavaScript이므로 HTML 속성명이 아닌 DOM 프로퍼티명 을 따릅니다. HTML 파서가 아니라 JavaScript 엔진이 처리하기 때문에, JavaScript 예약어와 충돌하는 class, for 등은 다른 이름을 사용합니다.
| HTML | JSX | 이유 |
|---|---|---|
class | className | class는 JS 예약어 |
for | htmlFor | for는 JS 예약어 |
tabindex | tabIndex | camelCase 규칙 |
onclick | onClick | camelCase 규칙 |
인라인 스타일은 객체
인라인 스타일은 문자열이 아니라 객체 로 전달합니다. CSS 속성명도 camelCase로 작성해야 합니다.
// ❌ 문자열 — HTML 방식
<div style="color: red; font-size: 16px">
// ✅ 객체 — JSX 방식
<div style={{ color: 'red', fontSize: '16px' }}>
Classic 런타임에서 import 누락
React 17 이전 프로젝트에서 import React from 'react'를 빠뜨리면 React is not defined 에러가 발생합니다. 변환 결과가 React.createElement를 참조하기 때문입니다. 레거시 프로젝트를 다룰 때 주의가 필요합니다.
정리
| 항목 | 설명 |
|---|---|
| JSX의 정체 | 함수 호출로 변환되는 문법 확장(syntactic sugar) |
| Classic 런타임 | React.createElement로 변환, import React 필수 |
| Automatic 런타임 | jsx() 함수로 변환, import 자동 |
| 변환 결과 | React Element — { type, props, key, ref } 형태의 일반 객체 |
| Fragment | 불필요한 래퍼 DOM 없이 여러 요소를 그룹화 |
| 중괄호 규칙 | 표현식만 가능, 문(if/for) 사용 불가 |
JSX는 "마법"이 아니라 함수 호출의 설탕 문법입니다. 이 사실을 알면 React의 동작 원리 전체가 명확해집니다.