JSX란?

const element = <h1>Hello, world!</h1>;

지금이야 JSX에 익숙하지만, 처음 리액트를 배울 때만해도 도대체 이게 무슨 문법인가하는 생각이 들었다.
JSX에 대해서 공식문서는 이렇게 소개한다.

JSX는 JavaScript를 확장한 문법으로, JavaScript 파일을 HTML과 비슷하게 마크업을 작성할 수 있도록 해줍니다. 리액트에서는 JSX를 활용해 컴포넌트를 만들고 화면에 어떤 내용을 보여줄 지를 결정한다.

JSX = Javascript + HTML 라고 할 수 있을 것 같다

기존의 웹은 HTML, CSS, Javascript의 역할 분담이 확실했다. HTML로는 내용, CSS로는 스타일, Javascript로는 로직을 작성하며 HTML, CSS, Javscript를 각각의 분리된 파일로 관리한다. 하지만 점점 더 웹이 인터랙티브해지면서 로직이 어떤 내용을 보여줄 지 결정하는 경우가 많아졌다.

이런 이유로 리액트에서는 렌더링 로직과 마크업 로직이 함께 JSX라는 문법으로 존재하게 되었다.


JSX는 어떻게 HTML로 바뀌는 걸까?

리액트로 만든 프로젝트를 개발자도구를 켜서 보면 JSX로 만들었지만 일반 html로 만든 것과 동일하게 보인다. JSX는 브라우저가 직접 이해하지 못하기 때문에 트랜스파일링을 통해 브라우저가 이해할 수 있는 언어로 바꿔주는 작업이 필요하다.

이 부분에 대해 찾아볼 때 트랜스파일링을 하면 Babel과 같은 트랜스파일러에 의해 JSX 코드가 React.createElement로 변환된다.
그리고 React.createElement에 의해 자바스크립트 객체로 최종적으로 바뀐다라고 설명 되어 있는 내용을 많이 봤다.

하지만 리액트 공식 문서를 살펴보면 React.createElement레거시 api로 소개되어 있다.
오잉? 그렇다면 내가 찾아본 설명이 옛날 거고 지금은 내부 로직이 바뀐건가? 왜 createElement가 레거시 api로 되어 있지 싶어서 더 찾아봤다.


createElement가 레거시 api가 된 이유

2019년에 createElement에 대한 RFC가 올라왔다.

RFC란?
"Request for Comments"의 약자로, 기술적인 제안이나 표준을 문서화하는 방식으로, 오픈소스에서 새로운 기능이나 변경사항을 제안하고 논의하기 위한 문서 같은 의미로 사용됨

RFC에서 말하는 createElement의 문제점을 정리하면 이렇다

  • 트랜스파일링 된 createElement를 호출하기 위헤 불필요한 import React from 'react' 구문이 항상 필요함
  • 클래스 컴포넌트를 사용할 때는 의미가 있었지만 함수형 컴포넌트로 바뀌면서 의미가 없어진 기능들이 존재
  • createElement는 JSX의 사용을 생각하고 만든 것이 아니라 JSX 사용 없이 수동으로 작성할 수 있도록 한 것

이러한 문제로 인해 함수형 컴포넌트 + JSX 상황에 맞게 createElement 개선하는 작업이 진행되었다.
createElement 대한 더 자세한 내용은 RFC에서 확인할 수 있다.


개선된 JSX 트랜스파일링

리액트 17 버전 부터는 트랜스파일링 결과가 createElement가 아니라 jsx로 바뀐다.

jsx에 대한 내부 코드를 살펴보자.
jsx 함수는 dev, prod 환경에 따라 다른 함수를 사용한다. 운영 환경에서 사용되는 jsxProd만 살펴볼 예정이다.

코드를 처음 살펴보면서 공부하는 거라 어렵게 느껴지고 코드에 압도(?) 당하는 거 같지만 차근 차근 살펴보면 jsx 함수는 props, key를 설정하는 함수라는 걸 알 수 있다.

