>[!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`를 쓰는 쪽이 좀 더 간결한 것 같기도 하다. > 실제로 리팩토링 전 패턴을 써 본적이 있어서 더 고민하게 됨...