리액트 이해하기] 01. JSX와 함께 React.createElement 구현하기

Song-Minhyung·2023년 11월 1일

리액트 이해하기

목록 보기
1/8
post-thumbnail

시작하기 앞서

이번 미션동안 리액트의 작동 과정을 이해하고 이를 코드로 옮겨 구현 해보려 했습니다. 실제로 코드로 동작하는걸 보며 리액트가 어떻게 동작하고 라이프 사이클이 어떻게 돌아가는지 이해할 수 있는 좋은 계기가 되었습니다.

그런데 실제로 리액트의 코드는 차이가 존재합니다.
제가 작성한 코드는 학습하기 위해 작성한 코드임을 미리 알립니다!

전체 코드는 깃허브 에서 확인 해보실 수 있습니다.

목차

JSX

리액트의 코드를 보면 거의 HTML 코드를 작성하는것과 흡사합니다.
만약 그러지 않으면 코드의 가독성이 매우 떨어지기 때문입니다.
그런데 말 그대로 흡사 HTML 실제로는 이를 JSX라 부릅니다.

JSX에 대한 자세한 내용은 이곳 공식문서를 확인해주세요.

그리고 JSX는 트랜스파일링 되어 아래와 같은 코드로 변환됩니다.

//JSX
function App() {
  return <div> Hello, JSX! </div>;
}

// After Transpiling
function App() {
  return /*#__PURE__*/_core_React__WEBPACK_IMPORTED_MODULE_0__["default"].createElement("div", null, "Hello, JSX!!");
}

그럼 변환 후에 createElement 라는 메소드는 뭘 해줄까요?

React.createElement?

React.createElement는 가상돔을 만들어주는 메소드 라고 이해햐면 될것같습니다.
그래서 JSX를 사용하지 않고 해당 함수만으로 코드를 작성할 수 있습니다.
하지만 그러면 가독성이 너무 떨어지기에 JSX를 사용하고 트랜스파일링된 코드를 사용합니다.

vite + typescript + jsx설정

실제로 사용해볼까요? 우선 vite를 사용할 것이기에 설치해줍니다.

> npm create vite@latest

그러고서 typescript를 사용할 것 이기에 추가 설정이 필요합니다.

우선 tsconfig.json파일을 수정해주세요

// tsconfig.json
{
  "compilerOptions": {
    ...
    
    "paths": {
      "@/*": ["./*"],
      "@core/*": ["./lib/React"],
    },
    "jsx": "react",
  },
  "include": ["src", "types"]
}

paths는 절대 경로를 위한 설정이고
include에 types 폴더를 넣어서 전역적으로 선언한 타입을 알려줍니다.

그리고 /types/jsx.d.ts 파일을 아래처럼 만듭니다.

//jsx.d.ts
declare module JSX {
  type Element = string;
  interface IntrinsicElements {
    [elemName: string]: any;
  }
}

다음으로 vite.config.js 파일을 수정 해줍니다.

jsxFactory을 넣어줄건데 이 값이 jsx를 트랜스파일링 해주는 함수가 됩니다.
그리고 tsconfig.json의 alias를 사용하기 위해 vite-tsconfig-paths 라이브러리를 추가로 설치해주세요.

// vite.config.js
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [tsconfigPaths()],
  esbuild: {
    jsxFactory: "React.createElement",
  },
});

createElement 함수 작성

저는 /lib/React 폴더를 만들고 여기서 createElement 함수를 작성 해줬습니다.
이부분은 원하는 방식의 폴더 아래에 넣어주면 됩니다.

React.createElement 함수는 위에서 봤듯이
createElement(tagType, props, children)을 인자로 받습니다.
tagType은 string이 될거고, props는 Object, children은 배열이 될것입니다.

이에 맞춰 타입을 정의해 함수를 만들어 줍니다.

// createElement.ts
interface VirtualDomNode {
  tag: string;
  props?: any;
  children: VirtualDomNode[];
}

