LAB

React.createElement


JSX 다루기 준비하기

  • JSX파일이 브라우져에서 동작하기 위해서는 바벨등의 컴파일러로 JSX를 JS파일로 변환하는 과정이 필요합니다.
  • 저는 이프로젝트에서 간단한 TSX를 사용할 것이기 때문에, 타입스크립트 컴파일러로 JSX를 자바스크립트로 변환하여 진행해보겠습니다.
  • 변환될 app.tsx파일 / 컴파일링 결과물인 app.js 파일을 script하는 index.html / tsconfig.json, 먼저 이렇게 세가지 파일이 필요합니다.
app.tsx
1 | console.log("Hello from createElement app.tsx");
index.html
1 | ...
2 | <script type="module" src="../dist/app.js"></script>
3 | <main id="app"></main>
4 |
tsconfig.json
1 | {
2 | "compilerOptions": {
3 | // File Layout
4 | "rootDir": "./src",
5 | "outDir": "./dist",
6 | "jsx": "react",
7 | "module": "ESNext",
8 | "moduleResolution": "Node",
9 | "target": "ES6",
10 | "strict": false
11 | },
12 | "include": ["src/**/*"],
13 | "exclude": ["node_modules", "dist"]
14 | }
15 |
app.tsx 파일에 위의 코드블록과 같이 console.log( ) 를 작성하고, 컴파일링후 브라우져 콘솔을 보니 아래와 같이 출력되는 것을 확인할 수 있었습니다. 아주 간단한 준비는 마친것 같습니다.
앞으로 app.tsx 파일에 변화가 생기면, tsc명령어로 다시 컴파일을 한뒤의 브라우져에서의 결과물을 확인할거고, 해당 설명은 생략하도록 하겠습니다.
small-react-image1

JSX 다루기

간단한 준비가 끝났고, 이제 app.tsx 파일에 진짜 JSX 문법을 사용해보겠습니다.
app.tsx
1 | const App = <div>hello</div>;
일단 위와 같이 냅다 jsx 문법을 사용후 tsc로 컴파일을 해보면, app.js 파일이 타입스크립트 컴파일러에 의해 아래와같이 변환된 것을 확인할 수 있습니다.
app.js
1 | var App = React.createElement("div", null, "hello");
  • typescript 컴파일러는 tsconfig.json의 설정이jsx:'react' 로 되어있을때는 JSX 문법을 React.createElement 호출로 변환하는것이 기본 동작입니다. -React.createElement
  • 그래서 타입스크립트 컴파일러가 app.js와 같은 결과물을 만들어내게 되었습니다.
  • 그러나 제 예제는 순수 JS로 리액트를 흉내내며 이해하는 것이 목표이기 때문에, 진짜 React객체를 사용하는 것은 의미가 없습니다.
따라서, 제가 직접 React 객체를 생성하고, 해당 객체에서 createElement 함수를 제공하도록 하겠습니다.
core/react.js
1 | const React = {
2 | createElement: (type, props, children) => {
3 | console.log("createElement called with:", type, props, children);
4 | return {
5 | type,
6 | props,
7 | children,
8 | };
9 | },
10 | };
11 |
12 | export default React;
13 |
14 |
단순히 인자를console.log()하고 return 하는 createElement를 만들었는데요
이제 위의 React 객체를 사용하도록 app.tsx에서 import 하고, 다른 JSX 구조를 작성해보겠습니다.
app.tsx
1 | import React from "../core/react.js";
2 |
3 | const App = (
4 | <div>
5 | <h2>안녕하세요!</h2>
6 | <p>저는 grimza99 입니다.</p>
7 | <input type="text" />
8 | </div>
9 | );
10 | export default App;
11 |
아래와 같이 변환된 것을 확인할 수 있습니다.
app.js
1 | import React from "../core/react.js";
2 |
3 | const App = (React.createElement("div", null,
4 | React.createElement("h2", null, "안녕하세요 React!"),
5 | React.createElement("p", null, "저는 grimza99 입니다."),
6 | React.createElement("input", { type: "text" })));
7 |
8 | export default App;
9 |
제가 작성한 React객체를 잘 사용하고 있는 것 같습니다. 브라우져 콘솔도 아래 사진과 같이 잘 나오고 있습니다! :)
image1
  • 저는 제가 작성한 createElement에 단지 호출된 인자들을 객체로 묶어서 반환하는 역할만 수행하도록 작성했습니다.
  • 그런데 app.js파일을 보면 중첩된 createElement 호출이 계층구조를 형성하고 있는 것을 볼수 있고, 브라우져에서는 자식요소를console.log()한뒤 부모 요소를console.log() 하는 것을 확인할 수 있습니다.
  • JSX 문법은 결국 createElement 호출의 중첩 호출로 변환된다는 것을 알 수 있습니다!

실은 나야, 가상돔

