🎈 Chapter 2: JSX
JSX의 X는 자바스크립트의 확장 구문(eXtension)을 뜻합니다. 자바스크립트 XML이라고도 합니다.
자바스크립트 XML?
JSX는 개발자가 자바스크립트 코드 내에서 HTML과 유사한 코드를 작성할 수 있게 하는 자바스크립트용 구문 확장자입니다. JSX는 별도의 언어가 아니라 컴파일러나 트랜스파일러에 의해 일반 자바스크립트 코드로 변환되는 확장 구문입니다. JSX 코드는 컴파일 과정을 거쳐 일반 자바스크립트 코드로 변환됩니다.
JSX를 사용한 목록의 예시
const MyComponent = () => (
<section id="list">
<h2>My List</h2>
<p>My list of things</p>
<ul>
{amazingThings.map((thing) => (
<li key={thing.id}>{thing.label}</li>
))}
</ul>
</section>
)
똑같은 목록을 JSX 없이 작성한 예시입니다.
const MyComponent = () => React.createElement(
'section',
{ id: 'list' },
React.createElement('h2', {}, 'My List'),
React.createElement('p', {}, 'My list of things'),
React.createElement(
'ul',
null,
amazingThings.map((thing) => React.createElement('li', { key: thing.id }, thing.label))
)
)
JSX의 장점
- 더 쉬운 읽기 및 쓰기
- 향상된 보안
- 새로운 엘리먼트를 생성할 수 있는
'<'
및'>'
같은 위험한 문자가 HTML 문자열에 포함되어있다면, JSX 코드를 컴파일할 때 다른 문자로 바꿔 더 안전한 자바스크립트 코드를 생성합니다.
- 새로운 엘리먼트를 생성할 수 있는
- 강력한 타이핑
- 컴포넌트 기반 아키텍처
- 광범위한 사용
- JSX는 리액트 업계에서 널리 사용될 뿐만 아니라 리액트가 아닌 라이브러리와 프레임워크에서도 지원됩니다.
JSX의 약점
- 학습 곡성 가중
- 전용 도구 필요
- JSX 코드를 실행하려면 먼저 일반 자바스크립트 코드로 컴파일해야 하기 때문에 이를 위한 개발 도구가 추가되어야 합니다.
- 관심사 혼합
- 일부 개발자들은 JSX가 HTML과 유사한 코드를 자바스크립트 코드에 결합함으로써 관심사를 혼합시키고, 표현과 논리를 분리하기 어렵게 만든다고 주장합니다.
- 자바스크립트 호환성 부족
- JSX는 인라인 표현식을 지원하지만 인라인 블록은 지원하지 않습니다. (switch, if, for, while, etc.)
JSX를 사용하면 강력하고 유연한 방식으로 컴포넌트를 만들고 사용자 인터페이스를 작성할 수 있습니다.
JSX는 브라우저에 전달되기 전에 바닐라 JS가 됩니다. 이 동작이 어떻게 이루어지는지 내부를 들여다보겠습니다.
내부 동작
코드는 어떻게 작동하나요?
컴파일러는 특정 규칙에 따라 고급 프로그래밍 언어로 작성된 소스 코드를 구문 트리로 변환하는 소프트웨어입니다. 여기서 구문 트리란 문자 그대로 자바스크립트 객체 같은 트리 자료 구조입니다.
컴파일러는 (적어도 자바스크립트에서는) 3단계 과정을 거칩니다. 각각 토큰화, 구문 분석, 코드 생성이라고 하는데 각 단계에 대해 자세히 살펴보겠습니다.
토큰화
간단하게 말하자면 문자열을 의미 있는 토큰으로 분해하는 것입니다. 토크나이저가 상태를 가지고 있고, 각 토큰이 자신의 부모나 자식에 관한 상태를 포함하고 있을 때는 토크나이저를 렉서라고 부릅니다. 한마디로 렉싱은 상태를 가지는 토큰화입니다.
렉서는 렉서 규칙이 있어서 이 규칙으로 정규 표현식을 사용해 프로그래밍 언어를 나타내는 텍스트 문자열에서 변수 이름, 객체 키 및 값 같은 주요 토큰을 감지합니다. 그런 다음 렉서는 구현에 따라 이러한 키워드를 열거 가능한 값으로 표현합니다. 가령 const
는 0으로, let
은 1로, function
은 2로 바뀝니다.
문자열이 토큰화되거나 렉싱되면 다음 단계인 구문 분석으로 넘어갑니다.
구문 분석
토큰을 가져와 구문 트리로 변환하는 과정입니다. 구문 트리는 코드의 구조를 나타내는 자료 구조입니다.
{
"type": "Program",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "console.log"
},
"arguments": [
{
"type": "Literal",
"value": "Hello World",
"raw": "\"Hello World\""
}
]
}
}
]
}
구문 분석기 덕분에 문자열은 JSON 객체가 됩니다.
코드 생성
컴파일러가 추상 구문 트리(AST)에서 기계어를 생성하는 과정입니다.
컴파일러는 여러 종류가 있으며, 저마다 특성과 사용 목적이 다릅니다. 가장 일반적인 컴파일러 종류는 다음과 같습니다.
- 네이티브 컴파일러
- 크로스 컴파일러
- JIT 컴파일러
- 코드를 미리 변환하지 않고 실행할 때 기계어로 변환합니다. JIT(Just-in-time) 컴파일러는 자바 가상 머신 같은 가상 머신에서 일반적으로 사용되며, 기존 인터프리터보다 성능이 훨씬 우수합니다.
- 인터프리터
- 컴파일하지 않고 소스 코드를 직접 실행합니다.
웹 브라우저를 비롯해 최신 환경에서는 자바스크립트 코드를 효율적으로 실행하기 위해 JIT 컴파일러는 많이 사용합니다.
런타임은 일반적으로 엔진과 연동해 특정 환경에 맞는 콘텍스트 헬퍼와 기능을 더 많이 재공합니다. 가장 인기 있는 자바스크립트 런타임은 구글 크롬 같은 일반적인 웹 브라우저입니다.
크롬 웹 브라우저는 엔진과 연동하는 크로미움 런타임을 제공합니다. 서버 측에서는 V8 엔진을 사용하는 Node.js 런타임을 사용합니다.
런타임은 자바스크립트 엔진에 브라우저 런타임이 제공되는 window
객체와 document
객체 같은 콘텍스트를 제공합니다.
JSX로 자바스크립트 구문 확장하기
자바스크립트 구문을 확장하려면 새로운 구문을 이해하는 다른 엔진이 필요하거나, 엔진보다 앞서 새로운 구문을 처리해야 합니다. 자바스크립트 엔진은 매우 다양한 곳에서 사용되므로 제작과 유지 보스에 많은 고민이 필요합니다. 따라서 JSX만을 위한 새로운 엔진을 만든다는 건 거의 불가능합니다.
기존 엔진보다 앞서 새로운 구문을 처리하도록 하는 게 당연히 더 쉽습니다. 새 구문이 엔진에 도달하기 전에 처리하는 과정을 살펴봅시다. 이를 위해서는 확장 언어를 이해하는, 다시 말해 확장 언어로 작성된 코드 문자열을 이해할 수 있는 렉서와 구문 분석기를 만들어야 합니다. 종래의 방식에서는 다음 단계로 기계어를 생성했지만, 여기서는 구문 트리를 사용해 기존의 모든 자바스크립트 엔진이 이해할 수 있는 일반적인 바닐라 JS를 생성합니다. 이것이 바로 자바스크립트 생태계에서 바벨(babel)이 하는 일이고, 타입스크립트, 트레이서, SWC 같은 도구가 하는 일입니다.
이 때문에 JSX는 브라우저에서 직접 사용할 수 없고, 특수한 구문 분석기를 통해 구문 트리로 컴파일하는 '빌드 단계'가 필요합니다. 이 코드는 이러한 과정을 거쳐 바닐라 JS로 변환되어 최종 배포용 번들에 포함됩니다. 이 과정을 가리켜 트랜스파일한다고 하는데, 달리 표현하면 코드를 변환한 후 컴파일한다는 말입니다.
트랜스파일은 한 언어로 작성된 소스 코드를 추상화 수준이 비슷한 다른 언어로 변환하는 과정입니다. 그래서 소스 대 소스 컴파일이라고도 합니다.
JSX 프라그마
JSX 프라그마(pragma: 컴파일러에 특정 작업을 수행하도록 지시하는 지시어를 뜻합니다.)는 모두 <
로 시작하는데, 사실 자바스크립트에서 이 문자는 비교 연산할 때를 제외하면 인식하지 못하는 문자입니다. 자바스크립트 엔진은 이 문자를 발견하면 Syntax Error: Unexpected token '<'
오류를 던집니다.
구문 분석기가 <
프라그마를 만날 때 호출할 함수의 이름은 설정 가능한데, 앞서 설명한 것처럼 React.createElement
(예전 버전) 또는 _jsxs
(최신 버전)가 기본값으로 정해집니다. 이 함수의 시그니처는 다음과 같습니다.
function pragma(tag, props, ...children)
이 함수는 tag
, props
, children
을 인자로 받습니다. JSX가 일반 자바스크립트 구문으로 바뀌는 방식은 다음과 같습니다. 먼저 JSX 코드를 보겠습니다.
<MyComponent prop="속성값">콘텐츠</MyComponent>
이 코드는 다음과 같은 자바스크립트 코드가 됩니다.
React.createElement(MyComponent, { prop: "속성값" }, "콘텐츠");
이러한 변환이 바로 여러 차례 반복적으로 호출되는 함수에 관한 문법 설탕(syntax sugar)인 JSX 프라그마의 역할입니다. 사실상 JSX 프라그마는 React.createElement
대신 <
문자를 사용하는 별칭에 불과합니다.
표현식
JSX의 강력한 기능 하나는 엘리먼트 트리 내부에서 코드를 실행하는 것입니다.
const a = 1;
const b = 2;
const MyComponent = () => <Box>표현식입니다: {a + b}</Box>;
이렇게 하면 중괄호 안의 내용이 표현식으로 실행되므로 표현식입니다: 3
이 렌더링됩니다.
JSX 표현식은 말 그대로 표현식입니다. JSX 엘리먼트 트리 내부에서는 문장을 실행하지 못합니다. 따라서 다음 방식은 동작하지 않습니다.
const MyComponent = () => <Box>표현식입니다: {
const a = 1;
const b = 2;
if (a > b) {
3
}
}</Box>;
이 코드는 동작하지 않습니다. 문장이 아무런 값도 반환하지 않는데, 값을 반환하지 않으면서 상태를 설정하는 부작용으로 간주되기 때문입니다.