Limsh.io

React는 어떻게 JSX를 구분할까?


  • React를 사용할 때 우리는 JSX 문법으로 작성된 컴포넌트를 아래와 같이 자연스럽게 사용하고 있습니다.
const App = () => {
  return <div>Hello React</div>;
};
  • 하지만 이처럼 작성된 JSX는 브라우저에서 직접 해석될 수 없습니다.
  • JSX는 문법 설탕(syntactic sugar)일 뿐이며, 실제로는 React.createElement()로 변환되어 JavaScript 객체가 만들어집니다.
  • 그렇다면 provider, forward_ref, Portal, suspense과 같은 고급 기능들은 JSX로 작성했을 때 어떻게 내부적으로 JSX를 구분할까요?
  • 이번 글에서는 React 내부의 Symbol 시스템을 통해 JSX가 어떻게 구분되는지 알아보겠습니다.

JSX는 결국 React.createElements로 변환된다.

  • JSX는 Babel등을 통해 컴파일되면 다음과 같이 변환됩니다.
const element = React.createElement('div', null, 'Hello React');
  • React의 공식 문서를 보면 createElement는 다음과 같은 객체를 반환합니다.
- 반환값
createElement는 아래 프로퍼티를 가지는 React 엘리먼트 객체를 반환합니다.

type: 전달받은 type.
props: ref와 key를 제외한 전달받은 props.
ref: 전달받은 ref. 누락된 경우 null.
key: 전달받은 key를 강제 변환한 문자열. 누락된 경우 null.

직접 확인해보자.

  • 실제로 React.createElement()가 어떤 객체를 반환하는지 직접 확인해보았습니다.
  • 아래와 같은 테스트 코드를 작성했습니다.
import { createElement } from "react";

function Component() {
  return createElement("h1", { className: "greeting" }, "Hello");
}

const App = () => {

  console.log(<Component/>)
  return (
    <>
      <Component/>
    </>
  );
};

export default App;
  • 여기서 <Component />는 내부적으로 createElement(Component, null)로 변환됩니다.
  • 이 함수(createElement)의 원본 정의를 따라가보면 다음과 같은 구현을 확인할 수 있습니다
exports.createElement = function (type, config, children) {
  // ... 중략 ...
  return ReactElement(type, key, void 0, void 0, null, props);
};
  • 내부에서 어떤 동작이 이루어지는 보지않고 저는 반환값만 확인하겠습니다.
  • ReactElement를 반환하는군요. ReactElement로 이동하겠습니다.

ReactElement의 정체는?

  • ReactElement 함수는 다음과 같은 객체를 반환합니다.
function ReactElement(type, key, self, source, owner, props) {
  self = props.ref;
  return {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    ref: void 0 !== self ? self : null,
    props: props
  };
}
  • 바로 여기서 우리가 주목해야 할 핵심 속성이 등장합니다.
  • 그것은 바로 $$typeof입니다.
  • 위에서 createElement는 전달받은 { type, props, ref, key }를 반환한다 했는데, $$typeof: REACT_ELEMENT_TYPE이라는 값이 존재합니다.
  • $$typeof가 뭘 의미하는 걸까요?

JSX 타입은 $$typeof로 구분된다.

  • $$typeof는 React 내부에서 사용하는 특수한 식별자입니다.
  • 이 값은 Symbol로 정의되어 있으며, React 요소가 어떤 종류의 요소인지를 구분하는 데 사용됩니다.
  • 이 심볼 값 덕분에 React는 이 객체가 일반 DOM 요소인지, 포탈인지, 프래그먼트인지 등을 정확히 구분할 수 있습니다.
  • 실제로 다음과 같은 타입으로 JSX를 구분합니다.
  • 이 타입들이 궁금해 React GitHub에 들어가 소스코드를 탐색해 보았습니다.

React GitHub

// ATTENTION
// When adding new symbols to this file,
// Please consider also adding to 'react-devtools-shared/src/backend/ReactSymbols'
// The Symbol used to tag the ReactElement-like types. If there is no native Symbol
// nor polyfill, then a plain number is used for performance.
var REACT_ELEMENT_TYPE = 0xeac7;
var REACT_PORTAL_TYPE = 0xeaca;
exports.Fragment = 0xeacb;
var REACT_STRICT_MODE_TYPE = 0xeacc;
var REACT_PROFILER_TYPE = 0xead2;
var REACT_PROVIDER_TYPE = 0xeacd;
var REACT_CONTEXT_TYPE = 0xeace;
var REACT_FORWARD_REF_TYPE = 0xead0;
var REACT_SUSPENSE_TYPE = 0xead1;
var REACT_SUSPENSE_LIST_TYPE = 0xead8;
var REACT_MEMO_TYPE = 0xead3;
var REACT_LAZY_TYPE = 0xead4;
var REACT_BLOCK_TYPE = 0xead9;
var REACT_SERVER_BLOCK_TYPE = 0xeada;
var REACT_FUNDAMENTAL_TYPE = 0xead5;
var REACT_SCOPE_TYPE = 0xead7;
var REACT_OPAQUE_ID_TYPE = 0xeae0;
var REACT_DEBUG_TRACING_MODE_TYPE = 0xeae1;
var REACT_OFFSCREEN_TYPE = 0xeae2;
var REACT_LEGACY_HIDDEN_TYPE = 0xeae3;

