Develop+

리액트 코어 - 리액트의 virtual dom 그리는 방식 파해쳐보기 본문

React

리액트 코어 - 리액트의 virtual dom 그리는 방식 파해쳐보기

Sunny Buddy 2022. 11. 7. 13:33
728x90

일단 바닐라js로 리액트 코어를 만들어보려고 한다.

컴포넌트의 ui를 만드는 JSX 는 js문법이 아니기 때문에 js로 바꿔야 하기 때문에 babel을 사용해 변환을 해줄 것이다.

 

시작하기 전 리액트 버츄어돔에 대한 이해

리엑트는 버츄얼돔을 가지고 있다.

버츄얼 돔을 만든다면 어떻게 만들 수 있을까???

리액트의 버츄어돔도 오픈소스이기 때문에 전부 공개되어있다.

버츄얼돔은 기본적으로 HTML태그를 변환시키는 구조로 되어있을 것이다.

 

그러면 html태그는 어떻게 생겼을까.

<div>태그가 있고 그 안에 속성 id등등이 있고, 태그는 열리고 닫히고 내부에는 자식 노드들이 있다. 

이걸 버츄어 돔으로 js친화적인 데이터로 만들면 어떨까? 

js 친화적긴 데이터란 객체이다. 

<div>
	<span>EXAMPLE</span>
</div>

이걸 객체로 바꾸면 어떨까

객체도 태그 구성과 유사하다

{
	tagName: 'div',
        props: {
            id: "root",
            className: "container",
        },
	children: [
            {
                tagName: 'span',
                    props: {},
                    children: 'EAAMPLE',
            }
	]
}

HTML을 객체로 변환해 생각해 보았을때 이런 구조를 가질 것이라고 생각하고 이제 react.js를 만들어보자.

 

react.js 생성

일단 우리는 react 패키지로 다운받아서 사용할 게 아니기 때문에 src폴더에 react.js를 만들어 

그 안에 react 메서드들을 구현시켜 볼 것이다. 

 

src/react.js

export function render() {

}

export function createElement() {

}

 

react에서 사용하는 render 메서드와 createElement 메서드를 구현된 코드는 없지만 메서드 바디만 작성해두었다. 

 

그리고 Title이라는 jsx를 리턴하는 펑셔널 컴포넌트를 만들어

우리가 만들었던 react.js를 import해 메서드를 불러서 사용하려고 한다.

import { createElement, render } from './react.js';

function Title() {
  return (
    <h2>정말 동작할까?</h2>
  )
};

render(<Title />, document.querySelector('#root'));

Title이라는 컴포넌트를 만들었고 react가 사용하듯이 Title을 render에 인자로 넣었고, 이 파일을 바벨로 변환해보았다.

 

build/index.js

import { createElement, render } from './react.js';
function Title() {
  return /*#__PURE__*/React.createElement("h2", null, "\uC815\uB9D0 \uB3D9\uC791\uD560\uAE4C?");
}
;
render( /*#__PURE__*/React.createElement(Title, null), document.querySelector('#root'));

빌드된 build/index.js를 열어보면 신기하게도 우리는 React 패키지를 사용한 적도 없는데  React.createElement를 사용해 타이틀이라는 반환 객체를 만들었다. 이 이유는 바벨에서 jsx를 변환할 때 내장되어있는 디폴트 값인 React 사용하기 때문이라고 한다

 

위코드의 createElement 메서드를 보면 

React.createElement("h2", null, "\uC815\uB9D0 \uB3D9\uC791\uD560\uAE4C?");
첫번째 인자에는 tagName, 두번째에는 props, 세번째에는 children이 들어가는 구조이다. 

이를 보고 바디만 작성했던 createElement를 작성해보자

 

아래의 코드는 리액트 코어를 담당하는 코드를 심플하게 구현한 코드이다.

export class Component {
  // 클래스 형식의 jsx인지 체크하기 위한 compoent 클래스 구조 정의
}

function renderRealDOM(vdom) {
 // 자식요소가 문자열이거나
  if(typeof vdom === 'string') {
    return document.createTextNode(vdom);
  }
  // 마지막 노드일 때 재귀를 탈출합니다. 
  if(vdom === undefined) return;

  // element객체를 만들어줍니다. 
  const $el = document.createElement(vdom.tagName);

  //vdom의 자식들을 돌며 만든 태그 안에 차곡차곡 append 해줍니다. 
  vdom.children.map(renderRealDOM).forEach(node => {
    $el.appendChild(node);
  });

  return $el;
}
// container에 map으로 전체 노드를 돌아 append된 vdom을 append해줍니다.
export const render = (function(){
  // 이전 상태를 저장하기 위해 클로저를 만들어줍니다. 
  let preVdom = null;

  return function (nextVdom, container) {
    if(preVdom === null) {
      preVdom = nextVdom;
    }
    container.appendChild(renderRealDOM(nextVdom))
  }
})();

export function createElement(tagName, props, ...children) {
  // 클래스와 function와 구분할 방법이 없기 때문에
  // 상속받은 function인지를 체크해줍니다.
  if(typeof tagName === 'function') {
    // 코드에서 Component를 상속받았는지를 체크합니다.
    if(tagName.prototype instanceof Component) {
      //클래스를 인스턴스화 한 후
      const instance = new tagName({...props, children});
      // 노드를 뱉는 render를 실행시켜 줍니다.
      return instance.render();
    }else {
      return tagName.apply(null, [props, ...children]);

    }
  }
  return {tagName, props, children}
}

 

 

2부에서 계속!

 

728x90