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`로 최초 렌더링과 업데이트 흐름을 구현해보자!
'Front > 향해플러스' 카테고리의 다른 글
| Virtual DOM 만들기 - #5: updateElement (향해 플러스 2주차) (1) | 2025.08.11 |
|---|---|
| Virtual DOM 만들기 - #4: renderElement (향해 플러스 2주차) (1) | 2025.08.11 |
| Virtual DOM 만들기 - #2. normalizeVNode (향해 플러스 2주차) (0) | 2025.08.10 |
| Virtual DOM 만들기 - #1. createVNode (향해 플러스 2주차) (3) | 2025.08.02 |
| 프레임워크 없이 SAP 구현하기 - (향해 플러스 1주차) (4) | 2025.07.29 |