본문 바로가기

React.JS

react직접 만들어 보기(2) _ render 메서드 구현

우아한 형제들 김민태님의 강의를 듣고 알기 쉽게 정리한 콘텐츠 입니다.


이전 포스팅을 통해 vdom이 어떻게 만들어지는지 확인했다.

이번 포스팅에서는 만들어진 vdom을 어떻게 화면에 렌더링하는지에 대하여 살펴볼 것이다.

 

src/index.js

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

function Title (props) {
    return (
        <div>
          <span>depth1_1</span>
          <span>depth1_2</span>
          <span>depth1_3</span>
        <div>
          <span>depth2_1</span>
          <span>depth2_2</span>
          <span>depth2_3</span>
        </div>
        <div>
          <span>depth3_1</span>
          <span>depth3_2</span>
          <span>depth3_3</span>
        </div>
        </div>
    )
}

// 사용자의 컴포넌트는 함수자체를 넘겨준다.
render(<Title/>, document.querySelector('#root'));

우선 사용자 컴포넌트는 위와 같다. 직접 만든 render 메서드를 통해 root태그에 렌더링 시켜주려고 한다.

build/index.js

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

function Title(props) {
  return createElement("div", null, createElement("span", null, "depth1_1"), 
  createElement("span", null, "depth1_2"), 
  createElement("span", null, "depth1_3"), 
  createElement("div", null, createElement("span", null, "depth2_1"),
  createElement("span", null, "depth2_2"), 
  createElement("span", null, "depth2_3")), 
  createElement("div", null, createElement("span", null, "depth3_1"), 
  createElement("span", null, "depth3_2"), 
  createElement("span", null, "depth3_3")));
} // 사용자의 컴포넌트는 함수자체를 넘겨준다.


render(createElement(Title, null), document.querySelector('#root'));

바벨 트렌스파일링 후에 위와 같은 모습이 된다. 이 상태에서 렌더메서드를 직접 만들어 가상돔을 리얼돔으로 변환후에 렌더링 시켜볼 것이다.

 

src/react.js

function renderRealDOM(vdom) {
    const $el = document.createElement(vdom.tagName)
    return $el;
}

export function render(vdom, container) {
    container.appendChild(renderRealDOM(vdom))
}

export function createElement(tagName, props, ...children) {
    if (typeof tagName === 'function') {
        return tagName.apply(null, [props, ...children])
    }
    return {tagName, props, children}
}

이제 본격적으로 render메서드를 구현해 보자. render메서드는 vdom과 container를 인자로 받는다. 그 후 vdom을 realDom으로 바꾼후 container에 appendChild해주어 렌더링을 시킨다. 현재 renderRealDom메서드의 경우 tagName을 태그형태로 만들어 리턴해주고 있다. 여기서 문제점은 vdom의 children들이 태그로 만들어지지 못하고 있다는 것이다. 아래 이미지를 보면 실제로 div태그만 append된 것을 확인할 수 있다.

children이 반영되지 않아 div태그만 렌더링 되었다.

 

src/react.js (renderRealDOM 함수 수정 _ 재귀적으로 가상돔의 children을 순회)

function renderRealDOM(vdom) {
    if (typeof vdom === 'string') {
       return document.createTextNode(vdom);
    }

    if(vdom === undefined) return;

    const $el = document.createElement(vdom.tagName);

    let test = vdom.children.map(renderRealDOM).forEach((node, idx) => {
        console.log('node', node, 'idx', idx)
        $el.appendChild(node);
    });
    return $el;
}

export function render(vdom, container) {
    container.appendChild(renderRealDOM(vdom))
}

export function createElement(tagName, props, ...children) {
    if (typeof tagName === 'function') {
        return tagName.apply(null, [props, ...children])
    }
    return {tagName, props, children}
}

때문에 이를 해결해주기 위해 renderRealDOM함수의 수정이 필요하다. 우선 vdom의 childern재귀적으로 호출해야 한다. map메서드를 통해 자식레벨단의 깊이로 들어가고 forEach를 통해 같은 레벨의 노드들을 순회하며 사용자 컴포넌트($el)에 appendChild한다. 단, vdom이 undefinded 일 경우 자식이 없는 마지막 깊이 까지 들어온 것이기 때문에 리턴해주어 무한 루프를 막는다. 또한 vdom이 string일 경우 텍스트 노드로 만들어주며 리턴한다. 아래 이미지는 renderRealDOM에서 찍어놓은 실행 흐름이다. 재귀적으로 자식노드로 들어가는 흐름을 확인할 수 있다.

가상돔의 childern을 리얼돔으로 만들때 재귀적으로 순회하는 과정

 

src/react.js (render 함수 수정 _ 클로저를 통해 이전의 가삼돔 상태 보유)

function renderRealDOM(vdom) {
    if (typeof vdom === 'string') {
       return document.createTextNode(vdom);
    }

    if(vdom === undefined) return;

    const $el = document.createElement(vdom.tagName);

    let test = vdom.children.map(renderRealDOM).forEach((node, idx) => {
        console.log('node', node, 'idx', idx)
        $el.appendChild(node);
    });
    return $el;
}

export const render = (() => {
    let prevVdom = null;
    return function (nextVdom, container) {
        if (prevVdom === null) {
            prevVdom = nextVdom;
        }
        // diff~
        container.appendChild(renderRealDOM(nextVdom))
    }
})();


export function createElement(tagName, props, ...children) {
    if (typeof tagName === 'function') {
        return tagName.apply(null, [props, ...children])
    }
    return {tagName, props, children}
}

render함수에 이전의 가상돔과 현재의 가상돔을 비교하는 로직을 의사코드정도로 추가해 보겠다. 우선 함수는 상태를 가질 수 없기 때문에 클로저를 통해 이전의 가상돔을 기억할 수 있게끔 해야한다. 첫번째 렌더링일경우에 nextVdom을 그대로 prevVdom으로 넣어준다. diff로직은 복잡하기 때문에 render함수에서 클로저를 활용해서 이전의 가상돔과 현재의 가상돔을 비교한다는 정도만 이해하면 될 것 같다.

 

리엑트 render 함수 최종 실행 결과

가상돔이 리얼돔으로 변환되어 잘 렌더링 된것을 확인 할 수 있다.