diff --git a/404.html b/404.html
new file mode 100644
index 00000000..d43ffde2
--- /dev/null
+++ b/404.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+ 상품 쇼핑몰
+
+
+
+
+
+
+
+
+
diff --git a/index.html b/index.html
index d43ffde2..94dead1b 100644
--- a/index.html
+++ b/index.html
@@ -1,26 +1,26 @@
-
-
-
- 상품 쇼핑몰
-
-
-
-
-
-
-
-
+
+
+
+ 상품 쇼핑몰
+
+
+
+
+
+
+
+
diff --git a/src/lib/createElement.js b/src/lib/createElement.js
index 5d39ae7d..9088e365 100644
--- a/src/lib/createElement.js
+++ b/src/lib/createElement.js
@@ -1,5 +1,40 @@
import { addEvent } from "./eventManager";
-export function createElement(vNode) {}
+export function createElement(vNode) {
+ if (vNode === null || vNode === undefined || typeof vNode === "boolean") {
+ return document.createTextNode("");
+ }
-function updateAttributes($el, props) {}
+ if (typeof vNode === "string" || typeof vNode === "number") {
+ return document.createTextNode(String(vNode));
+ }
+
+ if (Array.isArray(vNode)) {
+ const fragment = document.createDocumentFragment();
+
+ vNode.forEach((node) => fragment.appendChild(createElement(node)));
+
+ return fragment;
+ }
+
+ const el = document.createElement(vNode.type);
+ updateAttributes(el, vNode.props ?? {});
+
+ el.append(...vNode.children.map(createElement));
+
+ return el;
+}
+
+function updateAttributes($el, props) {
+ Object.entries(props).forEach(([attribute, value]) => {
+ if (/^on[A-Z]/.test(attribute) && typeof value === "function") {
+ addEvent($el, attribute.toLowerCase().substring(2), value);
+ } else if (attribute === "className") {
+ $el.setAttribute("class", value);
+ } else if (typeof value === "boolean") {
+ $el[attribute] = value;
+ } else {
+ $el.setAttribute(attribute, value);
+ }
+ });
+}
diff --git a/src/lib/createVNode.js b/src/lib/createVNode.js
index 9991337f..05e7a6ff 100644
--- a/src/lib/createVNode.js
+++ b/src/lib/createVNode.js
@@ -1,3 +1,9 @@
export function createVNode(type, props, ...children) {
- return {};
+ const flatChildren = children.flat(Infinity).filter((child) => child === 0 || Boolean(child));
+
+ return {
+ type,
+ props,
+ children: flatChildren,
+ };
}
diff --git a/src/lib/eventManager.js b/src/lib/eventManager.js
index 24e4240f..c4c4f820 100644
--- a/src/lib/eventManager.js
+++ b/src/lib/eventManager.js
@@ -1,5 +1,41 @@
-export function setupEventListeners(root) {}
+const eventMap = new WeakMap();
+const delegatedEvents = new Set();
-export function addEvent(element, eventType, handler) {}
+export function setupEventListeners(root) {
+ delegatedEvents.forEach((eventType) => {
+ root.removeEventListener(eventType, handleDelegatedEvent);
+ root.addEventListener(eventType, handleDelegatedEvent);
+ });
+}
-export function removeEvent(element, eventType, handler) {}
+export function addEvent(element, eventType, handler) {
+ if (!eventMap.has(element)) {
+ eventMap.set(element, new Map());
+ }
+
+ const elementEvents = eventMap.get(element);
+ elementEvents.set(eventType, handler);
+
+ delegatedEvents.add(eventType);
+}
+
+export function removeEvent(element, eventType) {
+ const elementEvents = eventMap.get(element);
+ elementEvents.delete(eventType);
+}
+
+function handleDelegatedEvent(event) {
+ let target = event.target;
+
+ // NOTE: cancelBubble 속성은 deprecated이므로 다른 방법으로 구현해야 함
+ while (target && !event.cancelBubble) {
+ const elementEvents = eventMap.get(target);
+
+ if (elementEvents?.has(event.type)) {
+ const handler = elementEvents.get(event.type);
+ handler(event);
+ }
+
+ target = target.parentNode;
+ }
+}
diff --git a/src/lib/normalizeVNode.js b/src/lib/normalizeVNode.js
index 7dc6f175..add04d36 100644
--- a/src/lib/normalizeVNode.js
+++ b/src/lib/normalizeVNode.js
@@ -1,3 +1,18 @@
export function normalizeVNode(vNode) {
- return vNode;
+ if (vNode === null || vNode === undefined || typeof vNode === "boolean") {
+ return "";
+ }
+
+ if (typeof vNode === "string" || typeof vNode === "number") {
+ return String(vNode);
+ }
+
+ if (typeof vNode.type === "function") {
+ return normalizeVNode(vNode.type({ ...vNode.props, children: vNode.children }));
+ }
+
+ return {
+ ...vNode,
+ children: vNode.children.map(normalizeVNode).filter(Boolean),
+ };
}
diff --git a/src/lib/renderElement.js b/src/lib/renderElement.js
index 04295728..2980e26e 100644
--- a/src/lib/renderElement.js
+++ b/src/lib/renderElement.js
@@ -3,8 +3,24 @@ import { createElement } from "./createElement";
import { normalizeVNode } from "./normalizeVNode";
import { updateElement } from "./updateElement";
+const vNodeMap = new WeakMap();
+
export function renderElement(vNode, container) {
// 최초 렌더링시에는 createElement로 DOM을 생성하고
// 이후에는 updateElement로 기존 DOM을 업데이트한다.
// 렌더링이 완료되면 container에 이벤트를 등록한다.
+
+ const oldVNode = vNodeMap.get(container);
+ const newVnode = normalizeVNode(vNode);
+
+ if (!oldVNode) {
+ const element = createElement(newVnode);
+ container.appendChild(element);
+ } else {
+ updateElement(container, newVnode, oldVNode);
+ }
+
+ vNodeMap.set(container, newVnode);
+
+ setupEventListeners(container);
}
diff --git a/src/lib/updateElement.js b/src/lib/updateElement.js
index ac321861..4acd503a 100644
--- a/src/lib/updateElement.js
+++ b/src/lib/updateElement.js
@@ -1,6 +1,113 @@
import { addEvent, removeEvent } from "./eventManager";
import { createElement } from "./createElement.js";
-function updateAttributes(target, originNewProps, originOldProps) {}
+export function updateAttributes(target, originNewProps, originOldProps) {
+ if (!originNewProps && !originOldProps) return;
-export function updateElement(parentElement, newNode, oldNode, index = 0) {}
+ if (originOldProps) {
+ Object.entries(originOldProps).forEach(([key, value]) => {
+ if (key === "children") return;
+
+ if (key.startsWith("on")) {
+ const eventType = key.substring(2).toLowerCase();
+ removeEvent(target, eventType, originOldProps[key]);
+ } else if (!originNewProps || !(key in originNewProps)) {
+ if (key === "className") {
+ target.removeAttribute("class");
+ } else if (typeof value === "boolean") {
+ target[key] = false;
+ target.removeAttribute(key);
+ } else {
+ target.removeAttribute(key);
+ }
+ }
+ });
+ }
+
+ if (originNewProps) {
+ Object.entries(originNewProps).forEach(([key, value]) => {
+ if (key === "children") return;
+
+ if (key === "className") {
+ if (value) {
+ target.setAttribute("class", value);
+ } else {
+ target.removeAttribute("class");
+ }
+ return;
+ }
+
+ if (key.startsWith("on")) {
+ const eventType = key.substring(2).toLowerCase();
+ addEvent(target, eventType, value);
+ return;
+ }
+
+ if (typeof value === "boolean") {
+ target[key] = value;
+ return;
+ }
+
+ if (value != null && (!originOldProps || originOldProps[key] !== value)) {
+ target.setAttribute(key, String(value));
+ }
+ });
+ }
+}
+
+export function updateElement(parentElement, newNode, oldNode, index = 0) {
+ if (!newNode && oldNode) {
+ if (parentElement.childNodes[index]) {
+ parentElement.removeChild(parentElement.childNodes[index]);
+ }
+ return;
+ }
+
+ if (newNode && !oldNode) {
+ parentElement.appendChild(createElement(newNode));
+ return;
+ }
+
+ if (typeof newNode === "string" || typeof newNode === "number") {
+ if (newNode !== oldNode) {
+ const newTextNode = document.createTextNode(String(newNode));
+ if (parentElement.childNodes[index]) {
+ parentElement.replaceChild(newTextNode, parentElement.childNodes[index]);
+ } else {
+ parentElement.appendChild(newTextNode);
+ }
+ }
+ return;
+ }
+
+ if (newNode.type !== oldNode.type) {
+ if (parentElement.childNodes[index]) {
+ parentElement.replaceChild(createElement(newNode), parentElement.childNodes[index]);
+ } else {
+ parentElement.appendChild(createElement(newNode));
+ }
+ return;
+ }
+
+ const childNode = parentElement.childNodes[index];
+ if (childNode) {
+ updateAttributes(childNode, newNode.props || {}, oldNode.props || {});
+
+ const newChildren = newNode.children || [];
+ const oldChildren = oldNode.children || [];
+ const maxLength = Math.max(newChildren.length, oldChildren.length);
+
+ for (let i = 0; i < maxLength; i++) {
+ updateElement(childNode, newChildren[i], oldChildren[i], i);
+ }
+
+ if (oldChildren.length > newChildren.length) {
+ for (let i = oldChildren.length - 1; i >= newChildren.length; i--) {
+ const extraChild = childNode.childNodes[i];
+ if (extraChild) {
+ childNode.removeChild(extraChild);
+ }
+ }
+ }
+ }
+}