이번 과제는 '문제에 답이 있다' 와 같은 적절한 가이드를 통해 답을 유도하고 있어서, 1주차 보다는 그래도 빠르게 해결했습니다.
그래서 오늘은 이 깔끔한 테스트 코드를 기반으로 어떤 로직을 이해해야 할 지를 정리해보려고합니다.
#가상 DOM, #JSX 변환, #props, #children, #평탄화
createVNode 로 트랜스 파일링 하여 실제로는 객체 {type, props, children} 형태로 변환합니다. createVNode() 로 변환되는 방식은 빌드 설정으로 인해 동작합니다.// createVNode.js
export function createVNode(type, props, ...children) {
// 자식은 평탄화 해야한다.
children = children.flat(Infinity);
// 랜더링 하지 않아야 할 것들을 예외한다.
children = children.filter(
(c) => c !== false && c !== null && c !== undefined && c !== true,
);
return { type, props, children };
}
// any.tsx
/** @jsx createVNode */
import { createVNode } from "@/lib/createVNode";
const Any = () => {
return (
<div></div>
)
};
export default Any;
// vite.config.js
import { defineConfig } from "vite";
export default defineConfig({
esbuild: {
jsxFactory: "createVNode"
},
optimizeDeps: {
esbuildOptions: {
jsx: "transform",
jsxFactory: "createVNode",
},
}
/* ... */
});
#정규화, #컴포넌트 처리, #재귀 호출, #스칼라 값 처리
null,undefined,boolean 은 빈 문자열로 처리children 배열도 재귀적으로 정규화export function nomalizeVNode(vNode) {
// vNode로 예측되는 값
// null, undefined, true, false → 정규화 제외, 빈 문자열로 전달
// "string", 1 → 랜더링 스칼라 String
// function () {}, () => {} → 함수형 실행 후 props 전달
if (vNode === null || vNode === undefined || typeof vNode === "boolean") return "";
else if (typeof vNode === "number" || typeof vNode === "string") return String(vNode);
else if (typeof vNode.type === "function") {
const { type, props = {}, children = []} = vNode;
return nomalizeVNode(type({ ...props, children }));
}
// 일관된 형태로 정규화
const { type, props = null, children = []} = vNode;
return { type, props, children: children.map(nomalizeVNode).filter(Boolean) };
}
#DOM 생성, #DocumentFragment, #props 처리, #이벤트 바인딩
document.createElement(type) 로 실제 DOM을 생성합니다. DocumentFragment로 처리합니다.import { updateAttributes } from "../utils";
export function createElement(vNode) {
// null | undefined | Boolean → "" 빈 문자열 DOM
// string | number → String() 문자열 DOM
// Array → DocumentFragment 를 활용하여 하위 노드를 재귀적으로 생성
// { type, props, children } → HTML 시멘틱 태그 type으로 Element를 생성
// props로 속성을 업데이트 하되, on 이벤트 함수는 매니저에 할당
}
#이벤트 위임, #등록 및 해제, #버블링 처리(?), #map 구조
addEvent / removeEvent로 이벤트 핸들러 관리합니다.setupEventListeners를 통해 이벤트 종류별로 container에 한 번만 등록합니다.const eventTypes = [];
const elementMap = new Map();
export function setupEventListeners($root) {
eventTypes.forEach((eventType) => {
$root.addEventListener(eventType, handleEvent);
});
}
const handleEvent = (e) => {
const handlerMap = elementMap.get(e.target);
const handler = handlerMap?.get(e.type);
if (handler) handler.call(e.target, e);
};
// TODO 이벤트 버블링 커스텀 구현하기 >> onClick, onClickCapture
export function addEvent(element, eventType, handler) {
if (!eventTypes.includes(eventType)) eventTypes.push(eventType);
const handlerMap = elementMap.get(element) || new Map();
if (handlerMap.get(eventType) === handler) return;
handlerMap.set(eventType, handler);
elementMap.set(element, handlerMap);
}
export function removeEvent(element, eventType, handler) {
const handlerMap = elementMap.get(element);
if (!handlerMap) return;
if (handlerMap.get(eventType) === handler) handlerMap.delete(eventType);
if (handlerMap.size === 0) elementMap.delete(element);
else elementMap.set(element, handlerMap);
}
#렌더링, #재렌더링, #이벤트 재등록
createElement로 생성 후 append 합니다.updateElement 또는 교체합니다.setupEventListeners를 통해 이벤트 재등록합니다.import { createElement } from "./createElement";
import { setupEventListeners } from "./eventManager";
import { normalizeVNode } from "./normalizeVNode";
import { updateElement } from "./updateElement";
let oldNode = null;
export function renderElement(vNode, $container) {
// 최초 렌더링시에는 createElement로 DOM을 생성하고
// 이후에는 updateElement로 기존 DOM을 업데이트한다.
// 렌더링이 완료되면 container에 이벤트를 등록한다.
if (!vNode) throw new Error();
const newNode = normalizeVNode(vNode);
if (!$container.firstChild) {
$container.appendChild(createElement(newNode));
} else {
updateElement($container, newNode, oldNode);
}
oldNode = newNode;
setupEventListeners($container);
}
(/** @jsx createVNode */)
#pragma, #Babel 설정, #esbuild, #JSX 팩토리
React.createElement()로 변환되지만, 이를 바꾸기 위해 Vite나 Babel 설정에서 @jsx createVNode 주석을 선언하고, jsxFactory: createVNode로 설정해 JSX → createVNode(...)로 트랜스파일되게 만들 수 있습니다.falsy 값 필터링, 배열 map, children 평탄화
{false && <span>...>} 등의 조건부 표현은 false가 children으로 들어갈 수 있으므로 createVNode에서 필터링이 필요합니다.normalizeVNode에서 불필요한 값은 제거합니다.아직 완벽한 배포에 성공한 것은 아니지만, 그래도 저번주보단 더 나은 과제 진행이었습니다. 다음주도 어려울 것 같던데, 이번주 만큼은 하면 좋을 것 같습니다. 😊