
이번 아티클에서는 본격적으로 React의 렌더링 과정을 살펴보기에 앞서서 React를 사용할때 함께 사용하는 jsx 문법에 대해서 파헤쳐보겠습니다.
렌더링을 하기전에 jsx 변환하기
React와 jsx 문법을 사용하여 Hello world를 렌더링하는 코드를 작성하면 아래와 같을것입니다.
<html>
<body>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="container"></div>
<script>
const App = () => {
return <div className="content">hello world</div>
}
ReactDOM.createRoot(document.getElementById('container')).render(<App/>);
</script>
</body>
</html>그런데 위 코드를 웹브라우저에서 곧바로 실행해보면 바로 syntaxError가 발생합니다.
왜냐하면 jsx가 js파일 내부에서 사용되는것은 맞지만 유효한 자바스크립트 문법이 아니기 때문입니다.
따라서 jsx 문법을 사용할 경우 코드를 브라우저에서 실행하기 이전에 바벨의 @babel/plugin-transform-react-jsx 플러그인을 사용하여 브라우저에서 실행가능한 코드로 변환해야합니다.
create-react-app등을 이용할 경우 코드가 변환된뒤 실행되기에 실제로 변환된 코드를 보게될일은 없지만, 바벨을 설정하고 변환해보면 다음과 같습니다.
npm install --save-dev @babel/core @babel/cli @babel/plugin-transform-react-jsx
// babel.config.json
{
"presets": [],
"plugins": ["@babel/plugin-transform-react-jsx"]
}// 바벨 변환전
const App = () => {
return <div className="content">hello world</div>
}
// 바벨 적용후
const App = () => {
return createElement(
'div',
{ className: 'content' },
'hello world'
);
}jsx에서 변환된 결과를 보면 jsx가 포함하고 있는 대부분의 정보가 포함되어, 실제 javascript로 실행가능한 형태가 되었음을 알 수 있습니다. 그렇다면 이렇게 만들어진 createElement 함수가 리턴하는 ReactElement는 무엇이고, 함수가 하는 역할은 무엇일까요?
CreateElement 함수가 하는일
먼저 createElement에 넣는 인자에 대해서 살펴보겠습니다. 첫번째 인자(tag)는 컴포넌트 혹은 태그이름, 두번째 인자(props)는 props객체, 세번째 이후의 인자는 (children)은 자식 elements입니다. 위 예제에서는 세번째 인자까지만 있지만 자식요소가 여러개일 경우 네번째, 다섯번째등 이후에 자식요소를 인자로 받게됩니다.
이러한 인자를 이용해 createElement가 하는일은 무엇일까요? 먼저 텍스트로 살펴본뒤 코드를 통해 확인해보세요
- props가 있는 경우 예약된 props인 key와 ref는 각각 변수에 담고, 나머지는 props 객체에 넣어줍니다.
- children이 하나 있는경우 그대로, 2개 이상인경우 배열에 넣은뒤 props.children에 담습니다.(0개이면 아무일도하지 않습니다.)
- defaultProps가 있고, props에 값이 할당되지 않은경우 props에 defaultProps를 넣어줍니다.
- 앞서 만들어낸 정보들을 이용하여 ReactElement를 리턴합니다.
export function createElement(type, config, children) {
let propName;
// 예약된 props인 key, ref는 저장되지 않습니다.
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
// 1. props가 있는 경우 예약된 props인 key와 ref는 각각 변수에 담고, 나머지는 props 객체에 넣어줍니다.
if (config != null) {
// props로 ref를 넘기는 경우
if (hasValidRef(config)) {
// ref 변수에 props ref를 저장합니다.
ref = config.ref;
}
// props로 key를 넘기는 경우
if (hasValidKey(config)) {
// key 변수에 문자열화 된 key를 저장합니다. 즉 0과 "0"은 같은 key입니다.
key = '' + config.key;
}
for (propName in config) {
// 현재 예약된 props인 key, ref를 제외한 나머지 props는 props 객체에 넣어줍니다.
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// 2. children이 하나 있는경우 그대로, 2개 이상인경우 배열에 넣은뒤 props.children에 담습니다.(0개이면 아무일도하지 않습니다.)
// createElement의 인자는 세개 뿐이지만 실제로 여러개의 children이 들어올수 있습니다.
const childrenLength = arguments.length - 2;
// children이 하나인 경우 props.children에 children을 그대로 넣어줍니다.
if (childrenLength === 1) {
props.children = children;
// children이 두개 이상인 경우 props.children에 children들을 배열에 넣어 넣어줍니다.
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
// 3. defaultProps가 있는 경우 props에 값이 할당되지 않은경우에 한해 넣어줍니다.
/*
아래 코드의 defaultProps를 props가 넘어온것처럼 넣어줍니다.
Person.defaultProps = {
name: "Rahul",
eyeColor: "deepblue",
age: "45"
}
*/
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 4. 앞서 만들어낸 정보들을 이용하여 ReactElement를 리턴합니다.
return ReactElement(
type,
key,
ref,
props,
);
}이제 ReactElement 함수가 반환하는것이 무엇인지 살펴보면 jsx의 결과물이 무엇인지 알수 있을것입니다.
const ReactElement = function(type, key, ref, props) {
return {
// 우리가 넣지 않은 새로운 값입니다.
$$typeof: REACT_ELEMENT_TYPE,
// 앞서 살펴본 값이 모두 그대로 삽입됩니다.
type: type,
key: key,
ref: ref,
props: props,
};
}ReactElement 함수는 우리가 인자로 넘긴 값들을 객체에 넣어 그대로 반환하고 있습니다. 따라서 jsx는 순수 자바스크립트 객체로 변환됨을 알 수 있습니다. 여기서 두가지사실을 짚고 넘어가면 좋겠습니다.
한가지는 우리가 key와 ref를 props로 넣지만 elements에서는 따로 관리한다는 사실입니다. 이러한 점을 볼때 key와 ref는 컴포넌트 props에서 꺼내 쓰는 방식이 사실상 불가능하고, react 내부적으로 이용됨을 알 수 있습니다. 내부적으로 이 값들이 어떻게 사용되는지는 추후 아티클에서 정확하게 알 수 있을것입니다.
다른 한가지는 인자로 넘기지 않지만 객체에 추가되는 프로퍼티인 $$typeof 이 프로퍼티의 존재 이유입니다. 이는 Element의 구분 및 보안을 위해서입니다. Element를 구분한다는 측면에서 살펴보면 우리가 ReactElement 함수를 위해 만들어낸 element는 REACT_ELEMENT_TYPE 이며, memo 컴포넌트를 이용해 만든 것은 REACT_MEMO_TYPE입니다. 이밖에도 더많은 타입이 있습니다. 보안 측면에서 살펴보면 바로 이들 타입 상수가 문자나 상수 리터럴이 아닌 Symbole.for를 사용하고 있습니다. 심볼을 사용하는 이유는 ReactElement를 json으로 관리해 서버에서 내려받는경우를 방지하기 위함입니다. Json을 통해 Symbol을 만들수는 없기 때문입니다. 만약 Symbol을 지원하지 않으면 대신 숫자를 사용하게 됩니다.
createElement 대신 jsx의 사용
우리가 앞서 살펴본 createElement는 React가 0.xx 버전일때부터 사용하던 함수로 최근에는 jsx라는 함수로 변경된 상태입니다. createElement 대신 jsx를 사용하여 파싱하면 다음과 같습니다.
const App = () => {
return jsx(
'div',
{ className: 'content',children:'hello world' },
undefind
);
}인자를 살펴보면 다른점이 있음을 알 수 있습니다. 물론 인자 뿐만 아니라 jsx 함수 내부적으로도 약간의 차이가 있습니다. 코드를 한번 살펴봅시다.
/**
* https://github.com/reactjs/rfcs/pull/107
* @param {*} type
* @param {object} props
* @param {string} key
*/
export function jsx(type, config, maybeKey) {
let propName;
// Reserved names are extracted
const props = {};
let key = null;
let ref = null;
// Currently, key can be spread in as a prop. This causes a potential
// issue if key is also explicitly declared (ie. <div {...props} key="Hi" />
// or <div key="Hi" {...props} /> ). We want to deprecate key spread,
// but as an intermediary step, we will use jsxDEV for everything except
// <div {...props} key="Hi" />, because we aren't currently able to tell if
// key is explicitly declared to be undefined or not.
if (maybeKey !== undefined) {
if (__DEV__) {
checkKeyStringCoercion(maybeKey);
}
key = '' + maybeKey;
}
if (hasValidKey(config)) {
if (__DEV__) {
checkKeyStringCoercion(config.key);
}
key = '' + config.key;
}
if (hasValidRef(config)) {
ref = config.ref;
}
// Remaining properties are added to a new props object
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
// Resolve default props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
return ReactElement(
type,
key,
ref,
undefined,
undefined,
ReactCurrentOwner.current,
props,
);
}앞서 살펴본것과 거의 유사한데, 약간의 차이점이 있습니다. 주요한 차이점을 하나씩 살펴보겠습니다.
children을 props에 담기
createElement에서는 세번째 이후의 인자에 children을 담고, 함수내부에서 props.children에 인자들을 모두 담아주었습니다.
// 바벨 변환전
const App = () => {
return <div className="content">hello world</div>
}
// 바벨 적용후
const App = () => {
return createElement(
'div',
{ className: 'content' },
'hello world'
);
}하지만 jsx에서는 props의 children 프로퍼티에 children이 담긴 배열이 추가됩니다.
// 바벨 변환전
const App = () => {
return <div className="content">hello world</div>
}
// 바벨 적용후
const App = () => {
return jsx(
'div',
{ className: 'content',children:'hello world' },
undefind
);
}
// 바벨 변환전
const App = () => {
return <div className="content">{a}{b}</div>
}
// 바벨 적용후
const App = () => {
return jsx(
'div',
{ className: 'content',children:[a,b] },
undefind
);
}이는 jsx와 jsxs 함수를 사용함으로써 key경고를 발행하는데 어려움이 없도록 해줍니다. (jsxs에 만 key 검사해서 경고 날리면됨)
props 객체에서 key 확산을 더이상 사용하지 않음
현재는 props의 스프레드 연산자를 이용해 key를 전달할수 있습니다. 이방법의 문제는 props에 실제로 key가 포함되어있는지 정적으로 알 수 없다는것입니다. 이를 알기위해서는 런타임에 객체의 모든 프로퍼티 키를 확인해보아야합니다.
let randomObj = {key: 'foo'};
let element = <div {...randomObj} />;
element.key; // 'foo'이전버전과의 호환성을 위해 현재는 key를 스프레드에 추가할수 있지만, 더이상 props에서 끌어내주지 않는다는 경고를 발행합니다. 추후 주요 버전에서 props에서 key를 추출하지 않는것이 예정되어 있습니다.
let {key, ...props} = obj;
<div key={key} {...props} />자동 가져오기
jsx 함수 자체의 변화는 아니지만, 기존에 React를 사용하는 모든 페이지에서는 import React from "react"를 사용해 컴파일에 필요한 createElement함수를 불러와야만 했습니다.
// 변환전
import React from 'react';
function App() {
return <h1>Hello World</h1>;
}
// 변환후
import React from 'react';
function App() {
return React.createElement('h1', null, 'Hello world');
}하지만 이는 트랜스파일 타임에 생성해주면 되는것이기에, react 17 버전부터는 더이상 import React from "react"를 명시하지 않아도, jsx 변환시점에 자동으로 import {jsx} "react"가 추가되도록 변경되었습니다.
// 변환전
function App() {
return <h1>Hello World</h1>;
}
// 변환후
import {jsx as _jsx} from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: 'Hello world' });
}모든 내용을 살펴보지 않은 만큼 더 많은 내용이 궁금하시다면 아래 링크를 참고해보세요! https://github.com/reactjs/rfcs/pull/107
마치며
이번 아티클에서는 jsx에서 ReactElemen까지 가는 길을 살펴보았습니다. 간단하였을테지만, 다음 아티클에서는 이를 통해 생성되는 fiber에 대해서 살펴보겠습니다.
참고자료
- https://babeljs.io/docs/babel-plugin-transform-react-jsx
- https://github.com/reactjs/rfcs/pull/107
