우리가 매일 쓰는 JSX는 정말 JavaScript일까요? 브라우저가 이해할 수 없는 이 문법은 어떤 과정을 거쳐 실행 가능한 코드가 되는 걸까요?

개념 정의

JSX(JavaScript XML)는 JavaScript 안에서 HTML과 유사한 마크업을 작성할 수 있게 해주는 문법 확장(syntax extension) 입니다. 브라우저는 JSX를 직접 이해하지 못하므로, Babel이나 SWC 같은 트랜스파일러가 이를 일반 JavaScript 함수 호출로 변환합니다.

왜 필요한가

HTML과 JavaScript를 분리하는 대신, UI 로직과 마크업을 하나의 단위로 관리 하기 위해 JSX가 탄생했습니다.

  • 템플릿 문자열보다 가독성이 높습니다
  • 컴파일 타임에 구문 오류를 잡아낼 수 있습니다
  • JavaScript의 모든 표현식을 중괄호 {} 안에서 사용할 수 있습니다
JSX
// 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 호출로 변환되었습니다.

JSX
// 변환 전
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부터 도입된 자동 런타임은 이 문제를 해결합니다.

JSX
// 변환 전
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 설정

JSON
// .babelrc 또는 babel.config.json
{
  "presets": [
    ["@babel/preset-react", {
      "runtime": "automatic"  // 'classic'이 기본값이었으나, 최신 프로젝트는 'automatic' 사용
    }]
  ]
}

Vite를 사용한다면 @vitejs/plugin-react가 내부적으로 이 설정을 자동으로 처리합니다.

SWC — Babel의 대안

SWC는 Rust로 작성된 트랜스파일러로, Babel보다 20~70배 빠른 변환 속도 를 자랑합니다.

JSON
// .swcrc
{
  "jsc": {
    "transform": {
      "react": {
        "runtime": "automatic"
      }
    }
  }
}

Next.js는 기본적으로 SWC를 사용하고, Vite에서도 @vitejs/plugin-react-swc 플러그인으로 전환할 수 있습니다.

React.createElement가 반환하는 것

JSX가 변환된 함수 호출의 결과는 React Element 라 불리는 일반 JavaScript 객체입니다.

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 노드 없이 그룹화하는 방법입니다.

JSX
// 방법 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}>를 사용해야 합니다.

JSX
// Fragment에 key가 필요한 경우
{items.map(item => (
  <Fragment key={item.id}>
    <dt>{item.term}</dt>
    <dd>{item.description}</dd>
  </Fragment>
))}

주의할 점

표현식 vs 문(Statement)

JSX 중괄호 안에는 표현식만 들어갈 수 있습니다. JSX가 함수 호출로 변환되기 때문입니다. 함수의 인자 위치에는 값을 반환하는 표현식만 올 수 있고, iffor 같은 문(statement)은 값을 반환하지 않으므로 컴파일 에러가 발생합니다.

JSX
// ✅ 표현식 — 사용 가능
{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 등은 다른 이름을 사용합니다.

HTMLJSX이유
classclassNameclass는 JS 예약어
forhtmlForfor는 JS 예약어
tabindextabIndexcamelCase 규칙
onclickonClickcamelCase 규칙

인라인 스타일은 객체

인라인 스타일은 문자열이 아니라 객체 로 전달합니다. CSS 속성명도 camelCase로 작성해야 합니다.

JSX
// ❌ 문자열 — 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의 동작 원리 전체가 명확해집니다.

댓글 로딩 중...