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 Layout4 |"rootDir": "./src",5 |"outDir": "./dist",6 |"jsx": "react",7 |"module": "ESNext",8 |"moduleResolution": "Node",9 |"target": "ES6",10 |"strict": false11 |},12 |"include": ["src/**/*"],13 |"exclude": ["node_modules", "dist"]14 |}15 |
app.tsx 파일에 위의 코드블록과 같이
앞으로 app.tsx 파일에 변화가 생기면, tsc명령어로 다시 컴파일을 한뒤의 브라우져에서의 결과물을 확인할거고, 해당 설명은 생략하도록 하겠습니다.
console.log( ) 를 작성하고, 컴파일링후 브라우져 콘솔을 보니 아래와 같이 출력되는 것을 확인할 수 있었습니다. 아주 간단한 준비는 마친것 같습니다.앞으로 app.tsx 파일에 변화가 생기면, tsc명령어로 다시 컴파일을 한뒤의 브라우져에서의 결과물을 확인할거고, 해당 설명은 생략하도록 하겠습니다.

JSX 다루기
간단한 준비가 끝났고, 이제 app.tsx 파일에 진짜 JSX 문법을 사용해보겠습니다.app.tsx
1 |const App = <div>hello</div>;
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 함수를 제공하도록 하겠습니다.단순히 인자를
이제 위의 React 객체를 사용하도록아래와 같이 변환된 것을 확인할 수 있습니다.제가 작성한 React객체를 잘 사용하고 있는 것 같습니다. 브라우져 콘솔도 아래 사진과 같이 잘 나오고 있습니다! :)
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 |

- 저는 제가 작성한 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 |

사실은 이게 바로 가상돔 입니다 😅 저희가 가상돔에 대해 알고 있는 것처럼 해당 요소가 어떤 태그인지, 어떤 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으로 받는걸 볼수 있습니다. - 사실은 당연합니다. 함수형 컴포넌트이기 때문입니다.
- 저희가 위에서 확인한 가상돔 객체의 모양과 너무 다르네요. 어떻게 하면 좋을까요?

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/>)를 확인해보면, 최종적으로 아래와 같은 객체가 출력되는 것을 확인할 수 있습니다. 좀더 리액트의 가상돔 형태와 비슷해졌네요.
가상돔 객체 렌더링하기
- 이제 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/>을 이용해서 삽입 되어있습니다!