>[!tip]-
> tip 블록 안의 내용은 책의 내용이 아닌 개인적인 궁금증으로 조사한 내용입니다.
## JSX
JSX란 **JavaScript eXtension**의 약자로 말 그대로 **자바스크립트 확장 문법**이다.
**ECMAScript의 일부는 아니고** 페이스북(메타)에서 React를 발표하면서 함께 발표한 문법이다.
자바스크립트로 **XML 스타일의 트리 구조를 쉽게 만들기 위해**서 사용하는 **문법적 설탕**이다. (React만을 위한 문법은 아니다!)
>[!tip]- XML (아직 링크 문서들 제대로 안읽어봄)
> [XML - Wikipedia](https://en.wikipedia.org/wiki/XML)
> [Extensible Markup Language (XML) 1.0 (Fifth Edition)](https://www.w3.org/TR/xml/)
> [📑 XML 기초 문법 정리](https://inpa.tistory.com/entry/XML-%F0%9F%93%91-XML-%EA%B8%B0%EC%B4%88-%EC%A0%95%EB%A6%AC)
>
> **XML은 데이터를 저장하고 전달할 목적으로 만들어진 언어**이고, **데이터의 구조를 기술**하는데 사용된다.
> 여기서 말하는 **데이터의 구조**란 데이터가 **어떤 항목을 가지고 있고**, 그들간의 **계층 구조는 어떻게 되는지(부모-자식), 순서나 속성(메타 정보)**같은 것들을 의미한다.
> 서로 다른 시스템 사이에 데이터를 주고받을 때 이런 데이터의 구조까지 기술해야 할 필요가 있고, **RSS나 Sitemap, DOCX가 실제로 XML이 사용되는 예시**이다.
>[!tip]- XML 스타일의 트리 구조를 왜 만들어야 하는가
>왜 자바스크립트로 XML 스타일의 트리 구조를 만들어야 했는가,
>그러니까 **이 문법 자체가 왜 필요했는지**가 궁금했다.
>**UI는 본질적으로 계층적 트리 구조를 갖는다.** (요소 안에 요소가 있는 형태)
>이 **UI의 계층적 구조를 자바스크립트 코드로 선언적으로 표현하기 위해**서 XML 스타일의 트리 구문이 필요했다.
>[!tip]- JSX라는 새로운 문법까지 만들 필요가 있었을까?
>[Introducing JSX – React \#Why JSX?](https://legacy.reactjs.org/docs/introducing-jsx.html#why-jsx)
>
> **HTML나 DOM 트리를 자바스크립트 코드 안에서 선언적으로 표현하기 위해서는 HTML과 비슷한 구문을 바로 자바스크립트 코드 안에 넣는 것이 가장 직관적**이었다.
> JSX를 사용하지 않고도 `React.createElement` 같은 함수를 호출해서 트리 구조를 만들 수는 있지만, **중첩이 깊어질수록 가독성이 나빠진다.**
> 즉 **JSX는 자바스크립트로 트리 구조를 편하고 읽기 쉽게 만들기 위해서 사용하는 문법적 설탕**이다.
>[!tip]- JSX를 React 외에서도 사용하는 사례가 있을까?
> **YES**
> JSX는 그 자체로 뭔가 의미를 갖는다기보다는 **트랜스파일된 실행 가능한 자바스크립트 코드가 실제로 의미를 갖는다.**
> 그래서 JSX가 **어떤 함수를 호출하도록 트랜스파일링되는지에 따라 React 외에도 여러 라이브러리나 프레임워크에서 사용될 수 있다.**
> [@babel/plugin-transform-react-jsx · Babel](https://babel.dev/docs/babel-plugin-transform-react-jsx#custom)
## JSX의 정의
**JSX는 문법적으로 `JSXElement`, `JSXAttributes`, `JSXChildren`, `JSXStrings`라는 4가지 구성요소로 이루어진다.**
> **이거 왜 공부해야 하나요 😫**
> 이 구성 요소들은 JSX 문법을 해석하고 파싱할 때 주요 역할을 담당하는 핵심 단위가 된다.
> 뒤에서 나올 JSX 코드가 JavaScript로 변환되는 과정을 이해하기 위해서는 이 구조를 이해할 필요가 있다!
### JSXElement
`JSXElement`는 **파서(parser)가 "이것은 하나의 JSX 요소다"라고 인식하는 최소 단위**이다.
그래서 다음과 같은 형태 중 하나면 파서는 이것을 `JSXElement`로 본다.
- `<div></div>` -> `JSXOpeningElement` + `JSXClosingElement`
- `<div/>` -> `JSXSelfClosingElement`
- `<></>` -> `JSXFragment` (축약형 프래그먼트)
> [!tip]- 축약형 프래그먼트는 속성을 가질 수 없다.
> `key` 같이 속성이 필요한 경우에는 `React.Fragment`를 사용해야 한다.
즉 JSXElement는 HTML 태그나 React 컴포넌트처럼 보이는 바로 그 부분이다.
> [!note]- JSXElementName
> `JSXElement`의 이름은 JSXElementName으로 표현된다.
> - `JSXIdentifier` : 일반 식별자. 자바스크립트 식별자 규칙과 동일한 규칙을 갖는다. (예: `div`, `MyComponent`)
> -> 식별자가 대문자로 시작하면 React에서는 "컴포넌트"로 해석하고, 소문자로 시작하면 DOM 태그로 해석한다.
> - `JSXNamespacedName` : `JSXIdentifier:JSXIdentifier` 같이 `:` 를 이용해서 서로 다른 식별자를 이어주는 것도 하나의 식별자로 취급된다. `:` 를 이용한 연결은 한번만 가능하다. (예: `svg:react`)
> - `JSXMemberExpression` : `JSXIdentifier.JSXIdentifier` 같이 `.` 을 이용해서 서로 다른 식별자를 이어주는 것도 하나의 식별자로 취급된다. `.`을 이용한 연결은 여러 개 이어서도 가능하다. `JSXNamespacedName`과 연결하는 것은 불가능하다. (예: `Layout.Header` 처럼 객체 속성 형태의 컴포넌트를 가리킬 때 사용된다.)
### JSXAttributes
JSXElement에 속성(props)을 지정하는 부분이다.
HTML의 속성과 비슷한데, JavaScript 표현식을 사용할 수 있다는 점이 다르다.
- `JSXSpreadAttributes`
-> `{ ...props }` 처럼 객체의 속성을 한번에 전달하는 부분이다.
(JavaScript의 전개 연산자와 같은 역할을 한다.)
- `JSXAttribute`
- 문법: name=value (value는 생략 가능)
- JSXAttributeName: JSXIdentifier 또는 JSXNamespacedName
- JSXAttributeValue (가능한 값들):
- `"큰따옴표 문자열"`
- `'작은따옴표 문자열'`
- `{ 표현식 }` (JavaScript 표현식)
- `JSXElement` (속성값으로 JSX 사용 가능)
- `JSXFragment` (속성값으로 여러 요소 그룹을 전달할 때)
- **속성 키만 쓴 경우 (값을 생략한 경우)**: 암묵적으로 true로 취급된다. (예: `<input disabled/>` → `<input disabled={true}/>`)
즉 `JSXAttributes`는 `<div className="box" {...props} />` 에서 `className="box"`
와 `{...props}` 같은 부분을 의미한다.
### JSXChildren
`JSXChildren`은 **`JSXElement`의 내부(자식)을 구성하는 부분**이다.
`<div> ... </div>` 에서 `...`에 들어가는 것이 `JSXChildren`이다.
`JSXChildren`은 0개 이상의 `JSXChild`로 이루어져 있다.
(= `JSXElement`는 자식을 가질 수도, 가지지 않을 수도 있다.)
하나의 JSXChild는 다음 중 하나일 수 있다.
- `JSXText`
->일반 텍스트. 중괄호(`{}`)나 꺽쇠(`<>`)를 제외한 순수 문자열
(예: `<div>Hello World</div>` 에서 `Hello World`)
- `JSXElement`
-> 자식으로 또 다른 JSX 요소가 들어가는 경우
(예: `<div><span>text</span></div>`)
- `JSXFragment`
-> `<></>` 형태의 빈 요소를 자식으로 포함할 때
(예: `<div><><span/><span/></></div>`)
- `{ JSXChildExpression }`
-> 중괄호로 감싸진 JavaScript 표현식
(예: `<div>{user.name}</div>`)
JSXElement의 자식 값을 나타낸다.
### JSXStrings
JSXStrings는 **JSX 안에서 표현되는 문자열을 정의한 문법 단위**이다.
다음 세가지 형태가 포함된다.
- "큰따옴표 문자열"
- '작은따옴표 문자열'
- JSXText (태그 안의 일반 텍스트)
이 규칙은 **HTML과 JSX간의 복사와 붙여넣기를 쉽게 하기 위해서 설계**되어 있다.
따라서 HTML에서 사용 가능한 문자열은 JSX에서도 '거의' 그대로 사용할 수 있다.
하지만 JavaScript와는 한 가지 중요한 차이점이 있는데
JavaScript에서는 `\`가 **이스케이프 문자로 처리**되지만,
JSX(정확히는 JSXText나 JSXAttributeValue)에서는 HTML과 동일하게 **일반 문자로 취급된다.**
예를 들면:
```jsx
<span>C:\path\to\file</span>
```
JSX에서는 위 코드가 의도한 대로 잘 동작하지만,
JavaScript 문자열 안에서는 `\\`로 이스케이프해야 같은 의미가 된다.
HTML과의 호환성을 위해서 의도적으로 남겨 둔 차이점인데,
페이스북(메타)에서는 이 정책을 미래에 수정할 가능성을 언급하기도 했다. ([JSX](https://facebook.github.io/jsx/#sec-jsx-string-characters))
(하지만 현재로선 유지되고 있다.)
## JSX는 어떻게 자바스크립트로 변환될까?
JSX가 자바스크립트로 변환되는 것을 알려면 @babel/plugin-transform-react-jsx 플러그인을 보면 된다.
이 플러그인은 JSX 구문을 자바스크립트가 이해할 수 있는 형태로 변환한다.
```jsx
const ComponentA = <A require={true}>Hello World</A>;
const ComponentB = <>Hello World</>;
const ComponentC = (
<div>
<span>Hello World</span>
</div>
);
```
이 코드를 JavaScript로 변환해봅시다.
필요한 패키지를 설치하고
```shell
npm install --save @babel/standalone @babel/plugin-transform-react-jsx
```
`transform-jsx.js` 파일을 아래와 같이 만들어준다.
```javascript
import * as Babel from '@babel/standalone'
import transformJSX from "@babel/plugin-transform-react-jsx";
Babel.registerPlugin("@babel/plugin-transform-react-jsx", transformJSX);
const BABEL_CONFIG = {
presets: [],
plugins: [
[
transformJSX,
{
throwIfNamespace: false,
runtime: 'automatic',
importSource: 'custom-jsx-library',
}
]
],
}
const SOURCE_CODE = `const ComponentA = <A require={true}>Hello World</A>;
const ComponentB = <>Hello World</>;
const ComponentC = (
<div>
<span>Hello World</span>
</div>
);`
// code 변수에 트랜스파일된 결과가 담긴다.
const {code} = Babel.transform(SOURCE_CODE, BABEL_CONFIG);
console.log(code);
```
ESM 환경에서 실행하기 위해서 package.json에 `"type": "module"` 을 추가해준다.
```json
{
"type": "module",
"dependencies": {
"@babel/plugin-transform-react-jsx": "^7.27.1",
"@babel/standalone": "^7.28.5"
}
}
```
그리고 `transform-jsx.js` 을 실행해주면
```shell
node transform-jsx.js
```
아래와 같은 출력 결과가 나온다.
```javascript
import { jsx as _jsx, Fragment as _Fragment } from "custom-jsx-library/jsx-runtime";
const ComponentA = _jsx(A, {
require: true,
children: "Hello World"
});
const ComponentB = _jsx(_Fragment, {
children: "Hello World"
});
const ComponentC = _jsx("div", {
children: _jsx("span", {
children: "Hello World"
})
});
```
위 결과는 React 17+의 "automatic runtime" 모드로 트랜스파일한 결과인데,
이전 버전인 고전 런타임 모드로 바꾸면
```diff
const BABEL_CONFIG = {
presets: [],
plugins: [
[
transformJSX,
{
throwIfNamespace: false,
- runtime: 'automatic',
- importSource: 'custom-jsx-library',
+ runtime: 'classic',
}
]
],
}
```
아래처럼 `React.createElement`로 변환된다.
```javascript
const ComponentA = /*#__PURE__*/React.createElement(A, {
require: true
}, "Hello World");
const ComponentB = /*#__PURE__*/React.createElement(React.Fragment, null, "Hello World");
const ComponentC = /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", null, "Hello World"));
```
> [!tip]- 두 모드의 차이
> **Classic Runtime** (React 16 이하, Babel 7.9 이전)
> - JSX는 `React.createElement()` 호출로 변환된다.
> - 변환된 코드가 React 객체를 직접 참조하기 때문에 React를 import해야 한다.
> ```jsx
> // JSX
> const el = <div className="hello">Hello World</div>
> // 변환 결과
> const el = React.createElement("div", { className: "hello" }, "Hello World");
> ```
> **Automatic Runtime** (React 17 이상, Babel 7.9 이후)
> - JSX는 `jsx()`, `jsxs()`, `Fragment` 같은 함수 호출로 변환된다.
> - 이 함수들은 `react/jsx-runtime`에서 자동으로 import되기 때문에 React를 직접 import 할 필요가 없다.
> ```jsx
> // JSX
> const el = <div className="hello">Hello World</div>
> // 변환 결과
> import { jsx as _jsx } from "custom-jsx-library/jsx-runtime";
> const el = _jsx("div", { className: "hello", children: "Hello World" });
> ```
> 참고: [@babel/plugin-transform-react-jsx · Babel](https://babeljs.io/docs/babel-plugin-transform-react-jsx)
어떤 런타임에서 트랜스파일했는지에 따라서 결과가 좀 다르긴 하지만 다음과 같은 공통점이 있다.
- **첫 번째 인수는 항상 요소 자체이다. (JSXElement)**
태그 이름 문자열(`"div"`), 컴포넌트 참조(`A`), 프래그먼트 식별자 등
- 이후 인수/객체는 속성(props)과 자식(children)을 담는 방식으로 전달된다.
```
<Component ...props>children</Component>
↓
(Component, props, children) 함수 호출
```
이렇게 구조가 동일하기 때문에 상황에 따라 다른 `JSXElement`를 랜더링해야 하는 상황에서 아래처럼 리팩토링 할 수 있다.
```jsx
// props 여부에 따라 다른 태그를 랜더링해야 하는 경우
// BEFORE
function TextOrHeading({
isHeading,
children
}: PropsWithChildren<{isHeading: boolean}>) {
return isHeading ? (
<h1 className="text">{children}</h1>
) : (
<span className="text">{children}</span>
)
}
// AFTER
import {createElement} from 'react'
function TextOrHeading({
isHeading,
children
}: PropsWithChildren<{isHeading: boolean}>) {
return createElement(
isHeading ? 'h1' : 'span',
{ className: 'text' },
children
)
}
```
> [!tip]- JSX에서 `createElement`를 사용하는 것이 권장되는 패턴일까.
> JSX는 `createElement`를 좀 더 이해하기 쉽게 사용하기 위해서 만들어진 문법이다.
> 그런데 JSX 형식의 코드를 리팩토링하기 위해서 `createElement`를 사용한다는 것이 좀 아이러니하다고 생각했다,, 🥺
> ---
> 하지만 props가 너무 길거나 한 경우에는 `createElement`를 쓰는 쪽이 좀 더 간결한 것 같기도 하다.
> 실제로 리팩토링 전 패턴을 써 본적이 있어서 더 고민하게 됨...