Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 79 additions & 2 deletions src/lib/createElement.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,82 @@
import { addEvent } from "./eventManager";

export function createElement(vNode) {}
export function createElement(vNode) {
// null, undefined, boolean 값 처리
if (vNode === null || vNode === undefined || vNode === true || vNode === false) {
return document.createTextNode("");
}

function updateAttributes($el, props) {}
// 문자열과 숫자 처리
if (typeof vNode === "string" || typeof vNode === "number") {
return document.createTextNode(String(vNode));
}

// 배열 처리 - DocumentFragment 생성
if (Array.isArray(vNode)) {
const fragment = document.createDocumentFragment();
vNode.forEach((child) => {
const childElement = createElement(child);
if (childElement) {
fragment.appendChild(childElement);
}
});
return fragment;
}

// vNode 객체가 아닌 경우
if (!vNode || typeof vNode !== "object" || !vNode.type) {
return document.createTextNode("");
}

// 함수형 컴포넌트 처리 - 오류 발생
if (typeof vNode.type === "function") {
throw new Error("Cannot create element from component");
}

// 일반 엘리먼트 처리
const element = document.createElement(vNode.type);

// 속성 처리
if (vNode.props) {
updateAttributes(element, vNode.props);
}

// 자식 요소 처리
if (vNode.children) {
vNode.children.forEach((child) => {
const childElement = createElement(child);
if (childElement) {
element.appendChild(childElement);
}
});
}

return element;
}

function updateAttributes($el, props) {
Object.entries(props).forEach(([key, value]) => {
if (key === "children") return; // children은 별도 처리

if (key.startsWith("on") && typeof value === "function") {
// 이벤트 핸들러는 DOM 속성으로 추가하지 않고 별도로 관리
const eventName = key.toLowerCase().substring(2);
addEvent($el, eventName, value);
} else if (key === "className") {
// className 처리
$el.className = value;
} else if (key.startsWith("data-")) {
// data 속성 처리
const dataKey = key.substring(5);
$el.dataset[dataKey] = value;
} else if (typeof value === "boolean") {
// 불리언 속성 처리
if (value) {
$el.setAttribute(key, "");
}
} else {
// 일반 속성 처리
$el.setAttribute(key, value);
}
});
}
10 changes: 9 additions & 1 deletion src/lib/createVNode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
export function createVNode(type, props, ...children) {
return {};
return {
type,
props,
children: flattenChildren(children),
};
}

function flattenChildren(children) {
return children.flat(Infinity).filter((value) => value === 0 || Boolean(value));
}
66 changes: 63 additions & 3 deletions src/lib/eventManager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,65 @@
export function setupEventListeners(root) {}
// 이벤트 핸들러를 저장할 Map
const eventHandlers = new Map();

export function addEvent(element, eventType, handler) {}
export function setupEventListeners(root) {
// 이벤트 위임을 위한 이벤트 리스너 설정
const eventTypes = ["click", "mouseover", "focus", "keydown", "input", "change", "submit"];

export function removeEvent(element, eventType, handler) {}
eventTypes.forEach((eventType) => {
root.addEventListener(
eventType,
(event) => {
// 이벤트가 발생한 요소에서 핸들러 찾기
let target = event.target;

while (target && target !== root) {
const handlers = eventHandlers.get(target);
if (handlers && handlers[eventType]) {
// 핸들러 실행
handlers[eventType].forEach((handler) => {
handler(event);
});
// 이벤트가 처리되었으므로 더 이상 전파하지 않음
return;
}
target = target.parentNode;
}
},
false,
); // 버블링 단계에서 이벤트 처리
});
}

export function addEvent(element, eventType, handler) {
if (!eventHandlers.has(element)) {
eventHandlers.set(element, {});
}

const elementHandlers = eventHandlers.get(element);
if (!elementHandlers[eventType]) {
elementHandlers[eventType] = [];
}

elementHandlers[eventType].push(handler);
}

export function removeEvent(element, eventType, handler) {
const elementHandlers = eventHandlers.get(element);
if (elementHandlers && elementHandlers[eventType]) {
const handlers = elementHandlers[eventType];
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}

// 핸들러가 없으면 해당 이벤트 타입 제거
if (handlers.length === 0) {
delete elementHandlers[eventType];
}

// 요소에 핸들러가 없으면 Map에서 제거
if (Object.keys(elementHandlers).length === 0) {
eventHandlers.delete(element);
}
}
}
48 changes: 47 additions & 1 deletion src/lib/normalizeVNode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,49 @@
export function normalizeVNode(vNode) {
return vNode;
// null, undefined, boolean 값 처리
if (vNode === null || vNode === undefined || vNode === true || vNode === false) {
return "";
}

// 문자열과 숫자 처리
if (typeof vNode === "string" || typeof vNode === "number") {
return String(vNode);
}

// 배열 처리 - 평탄화
if (Array.isArray(vNode)) {
const flattened = vNode.flat(Infinity);
const normalizedChildren = flattened.map((child) => normalizeVNode(child)).filter((child) => child !== "");
if (normalizedChildren.length === 0) return "";
if (normalizedChildren.length === 1) return normalizedChildren[0];
return normalizedChildren;
}

// vNode 객체가 아닌 경우
if (!vNode || typeof vNode !== "object" || !vNode.type) {
return "";
}

// 함수형 컴포넌트 처리
if (typeof vNode.type === "function") {
const component = vNode.type;
const props = { ...(vNode.props || {}), children: vNode.children };
const result = component(props);
// 컴포넌트 실행 결과를 재귀적으로 정규화
return normalizeVNode(result);
}
// 여기서 children이 배열이 아닐 수도 있으니 분기!
let normalizedChildren = [];
if (vNode.children !== undefined && vNode.children !== null) {
if (Array.isArray(vNode.children)) {
normalizedChildren = vNode.children.map((child) => normalizeVNode(child)).filter((child) => child !== "");
} else {
const norm = normalizeVNode(vNode.children);
if (norm !== "") normalizedChildren = [norm];
}
}
return {
type: vNode.type,
props: vNode.props,
children: normalizedChildren,
};
}
31 changes: 24 additions & 7 deletions src/lib/renderElement.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import { setupEventListeners } from "./eventManager";
import { createElement } from "./createElement";
import { normalizeVNode } from "./normalizeVNode";
import { updateElement } from "./updateElement";
import { createElement } from "./createElement.js";
import { normalizeVNode } from "./normalizeVNode.js";
import { setupEventListeners } from "./eventManager.js";
import { updateElement } from "./updateElement.js";

export function renderElement(vNode, container) {
// 최초 렌더링시에는 createElement로 DOM을 생성하고
// 이후에는 updateElement로 기존 DOM을 업데이트한다.
// 렌더링이 완료되면 container에 이벤트를 등록한다.
// vNode를 정규화
const normalizedVNode = normalizeVNode(vNode);

if (container._vNode) {
// 이전 가상 DOM과 비교하여 업데이트
updateElement(container, normalizedVNode, container._vNode);
} else {
// 최초 렌더링 시 DOM 생성
const element = createElement(normalizedVNode);
container.appendChild(element);
}

// 최신 vNode를 저장
container._vNode = normalizedVNode;

// 이벤트 리스너는 한 번만 설정
if (!container._eventListenersSetup) {
setupEventListeners(container);
container._eventListenersSetup = true;
}
}
Loading
Loading