React - createElement 알아보기

Dev.Jo·2023년 7월 14일
1

리액트

목록 보기
2/2
post-thumbnail

React 공식문서 - createElement를 번역하며...

최근 리액트 공식문서 번역에 기여하는 기회를 가졌습니다.

오픈소스에 기여하고 싶다는 생각이 들던 찰나에, 리뉴얼되는 리액트 공식문서 번역 기여자를 모집한다는 글을 보고 바로 기여를 신청했습니다.

늦은 순번으로 도착해서인지 공부해보고싶은 파트의 번역은 이미 담당이 있더군요....

남은 것 중 createElement가 재미있을 것 같아 번역을 맡았습니다.

createElement가 리액트 코드를 작성할 때 중요하고 많이 사용하냐? 물어본다면 아닙니다. Legacy API로 공식문서에서 사용을 권장하지 않는 API입니다. 😭

그럼에도 불구하고 배울 점은 많았습니다. 😀

번역 작업을 진행하면서 다음을 내용을 배웠습니다.

  1. 리액트에 대한 이해가 좀 더 깊어졌습니다(나름..?)
  2. 기술번역이 쉬운게 아니구나!...

기술번역을 위해서는 그 기술에 대해 자세히 알아야한다고 생각하는데요. createElement에 대해서 공부하는 시간이 되었습니다.

그렇다면 createElement가 무엇인지 차근차근히 정리해보겠습니다.

목차

  • createElement 내부 코드 분석
  • React Element
  • React 17 이전 JSX Transform
  • createElement가 legacy API가 된 이유
  • React 17 이후 JSX Transform

createElement 내부 코드 분석

그러면 createElement() 내부 코드를 보면서 어떻게 동작하는 지 알아보겠습니다


// /react/src/ReactElement.js

export function createElement(type, config, children) {
  let propName;
  // .....
  

  props[propName] = config[propName];
  // props 세팅
  
  key = '' + config.key;
  // key 세팅
  
  props.children = children;
  // childrent 세팅
  
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

코드가 꽤 길지만 전체적인 구조를 보면서 차근차근 접근하면 이해할만 합니다(?)

  1. 함수의 인자로 받은 type, config, children을 가공해서 props, key, chdildren을 생성합니다.

  2. 그밖에도 key, ref, source, ReactCurrentOwner.current등을 만듭니다. (이 녀석들이 무엇을 하는 녀석인지는 바로 이해하긴 힘들지만요)

  3. createElement의 리턴값을 보면 ReactElement() 를 호출한 값을 리턴하고 있습니다.

내부 코드를 분석하니 createElement는 ReactElement를 반환하고 있습니다.

그렇다면 ReactElement의 코드를 보러 갈까요?


// react/src/ReactElement.js

function ReactElement(type, key, ref, self, source, owner, props) {
 const element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner,
  };
  
  if(__DEV__) {
    // 개발환경에서만 동작하는 코드 
    
  }
  
  return element;
}
  1. ReactElement() 함수는 인자를 받아 자바스크립트 객체 element를 생성하고 있습니다.
  2. 그리고 생성한 element 그대로 반환합니다.

createElement( )의 반환값은 자바스크립트 객체(React Element)라는 것을 알았습니다.

그렇다면 React Element를 생성하는 목적은 무엇이고 어디에 사용하는 걸까요?

React Element

사실 Element라는 개념은 새로운 것이 아닙니다. HTML에도 Element가 있습니다. 다르지 않아요!

// HTML
<h1 class="title">Hello!</h1>

- tag: "h1"
- textContent: "Hello!"
- class: "title"


vs 비교해보세요!

// React Element
React.createElement("h1", { className: "title" }, "Hello!");

- type: h1
- props: 
	className: "title"
	children: "Hello"

HTML Element와 React Element가 비슷해 보이지 않나요?

React Element는 React 내부에서 동작하는 Element이라고 볼 수 있을 것 같습니다!

