JSX란?
XML이란?
JSX는 JavaScript XML(Extensible Markup Language)의 줄임말이다. 그렇다면 XML은 무엇인가?(개발 공부를 하다 보면 항상 이러한 재귀적 물음이 드는 상황이 자주 생기는 것 같다. 아무래도 지식이 아직 많이 모자라서 그런가 보다).
XML은 데이터를 저장하고 전달하는 데 있어 확장 가능하고 유연한 문법을 제공하는 마크업 언어이다. 유사한 언어론 HTML이 있는데, HTML은 웹 페이지의 구조와 콘텐츠를 표시한다면, XML은 특정한 데이터를 표현하고 전송하기 위한 형식 언어라는 점에선 차이가 있다. 또한, HTML은 어느 정도 정해진 태그들을 활용해 웹의 구조를 표현한다면, XML은 사용자 정의 태그를 사용할 수 있다는 특징도 있다.
아래는 XML을 사용해서 책에 대한 정보를 나타낸 표현이다.
<book>
<title>React for Beginners</title>
<author>Jay</author>
<price currency="USD">29.99</price>
</book>
XML은 JSON이 등장하기 전에 클라이언트-서버 사이의 통신에서 데이터를 주고받을 때 주로 사용됐다. 예를 들어, 서버에서 클라이언트에 전달하고자 하는 응답 내용을 아래와 같이 XML 언어로 전달되었다.
<response>
<status>success</status>
<data>
<id>123</id>
<name>Jay</name>
</data>
</response>
XML을 사용하면 계층적인 구조의 데이터를 저장하기에 유리하다. 위의 response
데이터도 계층을 표현해 보면 아래와 같이 나타낼 수 있다. 일종의 트리 구조가 된다.
또한, 트리 형태의 계층 구조는 자바스크립트의 객체 형태로도 변환이 수월하다.
const response = {
status: 'success',
data: {
id: 123,
name: 'Jay',
},
}
이렇게, XML은 사용자 정의 태그로 데이터의 위계를 표현할 수 있다는 특징이 있으며, JSX는 이러한 XML의 언어적 특징을 자바스크립트에서 활용할 수 있도록 지원된 확장판이라고 볼 수 있다.
JSX란?
그렇다면, 이제 JSX에 대해 다시 알아보자. 먼저 리액트 공식문서에서는 JSX를 '자바스크립트를 확장한 문법으로, Javascript 파일을 HTML과 비슷하게 마크업을 작성할 수 있도록 해줍니다'라고 설명하고 있다. 덧붙여서, '컴포넌트를 작성하는 다른 방법도 있지만, 대부분의 React 개발자는 JSX의 간결함을 선호하며 대부분의 코드 베이스에서 JSX를 사용합니다'라고도 설명하고 있다.
공식 문서의 설명처럼, JSX는 자바스크립트와 HTML과 유사한 형태로 마크업을 작성할 수 있게 해주는 확장 문법이다. JSX는 리액트 개발을 위한 유일한 대안은 아니지만, 충분히 편리하고 직관적이라는 큰 장점이 있다.
아래는 JSX로 작성한 코드와, 같은 코드를 리액트에서 기본 제공하는 React.createElement
메서드로 만든 코드이다.
import
// JSX 문법 사용
const elem1 = <h1 className='title'>Hello World!</h1>;
// React.createElement 사용
import React from 'react';
const elem2 = React.createElement(h1, { className: 'title }, 'Hello React!');
확실히 JSX 문법이 훨씬 간결하고 직관적이다. 특히, HTML과 거의 유사한 문법으로 사용이 가능하기 때문에 개발자가 UI의 구조를 손쉽게 잡는 데 아주 유용하다.
참고로, JSX는 리액트에서만 유효한 문법은 아니다. Vue.js, Emotion.js 등에서도 JSX를 사용한다. 하지만, 그 시작은 리액트의 선언적 UI라는 철학에 맞게 UI 구조를 직관적으로 표현하기 위한 문법으로 탄생됐으며, 리액트 오픈 소스를 주도했던 페이스북에서 만들어진 기술이다.
JSX Transform 과정 개요
JSX로 작성된 코드는 순수 자바스크립트로 변환되며, 해당 과정에서 트랜스파일러가 사용된다. 트랜스파일러란 작성된 코드를 다른 버전, 또는 다른 언어의 코드로 변환해 주는 변환기고, 비슷한 역할을 해주는 다양한 트랜스파일러가 존재한다(오랜 기간 Babel이 표준으로 사용됐지만 현재는 CRA는 Babel, Next.js 최신 버전은 SWC, Vite는 esbuild 등 다양하게 사용된다).
리액트 17 버전까진 JSX로 작성된 코드가 React.createElement
를 이용해 자바스크립트 객체로 변환되었다.
// JSX
<div className="wrapper">
<h1 className="title">'Hello, React!'</h1>
<div>
// 리액트 17까지의 변환 코드
React.createElement(
'div', // HTML 태그 (요소 타입)
{ className: 'wrapper' }, // props 객체
React.createElement( // children (하위 요소)
'h1', // HTML 태그 (요소 타입)
{ className: 'title' }, // props 객체
'Hello, React!' // children (태그 내용)
),
)
리액트 18 버전부턴 JSX 코드의 변환에 React.createElement
가 더 이상 사용되지 않는다. 대신, react/jsx-runtime
모듈의 _jsxs
와 _jsx
함수를 사용하는 것으로 변환 방식이 변경됐다(방식 변경으로 import React form 'react'
구문 생략 가능, 불필요한 코드를 최소화해서 번들 크기 감소, 성능 최적화의 장점을 얻을 수 있었다고 한다).
// 리액트 18 버전의 변환 과정
_jsxs("div", {
className: "wrapper",
children: [
_jsx("h1", { className: "title", children: "'Hello, React!'" })
]
});
변환 방식은 달라졌지만, 두 방식 모두 다 변환의 결과로 Javasrtipt 객체를 만들며, 해당 객체를 메모리 상에 저장해 둔다. 그리고, 메모리에 존재하는 해당 객체가 리액트 Element가 돼서 메모리 상에서 DOM을 관리하기 위해 사용되는 Virtual DOM의 구성 요소가 된다.
{
$$typeof: Symbol.for('react.element'), // React 요소를 나타내는 식별자
type: "div", // 태그 이름 (요소 타입)
key: null, // key 속성 (리스트에서 식별자, 없으므로 null)
ref: null, // ref 속성 (DOM 참조, 없으므로 null)
props: { // props 객체
className: "wrapper", // div의 클래스
children: [ // children 배열 (자식 요소)
{
$$typeof: Symbol.for('react.element'),
type: "h1",
key: null,
ref: null,
props: {
className: "title",
children: "'Hello, React!'" // h1의 텍스트 내용
},
_owner: null // React 내부에서 소유자 추적 (개발 도구용)
}
]
},
_owner: null // React 내부에서 소유자 추적 (개발 도구용)
}
Virtual DOM이 메모리 상에서 자바스크립트 객체 형태로 관리된다고 봤던 것 같은데, 아마도 실제 Virtual DOM의 메모리 상 구조는 위의 객체들이 DOM의 실제 구조에 따라 props.children
에 서로 연결되면서 트리 형태의 구조를 갖게 된 모습이지 않을까 싶다.
JSX 사용 시 주의점
JSX 문법은 주로 리액트 컴포넌트 내부에서 사용되며, HTML 태그와 형태가 유사하지만 조금 더 엄격한 규칙이 적용된다. 실제론 규칙을 지키지 않은 경우 알아서 에러를 뱉어주지만, 그래도 JSX의 특성을 이해하기 위해 잘 알아두자.
- 하나의 루트 엘리먼트로 반환하기: 하나의 컴포넌트는 반드시 하나의 리액트 엘리먼트를 반환해야 한다. 만약에 여러 리액트 엘리먼트를 반환하려면 하나의 부모 태그로 감싸야한다. 만약에 실제 DOM에서 하나의 태그로 감싸지 않고 여러 태그를 나열하고 싶다면
<Fragment>
(또는<>
)를 사용하자. - 모든 태그는 닫아주기: HTML은 규칙이 덜 엄격하게 적용하기 때문에 닫는 태그(e.g.
</div>
)가 없어도 문제가 발생하지 않을 수 있다. 하지만, 리액트는 엄격한 규칙이 사용되기 때문에 닫는 태그가 반드시 필요하다. 만약에img
태그처럼 태그가 쌍으로 필요하지 않다면<img />
처럼 닫아주자. - (거의) 대부분 캐멀 케이스로!: JSX에서 작성하는 어트리뷰트는 자바스크립트 문법을 따르기 때문에 캐멀 케이스로 사용해야 한다(
background-color
가 아니라backgroundColor
이다). 또한, 자바스크립트에서 예약어로 사용된 몇몇 HTML 어트리뷰트는 리액트에선 사용이 불가하기 때문에(자바스크립트로 변환 시 충돌하기 때문에) 다른 키워드가 주어진다. 예를 들어,class
는className
이라고 사용해야 하고,label
태그의for
는forHTML
이라고 사용해야 한다.
결론
JSX와 JSX가 변환되어 객체화된 Virtual DOM은 리액트를 이루는 코어인 게 아닌가 싶다. 특히 JSX 문법으로 작성된 코드가 변환되어 만들어지는 객체와, 해당 객체를 활용해서 DOM을 업데이트하는 로직을 잘 이해하면 리액트에 대한 이해가 한층 더 깊어질 수 있지 않을까 싶다. 아직 모르는 게 너무 많고, 한 발 더 나아가기엔 한계가 있는 듯하여, 아쉽지만 더 깊게 파보는 과정은 미뤄두지만, 나중엔 꼭 좀 더 심화해서 공부해 봐야겠다.