if (typeof Symbol === 'function' && Symbol.for) {
  var symbolFor = Symbol.for;
  REACT_ELEMENT_TYPE = symbolFor('react.element');
  REACT_PORTAL_TYPE = symbolFor('react.portal');
  exports.Fragment = symbolFor('react.fragment');
  REACT_STRICT_MODE_TYPE = symbolFor('react.strict_mode');
  REACT_PROFILER_TYPE = symbolFor('react.profiler');
  REACT_PROVIDER_TYPE = symbolFor('react.provider');
  REACT_CONTEXT_TYPE = symbolFor('react.context');
  REACT_FORWARD_REF_TYPE = symbolFor('react.forward_ref');
  REACT_SUSPENSE_TYPE = symbolFor('react.suspense');
  REACT_SUSPENSE_LIST_TYPE = symbolFor('react.suspense_list');
  REACT_MEMO_TYPE = symbolFor('react.memo');
  REACT_LAZY_TYPE = symbolFor('react.lazy');
  REACT_BLOCK_TYPE = symbolFor('react.block');
  REACT_SERVER_BLOCK_TYPE = symbolFor('react.server.block');
  REACT_FUNDAMENTAL_TYPE = symbolFor('react.fundamental');
  REACT_SCOPE_TYPE = symbolFor('react.scope');
  REACT_OPAQUE_ID_TYPE = symbolFor('react.opaque.id');
  REACT_DEBUG_TRACING_MODE_TYPE = symbolFor('react.debug_trace_mode');
  REACT_OFFSCREEN_TYPE = symbolFor('react.offscreen');
  REACT_LEGACY_HIDDEN_TYPE = symbolFor('react.legacy_hidden');
}
  • 이처럼 React는 초기에는 숫자를 기본값으로 설정하고, 환경이 Symbol을 지원하면 런타임에 Symbol을 덮어씌워 사용합니다.
  • React는 JSX를 단순히 문자열이나 DOM 노드처럼 취급하지 않습니다.
  • JSX는 이처럼 내부에 있는 $$typeof를 기준으로 철저하게 타입을 구분합니다.
  • 이를 통해 다양한 구조의 React 요소들—컴포넌트, 포탈, 프래그먼트, 서스펜스 등—을 유연하게 처리할 수 있는 것이죠.

createPortal과 같은 훅도 직접 만들 수 있다?

  • 위의 심볼 타입을 활용하면 ReactDOM.createPortal을 사용하지 않고도, 다음과 같이 포탈 객체를 수동으로 생성할 수 있습니다.
import { ReactNode } from "react";

import { createPortal } from "@/createPortal";

const App = () => {
  return (
    <div style={{ border: '2px solid black' }}>
      <p>This child is placed in the parent div.</p>
      {createPortal(
        <p>This child is placed in the document body.</p>,
        document.body
      ) as unknown as PortalNode}
    </div>
  );
};

export default App;
import { ReactNode } from "react";

import { isValidContainer } from "@/utils";

export interface PortalLike {
  $$typeof: symbol | number;
  key: string | null;
  children: ReactNode;
  containerInfo: Element | DocumentFragment;
};

export const createPortal = (
  children: ReactNode,
  container: Element | DocumentFragment,
  key?: string | number
): PortalLike => {

  if (!isValidContainer(container)) {
    throw Error("Target container is not a DOM element.");
  }

  return {
    $$typeof: Symbol.for('react.portal'), // 포탈 타입을 반환
    key: key === null ? null : String(key),
    children,
    containerInfo: container,
  };
};
export const isValidContainer = (container: Element | DocumentFragment) => {
  if(!container) return false;
  return container.nodeType === 1 || // Element
         container.nodeType === 9 || // Document
         container.nodeType === 11 // DocumentFragment
};
  • 이 객체를 반환하는 컴포넌트를 JSX에서 사용하면, React는 실제 포탈로 인식하고 document.body에 해당 내용을 렌더링합니다.

마무리

  • React는 우리가 작성한 JSX를 단순히 “HTML처럼 생긴 무언가”로 해석하지 않습니다.

  • 모든 JSX는 결국 React.createElement()를 통해 객체화된 구조로 변환되며,

  • 이 객체는 내부의 $$typeof를 통해 자신이 어떤 종류의 요소인지를 명확히 표현합니다.

  • 이를 통해 다양한 구조의 요소(컴포넌트, 포탈, 프래그먼트 등)를 유연하게 처리할 수 있는 것이죠.

  • 만약 직접 포탈 객체를 생성하거나 커스텀 렌더러를 만들 계획이 있다면, 이 Symbol 기반 시스템을 이해하는 것이 매우 큰 도움이 될 것입니다.