브라우저가 HTML을 파싱해서 DOM을 생성하듯, 리액트는 React Element으로 DOM을 렌더링합니다.

React Element로 렌더링하는 DOM을 Virtual DOM이라고 이름을 지어볼까요? (공식문서에는 등장하지 않는 용어지만, 이해를 위해 허용해봅시다).

주의해야할 점은 React Element 자체만으로는 DOM을 생성하지는 않습니다. Element는 DOM을 생성하기 위한 재료라고 할까요? DOM을 생성하기 위해서는 react/dom 라이브러리가 필요합니다.

그러면 "Virtual DOM을 사용하는 이유는 무엇인가?"라는 질문을 해볼 수 있지만 이 포스트가 길어질 것 같으니 일단 패스하겠습니다...

정리하자면 다음과 같습니다.

  • createElement는 React Element를 생성합니다.
  • React Element는 DOM을 생성하기 위한 재료인 HTML Element와 비슷합니다
  • React Element 자체로는 DOM 요소를 생성하지 않습니다

하지만 실제로 개발자가 createElement를 직접 사용하는 일을 거의 없습니다.

왜냐하면 불편하거든요.

Element가 조금만 복잡해져도 사람이 작성할 수 없는 코드가 되어버립니다!

<div>
   <div>
     <div>
       <h1></h1>
     </div>
   </div>
 </div> 

// 위 Element를 표현하기 위해 이런 짓을...

function Title() {
  return  React.createElement("div", null, 
            React.createElement("div", null, 
              React.createElement("div", null, 
                React.createElement("h1", null)
         )
      )
   );
}

그래서 우리는 React Element를 쉽게 작성하기 위해JSX 문법을 사용합니다. JSX는 브라우저가 이해할 수 없는 문법으로, 컴파일 시점에 트랜스파일러에 의해 변환이 필요합니다.

React 17 이전 JSX Transform

우선 JSX를 반환하는 리액트 컴포넌트가 실제로 트랜스파일러에 의해 어떻게 변환되는지 살펴보겠습니다.

// Title.jsx
function Title(){
 return <h1 className="title">Hello!</h1> 
}

// Babel에 의해 변환된 코드
function Title() {
 return React.createElement("h1", {
    className: "title"
  }, "Hello!");
}

JSX 문법은 트랜스파일러에 의해 React Element를 반환하는 자바스크립트 코드로 변환됩니다.

JSX 문법은 개발자의 편의를 위해 제공되는 문법일뿐입니다. 브라우저는 JSX를 이해하지 못하기 때문에 변환이 필요합니다!

JSX문법은 Babel 또는 TypeScript와 같은 트랜스파일러에 의해서 자바스크립트로 변환됩니다.

그런데 변환된 코드에는 문제가 하나 있습니다.

바로 React.createElement 코드 때문에 미리 import React from 'react' 가 선언되어 있어야 동작한다는 점인데요.

  • 트랜스파일러가 자동으로 import 해주지 않습니다...
import React from ‘react’ // 이게 없으면 RefrenceError가 발생 
// React is not defined

function Title() {
 return React.createElement( );
}

그래서 React 17버전 이하에서는 반드시 React를 import해야만 제대로 동작을 합니다.

createElement가 legacy API가 된 이유 - 문제점

2019년, React 메인테이너 sebmarkbage가 기존 createElement의 문제점을 개선하는 RFC를 발행합니다.

간단히 RFC 내용을 정리하면 다음과 같습니다.

  • createElement는 레거시
  • 불필요하게 React를 import함 import React from 'react'
  • 클래스 컴포넌트를 사용했을 때 createElement는 의미가 있었지만 함수 컴포넌트 등장으로 의미를 잃어버렸음
  • createElement는 애초에 JSX를 염두해두고 만든 것이 아님
  • 개발자들이 createElement를 사용하여 수동으로 작성할 수 있도록 의도한 것인데 이제는 가치를 잃어버렸음
  • 성능이슈 (key를 props로 전달하는 방식)
  • 리액트 학습에 불필요한 개념을 포함하고 있음