export function jsxProd(type, config, maybeKey) {
  let key = null;

  // key 설정
  if (maybeKey !== undefined) {
    // ...
    key = "" + maybeKey;
  }

  if (hasValidKey(config)) {
    // ...
    key = "" + config.key;
  }

  // props 설정
  let props;
  if (!("key" in config)) {
    props = config;
  } else {
    props = {};
    for (const propName in config) {
      // Skip over reserved prop names
      if (propName !== "key") {
        props[propName] = config[propName];
      }
    }
  }

  // ...

  // ReactElement 반환
  return ReactElement(
    type,
    key,
    undefined,
    undefined,
    getOwner(),
    props,
    undefined,
    undefined
  );
}

spread 구문으로 key 가 props로 전달되는 게 가능하지만 점차 deprecated할 예정인데,
이를 점진적으로 개선하기 위해 maybeKey를 사용해 props로 전달된 key와 명시적으로 전달된 key를 구분한다.

if (maybeKey !== undefined) {
  // ...
  key = "" + maybeKey;
}

if (hasValidKey(config)) {
  // ...
  key = "" + config.key;
}

jsx 함수에 대해 살펴 봤으니 그 다음으로는 jsx가 반환하는 ReactElement를 살펴보고자 한다.

function ReactElement(
  type,
  key,
  self,
  source,
  owner,
  props,
  debugStack,
  debugTask
) {
  // ...

  let element;

  // ...
  element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type,
    key,
    ref,
    props,
  };

  return element;
}

ReactElement 함수에 다른 내용도 있지만 간단히 정리하면 위와 같이 element 객체를 정의하고 그 객체를 반환하는 함수이다. 따라서, createElement에서 jsx로 코드가 바뀌었지만 동일하게 자바스크립트 객체로 최종적으로 반환한다.

// 이런 JSX 문법이
const element = <h1 className="greeting">Hello, world!</h1>;

// 이런 _jsx 호출로 바뀐다
import { jsx as _jsx } from "react/jsx-runtime";

const element = _jsx("h1", {
  className: "greeting",
  children: "Hello, world!",
});

// 최종적으로 자바스크립트 객체를 생성한다
const element = {
  type: "h1",
  props: {
    className: "greeting",
    children: "Hello, world!",
  },
};

childeren을 인자로 건내주었던 createElement와 달리 jsx 함수는 children을 props로 전달하고 있다.

React.createElement("h1", { className: "greeting" }, "Hello, world!");

jsx("h1", { className: "greeting", children: "Hello, world!" });

리액트는 이렇게 변환된 객체를 읽어서 DOM으로 렌더링 한다.

미니 리액트를 만들 때는 트랜스파일러까지는 작업하지 않고 jsx 함수가 최종적으로 반환하는 JSX 객체의 구조만 만들어서 DOM으로 렌더링 시키려고 한다.


JSX-Like 객체

{
  type: ""; // 태그 이름, 리액트 컴포넌트일 경우 대문자로 시작
  key: null; // 전달 받은 값이 있을 경우 전달 받은 값으로 할당, 기본은 null
  ref: null; // 전달 받은 값이 있을 경우 전달 받은 값으로 할당, 기본은 null
  props: {
    children: []; // 자식 노드 :: 문자열, 숫자, 빈 노드, 리액트 엘리먼트 등등
  }
}

리액트 트랜스파일링과 jsx 함수에 대한 내용을 참고해서 JSX-Like 객체의 구조를 이렇게 같이 잡았다.
이렇게 리액트의 내부 코드까지 살펴보면서 공부해본 적은 처음이라 정확히 이해가 전부 안 가는 부분들도 남아 있기는하다. 이해 안가면 계속 반복해서 보다보면 이해할 때가 있을 거라 생각하고 앞으로 계속 미니 리액트를 만들어가면서 더 이해가 깊어지지 않을까 생각한다.



참고