export default function createElement(
  tag: string,
  props: { [key: string]: unknown },
  ...children: VirtualDomNode[]
): VirtualDomNode {
  return {
    tag,
    props,
    children,
  };
}

마지막으로 이렇게 만든 createElement 함수가 제대로 작동하는지 테스트 해봅니다.
JSX로 작성한 코드는 React.createElement로 변환되므로 JSX로 작성 해보면 됩니다.

그 전에 index.html 파일에 script파일을 불러올 수 있도록 추가 해줍니다.
안그러면 이렇게 만든 스크립트의 실행이 되지 않기 때문입니다.

<!--index.html-->
<!DOCTYPE html>
<html lang="ko">
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

createElement 테스트 해보기

그리고 main.tsx 파일은 아래처럼 만들어서 실행 해봅니다.

// /src/main.tsx
import React from "@/lib/React";

console.log(<div>Hello, JSX!</div>);

이제 개발자 도구를 확인 해보면 아래와 같은 결과가 나옵니다!
성공적으로 작성했네요

{
    "tag": "div",
    "props": null,
    "children": [
        "Hello, JSX!"
    ]
}

한번 props와 children도 넣어볼까요?

// /src/main.tsx
import React from "@/lib/React";

console.log(
  <div check={true}>
    <div>first child</div>
    <div>first child</div>
  </div>
);

처음에 의도했던대로 tag, props, children이 제대로 호출되며 들어갑니다.

{
    "tag": "div",
    "props": {
        "check": true
    },
    "children": [
        {
            "tag": "div",
            "props": null,
            "children": [
                "first child"
            ]
        },
        {
            "tag": "div",
            "props": null,
            "children": [
                "first child"
            ]
        }
    ]
}

npm run build를 실행해 실제로 변환된 코드를 보면 아래와 같이 변환된걸 알 수 있습니다.

console.log(
  c.createElement(
    "div",
    { check: !0 },
    c.createElement("div", null, "first child"),
    c.createElement("div", null, "first child")
  )
);

Component도 넣어보기

그런데 여기에 Component를 넣으면 어떻게 될까요?
아래에 <App/> 컴포넌트를 넣어보도록 하겠습니다.

// src/App.tsx
import React from "@core/React";

function App() {
  return <div>Hello, JSX!</div>;
}

export default App;
// src/main/tsx
import React from "@/lib/React";
import App from "./App";

console.log(
  <div check={true}>
    <div>first child</div>
    <App />
  </div>
);

1번째 children에 Hello, JSX! 가 들어갈거라 예상했는데 아니었습니다.
왜일까요?

createElement 함수를 다시 살펴보면 tag, props, children을 그대로 리턴 해주고 있습니다. 근데 만약 여기에 컴포넌트 즉 App 함수가 들어가면 tag에 함수가 들어가게 될겁니다.이 부분을 수정하고 같은 코드를 다시 실행해봅니다.

interface VirtualDomNode {
  tag: string | Function;
  props?: any;
  ref?: HTMLElement | unknown | null;
  children: VirtualDomNode[];
}

export default function createElement(
  tag: string | Function,
  props: { [key: string]: unknown },
  ...children: VirtualDomNode[]
): VirtualDomNode {
  if (typeof tag === "function") {
    return tag({ ...props, children });
  }

  return {
    tag,
    props,
    children: children,
  };
}
{
    "tag": "div",
    "props": {
        "check": true
    },
    "children": [
        {
            "tag": "div",
            "props": null,
            "children": [
                "first child"
            ]
        },
        {
            "tag": "div",
            "props": null,
            "children": [
                "Hello, JSX!"
            ]
        }
    ]
}

처음에 의도했던대로 children이 들어가고 있습니다 성공입니다!
다음 글에서는 이렇게 만든 가상돔을 실제로 돔에 렌더링 시켜보겠습니다.

profile
기록하는 블로그

0개의 댓글