이러한 문제때문에 함수 컴포넌트 + JSX 환경에 맞도록 createElement 개선이 시작되었습니다.

React 17 이후 JSX Transform

createElement의 새로운 이름 jsx

React 17부터 개선된 JSX Transform을 지원하기 시작합니다.

새로운 JSX Transform은 기존 JSX 코드와 완벽히 호환되기 때문에 React를 사용하는 개발자는 Babel 옵션만 변경을 해주면 사용할 수 습니다. (babel v 7.9.0 이상에서 사용 가능)

// babel.config.js
{
  "presets": [
    ["@babel/preset-react", {
      "runtime": "automatic" // 새로운 JSX Trasnform 사용
    }]
  ]
}
  • {"runtime": "classic"}를 설정하면 이전 JSX Transform을 사용합니다.
  • Babel 8부터는 {"runtime": "automatic"}가 기본값이 됩니다.

새로운 JSX Transform은 JSX를 반환하는 함수 컴포넌트를 다음과 같이 변환합니다.

function Title() {
  return <h1 className="title">Hello!</h1>;
}

// 변환
import { jsx as _jsx } from "react/jsx-runtime";

function Title() {
  return _jsx("h1", {
    className: "title",
    children: " Hello!"
  });
}

우선 다음 특징이 보입니다.

  • 트랜스파일러에 의해 자동으로 import { jsx as _jsx } from "react/jsx-runtime" 가 추가
  • children이 두번째 인자로 전달

이제 불필요한 import React from 'react' 를 할 필요는 없어보입니다! 🙌

그러면 "react/jsx-runtime"의 "jsx" 내부 코드를 구경해볼까요?

// react/src/jsx/ReactJSXElement.js


function ReactElement(type, key, ref, self, source, owner, props) {
  const element = {
    $$typeof: REACT_ELEMENT_TYPE,

    type,
    key,
    ref,
    props,

    _owner: owner,
  };
  
   if (__DEV__) {
     // 개발모드에서만 동작하는 코드... 
   }
  
  return element
}

export function jsx(type, config, maybeKey) {
  let propName;

  const props = {};

  let key = null;
  let ref = null;
  
  // ...
  // ...
  
  // key 설정
  key = '' + maybeKey;
  
  // deprecated될 코드. 점진적인 변화를 위해 존재
  key = '' + config.key;
  
  
  // props 설정
  props[propName] = config[propName];

  
  return ReactElement(
    type,
    key,
    ref,
    undefined,
    undefined,
    ReactCurrentOwner.current,
    props,
  );
}

createElement와 내부구현이 사실 거의 같아보이네요. 똑같이 ReactElement 호출값을 반환하고 있습니다.

다른 점이라면 children, maybeKey 에 대한 내용과 React Element를 호출할 때 self, source 인자를 undefined로 넘기는 것 정도가 있겠군요.

key를 props에서 분리하기

maybeKey라는 인자로 key를 props에서 분리하고 있습니다.

function jsx(type, config, maybeKey){
  
}

하지만 아직까지는 props에서 key를 허용하고 있는 모습이네요. 주석을 읽어보면 점진적으로 deprecated할 예정으로 보입니다.

// but as an intermediary step, we will use jsxDEV for everything except
key = config.key

항상 children을 props로 전달하기

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

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

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

RFC에서 말하듯 점진적으로 개선하면서 레거시를 제거할 예정으로 보입니다.

목표
1. 새로운 JSX Transform
2. 레거시 Deprecations 그리고 warnings
3. 레거시 제거

정리

이상으로 createElement에 대해 알아보았습니다. 간단해 보이는 함수임에도 리액트에 대해 많은 것을 배울 수 있었습니다.

정작 번역작업에 대한 회고는 하지 못한 것 같네요..! 다음 기회에...

profile
소프트웨어 엔지니어, 프론트엔드 개발자

0개의 댓글