위에서는 createElement를 중첩해서 호출하는 것을 확인할 수 있었습니다. 이를 이용하면 좀더 멋지게 계층구조를 표현할 수 있지 않을까요?
core/react.js
1 | const React = {
2 | createElement: (tag, props, ...children) => {
3 | const el = {
4 | tag,
5 | props,
6 | children,
7 | };
8 | console.log(el);
9 | return el;
10 | },
11 | };
12 |
위와 같이 createElement 함수를 작성하면, JSX문법으로 작성된 계층구조가 전역객체의 트리구조로 변환되는 것을 확인할 수 있습니다.
image1
사실은 이게 바로 가상돔 입니다 😅 저희가 가상돔에 대해 알고 있는 것처럼 해당 요소가 어떤 태그인지, 어떤 props를 가지고 있는지, 또 어떤 children을 가지고 있는지를 트리구조로 표현되어 있습니다.
  • 그런데 사실 app.tsx 파일에 작성된 문법은 우리가 평소에 작성하는 함수형 컴포넌트와는 조금 다릅니다.
  • 좀더 익숙한 함수형 컴포넌트의 모습으로 바꿔서 작성해보겠습니다.
app.tsx
1 | ...
2 | const App = () => {
3 | return (
4 | <div>
5 | <h2>안녕하세요!</h2>
6 | <p>저는 grimza99 입니다.</p>
7 | <input type="text" />
8 | </div>
9 | );
10 | };
11 |
12 | console.log(<App />);
13 |
  • 컴파일후 브라우져에서 console.log(<App/>)을 확인해보면 당연하게도 tag를 function으로 받는걸 볼수 있습니다.
  • 사실은 당연합니다. 함수형 컴포넌트이기 때문입니다.
  • 저희가 위에서 확인한 가상돔 객체의 모양과 너무 다르네요. 어떻게 하면 좋을까요?
image1
createElement에서 tag가 함수일경우, 즉 함수형 컴포넌트일경우 해당 함수를 호출하도록 수정해보겠습니다.
core/react.js
1 | const React = {
2 | createElement: (tag, props, ...children) => {
3 | if (typeof tag === "function") {
4 | return tag(props, ...children);
5 | }
6 | const el = {
7 | tag,
8 | props,
9 | children,
10 | };
11 | return el;
12 | },
13 | };
14 |
이제 다시 브라우져에서 console.log(<App/>)를 확인해보면, 최종적으로 아래와 같은 객체가 출력되는 것을 확인할 수 있습니다. 좀더 리액트의 가상돔 형태와 비슷해졌네요.
image1

가상돔 객체 렌더링하기

  • 이제 createElement로 만들어진 가상돔 객체를 실제로 렌더링 해보겠습니다.
  • 우리가 왕왕 사용하는 템플릿 생성 명령어로 리액트 프로젝트를 만들었을 때를 떠올려보면, 엔트리 포인트가 되는 main.js 안에서 container가 될 부분의 id를 받고, <App />컴포넌트를 렌더링 하던 것을 기억하시나요?
  • 이때 render함수라는 것을 main.js에서 볼수 있는데, 이걸 한번 흉내내보겠습니다.
  • 아마도 render함수는 JSX요소를 받고, 화면을 그릴 컨테이너의 id를 받는 함수일것 같습니다.
  • 그뒤 JSX요소는 createElement로 변환될것이고, 변환된 가상돔 객체를 실제 DOM으로 변환하여 컨테이너에 추가하는 일을 할것으로 예상됩니다.
core/render.js
1 | export const render = (el, container) => {
  • 인자로 받는 el에는 tag 라는 속성이 있으니document.createElement로 해당 태그를 생성할수 있고, 자식요소가 있을경우에는 자식 요소를 재귀호출하면 될 것 같습니다.
  • 자세한 내용은 아래 주석과 함께 코드를 남겨놓았습니다
core/render.js
1 | const render = function (el, container) {
2 | let domEl;
3 |
4 | // 1. el의 유형을 확인한다.
5 | if (typeof el === "string" || typeof el === "number") {
6 | // 문자열인 경우 텍스트 노드처럼 처리해야 함.
7 | domEl = document.createTextNode(String(el));
8 | container.appendChild(domEl); // 텍스트에 대한 자식이 없으므로 반환
9 | return;
10 | }
11 |
12 | // 2. 먼저 el에 해당하는 문서 노드를 만든다.
13 | domEl = document.createElement(el.tag);
14 |
15 | // 3. domEl에 props를 설정한다.
16 | let elProps = el.props ? Object.keys(el.props) : null;
17 | if (elProps && elProps.length > 0) {
18 | for (const key in el.props) {
19 | domEl[key] = el.props[key];
20 | }
21 | }
22 |
23 | // 4. 자식을 만든다.
24 | if (el.children && el.children.length > 0) {
25 | // child가 렌더링되면 컨테이너는 여기서 생성한 domEl이 된다.
26 | el.children.forEach(function (node) {
27 | return render(node, domEl);
28 | });
29 | }
30 |
31 | // 5. DOM 노드를 컨테이너에 추가한다.
32 | container.appendChild(domEl);
33 | };
app.tsx
1 | const App = () => {
2 | return (
3 | <div>
4 | <h2>안녕하세요!</h2>
5 | <p>저는 grimza99 입니다.</p>
6 | <input type="text" />
7 | </div>
8 | );
9 | };
10 | render(<App />, document.getElementById("app"));
11 |

아래는 위의 과정을 거쳐 완성된 index.html 파일입니다!
<iframe/>을 이용해서 삽입 되어있습니다!