From e829435f7250fc3d2cedd94716ca281916834210 Mon Sep 17 00:00:00 2001 From: nopinokio Date: Tue, 24 Dec 2024 01:51:33 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20createVNode=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 136 ----------------------------------------- src/lib/createVNode.js | 17 +++++- src/lib/validCheck.js | 2 + 3 files changed, 18 insertions(+), 137 deletions(-) create mode 100644 src/lib/validCheck.js diff --git a/package-lock.json b/package-lock.json index 80ab35e..666feed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -303,19 +303,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", - "dev": true, - "peer": true, - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", @@ -1367,60 +1354,6 @@ "win32" ] }, - "node_modules/@testing-library/dom": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", - "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/dom/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "peer": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/@testing-library/dom/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "peer": true - }, "node_modules/@testing-library/jest-dom": { "version": "6.6.3", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", @@ -1454,13 +1387,6 @@ "@testing-library/dom": ">=7.21.4" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "peer": true - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2097,16 +2023,6 @@ "node": ">=0.4.0" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/dom-accessibility-api": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", @@ -3277,16 +3193,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "peer": true, - "bin": { - "lz-string": "bin/bin.js" - } - }, "node_modules/magic-string": { "version": "0.30.15", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.15.tgz", @@ -3803,34 +3709,6 @@ "node": ">=6.0.0" } }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3840,13 +3718,6 @@ "node": ">=6" } }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "peer": true - }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -3869,13 +3740,6 @@ "node": ">=8" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true, - "peer": true - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", diff --git a/src/lib/createVNode.js b/src/lib/createVNode.js index 9991337..922a2de 100644 --- a/src/lib/createVNode.js +++ b/src/lib/createVNode.js @@ -1,3 +1,18 @@ +/** + * ------> createVNode <------ + * 파라미터 설명 + * type >> (엘리먼트의 유형) + * props >> 속성 객체(예: { id: "myDiv", className: "container" }) + * children >> 엘리먼트가 포함하고 있는 자식 엘리먼트 + * flat(Infinity)는 배열을 모든 평평하게 만든다. + * filter로 유효하지 않은 값(null, undefined, boolean)을 제거하여 최종 렌더링 데이터를 클린하게 유지. + * + */ +import { isValidVnode } from "./validCheck"; export function createVNode(type, props, ...children) { - return {}; + return { + type, + props, + children: children.flat(Infinity).filter(isValidVnode), + }; } diff --git a/src/lib/validCheck.js b/src/lib/validCheck.js new file mode 100644 index 0000000..d5d5163 --- /dev/null +++ b/src/lib/validCheck.js @@ -0,0 +1,2 @@ +export const isValidVnode = (vNode) => + vNode !== null && typeof vNode !== "undefined" && typeof vNode !== "boolean"; From c849722efdfb21d497dae1e03ef9f89ccab3ebb6 Mon Sep 17 00:00:00 2001 From: nopinokio Date: Tue, 24 Dec 2024 02:15:14 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20normalizeVNode=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/normalizeVNode.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/lib/normalizeVNode.js b/src/lib/normalizeVNode.js index 7dc6f17..4e2df53 100644 --- a/src/lib/normalizeVNode.js +++ b/src/lib/normalizeVNode.js @@ -1,3 +1,27 @@ +import { isValidVnode } from "./validCheck"; + export function normalizeVNode(vNode) { - return vNode; + // null, undefined, falsy 의 경우 빈 텍스트 return + if (!isValidVnode(vNode)) { + return ""; + } + + // 문자열, 숫자일 경우 string 노드로 return + if (typeof vNode === "string" || typeof vNode === "number") { + return String(vNode); + } + + // 함수형태 처리 + if (typeof vNode.type === "function") { + const resolvedVNode = vNode.type(vNode.props || {}); + return normalizeVNode(resolvedVNode); + } + + return { + type: vNode.type, + props: vNode.props || {}, + children: Array.isArray(vNode.children) + ? vNode.children.filter(Boolean).map(normalizeVNode) + : [], + }; } From d86ff1a4360bbbbae4bc83edf45a5a22ce3d8593 Mon Sep 17 00:00:00 2001 From: nopinokio Date: Tue, 24 Dec 2024 02:16:43 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20isValidVNode=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/createVNode.js | 4 ++-- src/lib/normalizeVNode.js | 4 ++-- src/lib/validCheck.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/createVNode.js b/src/lib/createVNode.js index 922a2de..ac70219 100644 --- a/src/lib/createVNode.js +++ b/src/lib/createVNode.js @@ -8,11 +8,11 @@ * filter로 유효하지 않은 값(null, undefined, boolean)을 제거하여 최종 렌더링 데이터를 클린하게 유지. * */ -import { isValidVnode } from "./validCheck"; +import { isValidVNode } from "./validCheck"; export function createVNode(type, props, ...children) { return { type, props, - children: children.flat(Infinity).filter(isValidVnode), + children: children.flat(Infinity).filter(isValidVNode), }; } diff --git a/src/lib/normalizeVNode.js b/src/lib/normalizeVNode.js index 4e2df53..8e22786 100644 --- a/src/lib/normalizeVNode.js +++ b/src/lib/normalizeVNode.js @@ -1,8 +1,8 @@ -import { isValidVnode } from "./validCheck"; +import { isValidVNode } from "./validCheck"; export function normalizeVNode(vNode) { // null, undefined, falsy 의 경우 빈 텍스트 return - if (!isValidVnode(vNode)) { + if (!isValidVNode(vNode)) { return ""; } diff --git a/src/lib/validCheck.js b/src/lib/validCheck.js index d5d5163..67f1729 100644 --- a/src/lib/validCheck.js +++ b/src/lib/validCheck.js @@ -1,2 +1,2 @@ -export const isValidVnode = (vNode) => +export const isValidVNode = (vNode) => vNode !== null && typeof vNode !== "undefined" && typeof vNode !== "boolean"; From 54151ce5a1fa628df1afb9e5e55a499dba9bad3f Mon Sep 17 00:00:00 2001 From: nopinokio Date: Wed, 25 Dec 2024 21:51:21 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20createElement=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/createElement.js | 43 +++++++++++++++++++++++++++++++++++++-- src/lib/normalizeVNode.js | 28 ++++++++++++++----------- src/lib/renderElement.js | 4 ++++ src/lib/validCheck.js | 4 ++++ 4 files changed, 65 insertions(+), 14 deletions(-) diff --git a/src/lib/createElement.js b/src/lib/createElement.js index 5d39ae7..c85d752 100644 --- a/src/lib/createElement.js +++ b/src/lib/createElement.js @@ -1,5 +1,44 @@ +import { isValidVNode, isString, isNumber } from "./validCheck"; import { addEvent } from "./eventManager"; -export function createElement(vNode) {} +export function createElement(vNode) { + // null, undefined, boolean > 빈 텍스트 노드 + if (!isValidVNode(vNode)) { + return document.createTextNode(""); + } + // 텍스트와 숫자 > 텍스트 노드 + if (isString(vNode) || isNumber(vNode)) { + return document.createTextNode(vNode); + } -function updateAttributes($el, props) {} + if (Array.isArray(vNode)) { + const fragment = document.createDocumentFragment(); + vNode.forEach((child) => fragment.appendChild(createElement(child))); + return fragment; + } + + const $el = document.createElement(vNode.type); + + $el.append(...vNode.children.map(createElement)); + updateAttributes($el, vNode.props); + + return $el; +} + +/** + * DOM 요소에 속성 추가 + */ +function updateAttributes(element, props) { + Object.entries(props || {}) + //.filter(([attr, value]) => value) + .forEach(([attr, value]) => { + // className > class속성으로 변경 + if (attr === "className") { + element.setAttribute("class", value); + } else if (attr.startsWith("on")) { + addEvent(element, attr, value); + } else { + element.setAttribute(attr, value); + } + }); +} diff --git a/src/lib/normalizeVNode.js b/src/lib/normalizeVNode.js index 8e22786..7cdecae 100644 --- a/src/lib/normalizeVNode.js +++ b/src/lib/normalizeVNode.js @@ -1,4 +1,4 @@ -import { isValidVNode } from "./validCheck"; +import { isValidVNode, isString, isNumber } from "./validCheck"; export function normalizeVNode(vNode) { // null, undefined, falsy 의 경우 빈 텍스트 return @@ -7,21 +7,25 @@ export function normalizeVNode(vNode) { } // 문자열, 숫자일 경우 string 노드로 return - if (typeof vNode === "string" || typeof vNode === "number") { + if (isString(vNode) || isNumber(vNode)) { return String(vNode); } - // 함수형태 처리 + // 함수형 VNode 처리 if (typeof vNode.type === "function") { - const resolvedVNode = vNode.type(vNode.props || {}); - return normalizeVNode(resolvedVNode); + const processedVNode = vNode.type({ + ...vNode.props, + children: vNode.children, + }); + return normalizeVNode(processedVNode); } - return { - type: vNode.type, - props: vNode.props || {}, - children: Array.isArray(vNode.children) - ? vNode.children.filter(Boolean).map(normalizeVNode) - : [], - }; + // children(자식요소들) 필터링 + if (Array.isArray(vNode.children)) { + vNode.children = vNode.children + .map(normalizeVNode) // children을 재귀적으로 정규화 + .filter((child) => isValidVNode(child) && child !== ""); // 유효한 children을 필터링 + } + + return vNode; } diff --git a/src/lib/renderElement.js b/src/lib/renderElement.js index 0429572..8064eed 100644 --- a/src/lib/renderElement.js +++ b/src/lib/renderElement.js @@ -3,8 +3,12 @@ import { createElement } from "./createElement"; import { normalizeVNode } from "./normalizeVNode"; import { updateElement } from "./updateElement"; +let oldVNode = null; + export function renderElement(vNode, container) { // 최초 렌더링시에는 createElement로 DOM을 생성하고 // 이후에는 updateElement로 기존 DOM을 업데이트한다. // 렌더링이 완료되면 container에 이벤트를 등록한다. + oldVNode = normalizeVNode(vNode); + container.appendChild(createElement(oldVNode)); } diff --git a/src/lib/validCheck.js b/src/lib/validCheck.js index 67f1729..9f8d612 100644 --- a/src/lib/validCheck.js +++ b/src/lib/validCheck.js @@ -1,2 +1,6 @@ export const isValidVNode = (vNode) => vNode !== null && typeof vNode !== "undefined" && typeof vNode !== "boolean"; + +export const isString = (vNode) => typeof vNode === "string"; + +export const isNumber = (vNode) => typeof vNode === "number"; From 5c136eb3c0d90d10b9071c6cc39986735a2c1b3b Mon Sep 17 00:00:00 2001 From: nopinokio Date: Thu, 26 Dec 2024 00:26:53 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20addevent,=20removeEvent=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/createElement.js | 3 ++- src/lib/eventManager.js | 51 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/lib/createElement.js b/src/lib/createElement.js index c85d752..44442f2 100644 --- a/src/lib/createElement.js +++ b/src/lib/createElement.js @@ -36,7 +36,8 @@ function updateAttributes(element, props) { if (attr === "className") { element.setAttribute("class", value); } else if (attr.startsWith("on")) { - addEvent(element, attr, value); + const eventType = attr == "onClick" ? "click" : attr; + addEvent(element, eventType, value); } else { element.setAttribute(attr, value); } diff --git a/src/lib/eventManager.js b/src/lib/eventManager.js index 24e4240..8a8f78d 100644 --- a/src/lib/eventManager.js +++ b/src/lib/eventManager.js @@ -1,5 +1,50 @@ -export function setupEventListeners(root) {} +const eventRegistry = []; +let rootElement = null; -export function addEvent(element, eventType, handler) {} +function isEventMatch(eventA, eventB) { + return ( + eventA.element === eventB.element && + eventA.eventType === eventB.eventType && + eventA.handler === eventB.handler + ); +} -export function removeEvent(element, eventType, handler) {} +export function setupEventListeners(root) { + rootElement = root; + + // eventRegistry 배열에 있는 모든 이벤트를 루트 요소에 등록 + eventRegistry.forEach(({ eventType, handler }) => { + rootElement.addEventListener(eventType, handler); + }); +} + +export function addEvent(element, eventType, handler) { + let eventExists = false; + + for (const $event of eventRegistry) { + if (isEventMatch($event, { element, eventType, handler })) { + eventExists = true; + break; + } + } + + if (eventExists) return; + + // 이벤트 정보를 배열에 추가 + eventRegistry.push({ element, eventType, handler }); +} + +export function removeEvent(element, eventType, handler) { + let matchingEvent = null; + eventRegistry.forEach(($event) => { + if (isEventMatch($event, { element, eventType, handler })) { + matchingEvent = $event; + } + }); + + if (!matchingEvent) return; //값이 없으면 그대로 종료 + + if (matchingEvent !== null) { + rootElement.removeEventListener(eventType, matchingEvent.handler); + } +} From 29ec3bad63edf0aef41083683b0f3131e7327ca0 Mon Sep 17 00:00:00 2001 From: nopinokio Date: Fri, 27 Dec 2024 02:44:33 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20updateElement=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/createElement.js | 47 ++++++++++---------- src/lib/eventManager.js | 6 ++- src/lib/renderElement.js | 13 +++++- src/lib/updateElement.js | 92 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 130 insertions(+), 28 deletions(-) diff --git a/src/lib/createElement.js b/src/lib/createElement.js index 44442f2..eb56dfb 100644 --- a/src/lib/createElement.js +++ b/src/lib/createElement.js @@ -2,44 +2,45 @@ import { isValidVNode, isString, isNumber } from "./validCheck"; import { addEvent } from "./eventManager"; export function createElement(vNode) { - // null, undefined, boolean > 빈 텍스트 노드 if (!isValidVNode(vNode)) { return document.createTextNode(""); } - // 텍스트와 숫자 > 텍스트 노드 + if (isString(vNode) || isNumber(vNode)) { return document.createTextNode(vNode); } if (Array.isArray(vNode)) { - const fragment = document.createDocumentFragment(); - vNode.forEach((child) => fragment.appendChild(createElement(child))); - return fragment; + const $fragment = document.createDocumentFragment(); + vNode.forEach((child) => $fragment.appendChild(createElement(child))); + return $fragment; + } + + if (typeof vNode.type === "function") { + throw new Error("Function Components are not supported."); } - const $el = document.createElement(vNode.type); + const $element = document.createElement(vNode.type); + + updateAttributes($element, vNode.props ?? {}); - $el.append(...vNode.children.map(createElement)); - updateAttributes($el, vNode.props); + $element.append(...vNode.children.map(createElement)); - return $el; + return $element; } /** * DOM 요소에 속성 추가 */ -function updateAttributes(element, props) { - Object.entries(props || {}) - //.filter(([attr, value]) => value) - .forEach(([attr, value]) => { - // className > class속성으로 변경 - if (attr === "className") { - element.setAttribute("class", value); - } else if (attr.startsWith("on")) { - const eventType = attr == "onClick" ? "click" : attr; - addEvent(element, eventType, value); - } else { - element.setAttribute(attr, value); - } - }); +function updateAttributes($element, props) { + Object.entries(props).forEach(([attr, value]) => { + if (attr === "className") { + $element.setAttribute("class", value); + } else if (attr.startsWith("on") && typeof value === "function") { + const eventType = attr.toLowerCase().slice(2); + addEvent($element, eventType, value); + } else { + $element.setAttribute(attr, value); + } + }); } diff --git a/src/lib/eventManager.js b/src/lib/eventManager.js index 8a8f78d..b9c7ac2 100644 --- a/src/lib/eventManager.js +++ b/src/lib/eventManager.js @@ -42,7 +42,11 @@ export function removeEvent(element, eventType, handler) { } }); - if (!matchingEvent) return; //값이 없으면 그대로 종료 + if (!matchingEvent) return; + + // eventRegistry에서 matchingEvent를 제거 제거 + const index = eventRegistry.indexOf(matchingEvent); + eventRegistry.splice(index, 1); if (matchingEvent !== null) { rootElement.removeEventListener(eventType, matchingEvent.handler); diff --git a/src/lib/renderElement.js b/src/lib/renderElement.js index 8064eed..4699deb 100644 --- a/src/lib/renderElement.js +++ b/src/lib/renderElement.js @@ -9,6 +9,15 @@ export function renderElement(vNode, container) { // 최초 렌더링시에는 createElement로 DOM을 생성하고 // 이후에는 updateElement로 기존 DOM을 업데이트한다. // 렌더링이 완료되면 container에 이벤트를 등록한다. - oldVNode = normalizeVNode(vNode); - container.appendChild(createElement(oldVNode)); + + if (!container.firstChild) { + oldVNode = normalizeVNode(vNode); + container.appendChild(createElement(oldVNode)); + } else { + const newVNode = normalizeVNode(vNode); + updateElement(container, newVNode, oldVNode, 0); + oldVNode = newVNode; + } + + setupEventListeners(container); } diff --git a/src/lib/updateElement.js b/src/lib/updateElement.js index ac32186..d32bfda 100644 --- a/src/lib/updateElement.js +++ b/src/lib/updateElement.js @@ -1,6 +1,94 @@ import { addEvent, removeEvent } from "./eventManager"; import { createElement } from "./createElement.js"; -function updateAttributes(target, originNewProps, originOldProps) {} +/* + * popstate실행, 상태 변경 될 때마다 notify()를 호출, 실행하도록 createObserver의 subscribe()를 통해 등록이 돼있음 + * 따라서 render함수가 호출될 때마다 newNode와 oldNode를 비교 후 업데이트 해주어야 함 + * */ +export function updateElement(parentElement, newNode, oldNode, index = 0) { + // 노드 삭제 + if (!newNode && oldNode) { + parentElement.removeChild(parentElement.childNodes[index]); + return; + } -export function updateElement(parentElement, newNode, oldNode, index = 0) {} + // 노드 새로 추가 + if (newNode && !oldNode) { + parentElement.appendChild(createElement(newNode)); + return; + } + + // 노드 타입 변경 + if (newNode.type !== oldNode.type) { + return parentElement.replaceChild( + createElement(newNode), + parentElement.childNodes[index], + ); + } + + // 텍스트 노드 변경 + if (typeof newNode === "string" && typeof oldNode === "string") { + if (newNode === oldNode) return; + return parentElement.replaceChild( + createElement(newNode), + parentElement.childNodes[index], + ); + } + + updateAttributes( + parentElement.childNodes[index], + newNode.props || {}, + oldNode.props || {}, + ); + + // 재귀적으로 자식 노드 비교 + const maxLength = Math.max(newNode.children.length, oldNode.children.length); + for (let i = 0; i < maxLength; i++) { + updateElement( + parentElement.childNodes[index], + newNode.children[i], + oldNode.children[i], + i, + ); + } +} + +function updateAttributes(target, originNewProps, originOldProps = {}) { + // 이전 속성들 제거 + for (const attr in originOldProps) { + if (attr === "children") continue; + + // 이벤트 리스너인 경우 + if (attr.startsWith("on")) { + const eventType = attr.toLowerCase().substring(2); + removeEvent(target, eventType, originOldProps[attr]); + continue; + } + + // 새로운 props에 없는 경우 속성 제거 + if (!(attr in originNewProps)) { + target.removeAttribute(attr); + } + } + + // 새로운 속성 추가/업데이트 + for (const attr in originNewProps) { + if (attr === "children") continue; + + // 이벤트 리스너인 경우 + if (attr.startsWith("on")) { + const eventType = attr.toLowerCase().substring(2); + addEvent(target, eventType, originNewProps[attr]); + continue; + } + + // 값이 변경된 경우만 업데이트 + if (originOldProps[attr] !== originNewProps[attr]) { + if (attr === "className") { + target.setAttribute("class", originNewProps[attr]); + } else { + target.setAttribute(attr, originNewProps[attr]); + } + } + } +}