본문 바로가기

Front/향해플러스

Virtual DOM 만들기 - #3. createElement (향해 플러스 2주차)

createElement

정제된 vNode를 이제 실제 브라우저 DOM으로 변환해야 한다. `createElement`는 가상 DOM 객체를 기반으로 진짜 DOM 노드를 생성하는 역할을 한다.


1. null, undefined, boolean 인 경우 빈 문자열이 담긴 텍스트 노드로 변환

// vNode가 undefined, null, boolean인 경우 빈 문자열이 담김 textNode를 반환한다.
if (vNode === undefined || vNode === null || typeof vNode === "boolean") {
  return document.createTextNode("");
}

 

예시

<div>{isShow && "Hello"}</div>
// isShow가 false면 false 가 들어오면서 빈 문자열이 담긴 텍스트 노드로 변환

2. 문자열, 숫자인 경우 텍스트 노드 반환

  // vNode가 문자열, 숫자인 경우 textNode를 반환한다.
  if (typeof vNode === "string" || typeof vNode === "number") {
    return document.createTextNode(vNode);
  }

 

예시

createElement("Hello") // "Hello"
createElement(42) // "42"

 


3. 배열인 경우 DocumentFragment 에 각 자식을 재귀적으로 추가

  // vNode가 배열인 경우 DocumentFragment를 생성해야 한다.
  if (Array.isArray(vNode)) {
    const fragment = document.createDocumentFragment();
    vNode.forEach((child) => {
      const childNode = createElement(child);
      fragment.appendChild(childNode);
    });
    return fragment;
  }

 

예시

createElement([
  "Hi",
  createVNode("span", null, "child"),
  42
])
// 결과
<>
	{Hi}
	<span>child</span>
	{42}
</>

 


4. vNode.type 이 문자열 태그이면 DOM 노드 생성

  // vNode가 태그(예: div, span 등)일 때
  if (typeof vNode.type === "string") {
    const $el = document.createElement(vNode.type);

    // props가 있을 경우, props를 vNode의 속성으로 추가한다.
    if (vNode.props) {
      updateAttributes($el, vNode);
    }

    // children이 있을 경우, children을 vNode의 자식으로 추가한다.
    if (vNode.children) {
      vNode.children.forEach((child) => {
        const childNode = createElement(child);
        $el.appendChild(childNode);
      });
    }
    return $el;
  }

 

예시

createElement(
	createVNode("button", { className: "btn", disabled: true }, "Click")
)
// 결과
<button class="btn" disabled>Click</button>
  • `document.createElement` 로 DOM 노드 생성
  • `updateAttributes` 로 속성 처리
  • 자식은 `createElement` 를 재귀 호출하여 DOM 노드로 변환

5. vNode.type 이 함수인 경우 예외 처리

// vNode가 함수형 컴포넌트일 경우
if (typeof vNode.type === "function") {
  throw new Error("함수형 컴포넌트는 createElement에서 직접 DOM으로 변환할 수 없습니다.");
}

`createElement` 에서는 직접 DOM을 변환할수 없고 `normalizeVNode` 에서 처리해야한다.


 

6. updateAttributes 로 속성(props) 차이를 계산하여 속성과 이벤트 핸들러를 갱신한다.

function updateAttributes($el, vNode) {
  Object.entries(vNode.props || {})
    .filter(([, value]) => value)
    .forEach(([attr, value]) => {
      // 이벤트 핸들러라면 addEvent로 addEventListener를 등록한다
      if (attr.startsWith("on") && typeof value === "function") {
        addEvent($el, attr.slice(2).toLowerCase(), value, vNode);
      } else {
        if (PROPERTY_ONLY_PROPS.has(attr)) {
          // checked, selected: property만 설정하고 DOM attribute는 항상 제거
          $el[attr] = !!value;
          $el.removeAttribute(attr);
        } else if (BOOLEAN_ATTRIBUTE_PROPS.has(attr)) {
          // disabled 등: property와 DOM attribute 모두 관리
          if (value) {
            $el[attr] = true;
            $el.setAttribute(attr, "");
          } else {
            $el[attr] = false;
            $el.removeAttribute(attr);
          }
        } else {
          // 일반 속성 처리
          const attribute = attr === "className" ? "class" : attr;
          $el.setAttribute(attribute, value);
        }
      }
    });
}

 

예시

// className
oldNode: <button>Click</button>
newNode: <button className="btn secondary">Click</button>

// checked
oldNode: <input type="checkbox" checked />
newNode: <input type="checkbox" />

// disabled
oldNode: <button disabled></button>
newNode: <button>Click</button>
// 실제 DOM

// className
<button class="btn secondary">Click</button> // className -> class 로 변경

// checked
<input type="checkbox" /> // checked 속성 제거

// disabled
<button>Click</button> // disabled 속성 제거

 

 

마무리

`createElement`는 정제된 vNode를 실제 DOM으로 변환하고, 속성과 이벤트까지 적용하는 단계다. 이 과정을 거친 결과물이 브라우저에 직접 그려지게 된다.

 

다음 글에서는 이 `createElement`와 함께 동작하는 `renderElement`로 최초 렌더링과 업데이트 흐름을 구현해보자!