From 37ac0317cdd9a209f6b4babd9575ff8ebde69c2f Mon Sep 17 00:00:00 2001 From: JUNMAN CHOI Date: Sat, 21 Dec 2024 16:36:53 +0900 Subject: [PATCH 01/25] Create README.md --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f80994 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +[8팀 최준만] Chapter 1-2. 프레임워크 없이 SPA 만들기 Part 2 From 46c2252770374a2d4f714beae60b34bef1be5058 Mon Sep 17 00:00:00 2001 From: JunmanChoi Date: Mon, 23 Dec 2024 21:00:56 +0900 Subject: [PATCH 02/25] =?UTF-8?q?feat:=20createVNode=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20*=20=EC=9E=AC=EA=B7=80=EC=A0=81=20?= =?UTF-8?q?=ED=8F=89=ED=83=84=ED=99=94=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EA=B5=AC=ED=98=84=20*=20=ED=97=AC=ED=8D=BC=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=EB=A1=9C=200=EC=9D=84=20=EC=A0=9C=EC=99=B8?= =?UTF-8?q?=20=ED=95=9C=20=EA=B0=92=EC=9D=80=20falsy=20=EC=B2=B4=ED=81=AC?= =?UTF-8?q?=EB=A5=BC=20=ED=86=B5=ED=95=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/chapter1-2/custom.test.jsx | 26 ++++++++++++++++++++++++ src/lib/createVNode.js | 12 ++++++++++- src/utils/arrayUtils.js | 17 ++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/chapter1-2/custom.test.jsx create mode 100644 src/utils/arrayUtils.js diff --git a/src/__tests__/chapter1-2/custom.test.jsx b/src/__tests__/chapter1-2/custom.test.jsx new file mode 100644 index 0000000..412757a --- /dev/null +++ b/src/__tests__/chapter1-2/custom.test.jsx @@ -0,0 +1,26 @@ +/** + * 개인적으로 만든 순수함수들에 대한 테스트 입니다. + */ + +import { recursiveFlatten } from "../../utils/arrayUtils"; + +describe("유틸 함수 체크", () => { + describe("recursiveFlatten", () => { + it("올바른 구조의 평탄화된 배열을 생성해야 한다", () => { + const vNode = recursiveFlatten([ + "Hello", + ["world", ["i", "am", ["junman"]], "!"], + ]); + expect(vNode).toEqual(["Hello", "world", "i", "am", "junman", "!"]); + }); + + it("falsy한 값은 포함되지 않아야 한다.", () => { + const vNode = recursiveFlatten( + ["Hello", [null, [undefined], "world", "!"]], + (value) => Boolean(value), + ); + + expect(vNode).toEqual(["Hello", "world", "!"]); + }); + }); +}); diff --git a/src/lib/createVNode.js b/src/lib/createVNode.js index 9991337..2b4c054 100644 --- a/src/lib/createVNode.js +++ b/src/lib/createVNode.js @@ -1,3 +1,13 @@ +import { recursiveFlatten } from "../utils/arrayUtils"; + +const checkNullishExceptZero = (value) => { + // 0은 falsy한 값이 아니고 숫자로 처리할 것이다. + if (value === 0) return true; + return Boolean(value); +}; + export function createVNode(type, props, ...children) { - return {}; + const flattenedChildren = recursiveFlatten(children, checkNullishExceptZero); + + return { type, props, children: flattenedChildren }; } diff --git a/src/utils/arrayUtils.js b/src/utils/arrayUtils.js new file mode 100644 index 0000000..09ea556 --- /dev/null +++ b/src/utils/arrayUtils.js @@ -0,0 +1,17 @@ +/** + * + * @param {Array} arr - 평탄화 할 배열 + * @param {Function} helper - 평탄화 시킬 때 필터링할 함수 + * @returns + */ +export function recursiveFlatten(arr, helper = () => true) { + return arr.reduce((acc, cur) => { + if (Array.isArray(cur)) { + const flattenedChild = recursiveFlatten(cur, helper); + acc = acc.concat(flattenedChild); + } else { + if (helper(cur)) acc.push(cur); + } + return acc; + }, []); +} From ba6557bb5f143c16300bf2eec7331560616cc0b5 Mon Sep 17 00:00:00 2001 From: JunmanChoi Date: Tue, 24 Dec 2024 01:22:01 +0900 Subject: [PATCH 03/25] =?UTF-8?q?feat:=20normalize=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?*=200=EC=9D=84=20=EC=A0=9C=EC=99=B8=ED=95=9C=20falsy=EA=B0=92?= =?UTF-8?q?=20=EC=B0=BE=EB=8A=94=20=ED=95=A8=EC=88=98=20=EC=9C=A0=ED=8B=B8?= =?UTF-8?q?=ED=99=94=20*=20=EC=A0=95=EA=B7=9C=ED=99=94=20=EC=A2=85?= =?UTF-8?q?=EB=A3=8C=20=EB=8B=A8=EA=B3=84=EC=97=90=EC=84=9C=20props=20?= =?UTF-8?q?=ED=95=A9=EC=B3=90=EC=A3=BC=EA=B8=B0=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84(=EC=9D=B4=EB=B6=80=EB=B6=84=20=EC=A1=B0?= =?UTF-8?q?=EC=96=B8=20=ED=95=84=EC=9A=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/createVNode.js | 11 +++----- src/lib/normalizeVNode.js | 56 +++++++++++++++++++++++++++++++++++++-- src/utils/commonUtils.js | 7 +++++ 3 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 src/utils/commonUtils.js diff --git a/src/lib/createVNode.js b/src/lib/createVNode.js index 2b4c054..6d6e4b2 100644 --- a/src/lib/createVNode.js +++ b/src/lib/createVNode.js @@ -1,13 +1,10 @@ import { recursiveFlatten } from "../utils/arrayUtils"; - -const checkNullishExceptZero = (value) => { - // 0은 falsy한 값이 아니고 숫자로 처리할 것이다. - if (value === 0) return true; - return Boolean(value); -}; +import { checkNullishExceptZero } from "../utils/commonUtils"; export function createVNode(type, props, ...children) { - const flattenedChildren = recursiveFlatten(children, checkNullishExceptZero); + const flattenedChildren = recursiveFlatten(children, (val) => { + return checkNullishExceptZero(val); + }); return { type, props, children: flattenedChildren }; } diff --git a/src/lib/normalizeVNode.js b/src/lib/normalizeVNode.js index 7dc6f17..165ca4e 100644 --- a/src/lib/normalizeVNode.js +++ b/src/lib/normalizeVNode.js @@ -1,3 +1,55 @@ -export function normalizeVNode(vNode) { - return vNode; +import { checkNullishExceptZero } from "../utils/commonUtils"; + +// falsy 값은 빈 문자열로 변환 +// 숫자, 문자 = > 문자열로 변환 +// 함수 => 함수 실행 결과로 변환 + +// vNode : {type , props, children} +export function normalizeVNode(vNode, depth = 0) { + if (typeof vNode === "boolean" || !checkNullishExceptZero(vNode)) { + return ""; + } + + if (typeof vNode === "number" || typeof vNode === "string") { + return String(vNode); + } + + if (typeof vNode === "object") { + if (typeof vNode.type === "function") { + return normalizeVNode( + vNode.type({ props: vNode.props, children: vNode.children }), + depth + 1, + ); + } + + return { + type: vNode.type, + props: vNode.props ? combineProps(vNode.props) : null, + children: vNode.children + .map((child) => { + // 자식 노드 정규화 + return normalizeVNode(child, depth + 1); + }) + .filter((child) => { + // 0을 제외한 falsy 값 제거 + return checkNullishExceptZero(child); + }), + }; + } } + +// 최하단까지(type이 string) 내려가서 props를 합치는 함수 +// 이런식으로 하는게 아닌 것 같은데,, 뭔가 잘못된 것 같은데,,, +const combineProps = (props) => { + const nestedProps = props?.props; + delete props.props; + let res = { ...props }; + if (!nestedProps) { + return res; + } + Object.entries(nestedProps).forEach(([key, value]) => { + res[key] = `${res[key] ?? ""}${value ?? ""}`; + res[key] = res[key].trim(); + }); + return res; +}; diff --git a/src/utils/commonUtils.js b/src/utils/commonUtils.js new file mode 100644 index 0000000..622dace --- /dev/null +++ b/src/utils/commonUtils.js @@ -0,0 +1,7 @@ +// 도메인을 정의할 수 없는 유틸들 모음 + +export function checkNullishExceptZero(value) { + // 0은 falsy한 값이 아니고 숫자로 처리할 것이다. + if (value === 0) return true; + return Boolean(value); +} From 17df31561429075081cc368f41ff1cacd26a8dfa Mon Sep 17 00:00:00 2001 From: JunmanChoi Date: Tue, 24 Dec 2024 01:53:52 +0900 Subject: [PATCH 04/25] =?UTF-8?q?fix:=20=EC=A2=85=EB=8B=A8=EC=97=90?= =?UTF-8?q?=EC=84=9C=20props=20=EA=B0=95=EC=A0=9C=20=ED=95=A9=EC=84=B1?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=BD=94=EB=93=9C=EA=B0=80=20=EC=95=84?= =?UTF-8?q?=EB=8B=8C=20props=20=EC=8A=A4=ED=94=84=EB=A0=88=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=ED=86=B5=ED=95=9C=20=EC=A0=84=EB=8B=AC=EB=A1=9C=20?= =?UTF-8?q?=EC=9E=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/normalizeVNode.js | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/src/lib/normalizeVNode.js b/src/lib/normalizeVNode.js index 165ca4e..3b83dca 100644 --- a/src/lib/normalizeVNode.js +++ b/src/lib/normalizeVNode.js @@ -5,7 +5,7 @@ import { checkNullishExceptZero } from "../utils/commonUtils"; // 함수 => 함수 실행 결과로 변환 // vNode : {type , props, children} -export function normalizeVNode(vNode, depth = 0) { +export function normalizeVNode(vNode) { if (typeof vNode === "boolean" || !checkNullishExceptZero(vNode)) { return ""; } @@ -17,18 +17,17 @@ export function normalizeVNode(vNode, depth = 0) { if (typeof vNode === "object") { if (typeof vNode.type === "function") { return normalizeVNode( - vNode.type({ props: vNode.props, children: vNode.children }), - depth + 1, + vNode.type({ ...vNode.props, children: vNode.children }), ); } return { type: vNode.type, - props: vNode.props ? combineProps(vNode.props) : null, + props: vNode.props, children: vNode.children .map((child) => { // 자식 노드 정규화 - return normalizeVNode(child, depth + 1); + return normalizeVNode(child); }) .filter((child) => { // 0을 제외한 falsy 값 제거 @@ -37,19 +36,3 @@ export function normalizeVNode(vNode, depth = 0) { }; } } - -// 최하단까지(type이 string) 내려가서 props를 합치는 함수 -// 이런식으로 하는게 아닌 것 같은데,, 뭔가 잘못된 것 같은데,,, -const combineProps = (props) => { - const nestedProps = props?.props; - delete props.props; - let res = { ...props }; - if (!nestedProps) { - return res; - } - Object.entries(nestedProps).forEach(([key, value]) => { - res[key] = `${res[key] ?? ""}${value ?? ""}`; - res[key] = res[key].trim(); - }); - return res; -}; From e398fa5fd83af03917920f918de538252990fcc5 Mon Sep 17 00:00:00 2001 From: JunmanChoi Date: Tue, 24 Dec 2024 03:49:04 +0900 Subject: [PATCH 05/25] =?UTF-8?q?feat:=20createElement=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20*=20input=20vNode=EA=B0=80=20=EB=B0=B0=EC=97=B4?= =?UTF-8?q?=EC=9D=BC=20=EA=B2=BD=EC=9A=B0,=20=EC=9E=AC=EA=B7=80=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20*=20=EC=8A=A4=ED=8A=B8=EB=A7=81,=20falsy?= =?UTF-8?q?=EB=93=B1=20=EC=B2=98=EB=A6=AC=20=EB=81=9D=EB=82=98=EB=A9=B4=20?= =?UTF-8?q?dom=20=EC=9A=94=EC=86=8C=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/createElement.js | 63 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/src/lib/createElement.js b/src/lib/createElement.js index 5d39ae7..0d949af 100644 --- a/src/lib/createElement.js +++ b/src/lib/createElement.js @@ -1,5 +1,62 @@ -import { addEvent } from "./eventManager"; +import { checkNullishExceptZero } from "../utils/commonUtils"; +// import { addEvent } from "./eventManager"; -export function createElement(vNode) {} +export function createElement(vNode) { + if (typeof vNode === "function" || typeof vNode?.type === "function") { + throw new Error("NEED NORMALIZE"); + } + if (typeof vNode === "boolean" || !checkNullishExceptZero(vNode)) { + return document.createTextNode(""); + } -function updateAttributes($el, props) {} + // 문자열 또는 숫자인 경우 텍스트 노드로 처리 + if (typeof vNode === "string" || typeof vNode === "number") { + return document.createTextNode(vNode); + } + + // 배열로 들어온 경우 fragment로 처리 + if (Array.isArray(vNode)) { + const frag = document.createDocumentFragment(); + vNode.forEach((node) => { + frag.append(makeElement(node)); + }); + return frag; + } + + // 그 외의 경우 element로 처리 + const el = makeElement(vNode); + + if (Array.isArray(vNode.children)) { + vNode.children.forEach((child) => { + el.appendChild(createElement(child)); + }); + } + + return el; +} + +function makeElement(vNode) { + const el = document.createElement(vNode.type); + updateAttributes(el, vNode.props); + return el; +} + +/** + * + * @param {HTMLElement} $el + * @param {object} props + */ +function updateAttributes($el, props) { + for (const prop in props) { + $el.setAttribute(replaceIfPropIsClass(prop), props[prop]); + } +} + +/** + * @description prop이 className인 경우 class로 변경 + * @param {object} prop + * @returns {string} + */ +function replaceIfPropIsClass(prop) { + return prop === "className" ? "class" : prop; +} From 1c6fc3ab3f34bb9399601a17c817e314c482731f Mon Sep 17 00:00:00 2001 From: JunmanChoi Date: Tue, 24 Dec 2024 22:58:29 +0900 Subject: [PATCH 06/25] =?UTF-8?q?feat:=20eventManager=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20*=20createElement=EC=8B=9C=20on~=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=ED=95=98=EB=8A=94=20prop=EC=9D=98=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20addEvent=EC=B2=98=EB=A6=AC=20*=20WeakMap?= =?UTF-8?q?=20=EC=9E=90=EB=A3=8C=ED=98=95=EC=9D=84=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=EB=93=B1=EB=A1=9D=EB=90=A0=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/createElement.js | 5 +++++ src/lib/eventManager.js | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/lib/createElement.js b/src/lib/createElement.js index 0d949af..07fec30 100644 --- a/src/lib/createElement.js +++ b/src/lib/createElement.js @@ -1,4 +1,5 @@ import { checkNullishExceptZero } from "../utils/commonUtils"; +import { addEvent } from "./eventManager"; // import { addEvent } from "./eventManager"; export function createElement(vNode) { @@ -48,6 +49,10 @@ function makeElement(vNode) { */ function updateAttributes($el, props) { for (const prop in props) { + if (prop?.startsWith("on")) { + addEvent($el, prop.slice(2).toLowerCase(), props[prop]); + continue; + } $el.setAttribute(replaceIfPropIsClass(prop), props[prop]); } } diff --git a/src/lib/eventManager.js b/src/lib/eventManager.js index 24e4240..0ba84b4 100644 --- a/src/lib/eventManager.js +++ b/src/lib/eventManager.js @@ -1,5 +1,37 @@ -export function setupEventListeners(root) {} +/** + * globalEvents + * { + * click: { + * element: handler + * } + * } + */ -export function addEvent(element, eventType, handler) {} +const globalEvents = {}; -export function removeEvent(element, eventType, handler) {} +export function setupEventListeners(root) { + Object.entries(globalEvents).forEach(([eventType, eventsMap]) => { + root.addEventListener(eventType, (e) => handleGlobalEvents(e, eventsMap)); + }); +} + +export function handleGlobalEvents(e, eventsMap) { + if (eventsMap.has(e.target)) { + eventsMap.get(e.target)(e); + } +} + +export function addEvent(element, eventType, handler) { + if (!element || typeof handler !== "function") return; + + globalEvents[eventType] = globalEvents[eventType] || new WeakMap(); + globalEvents[eventType].set(element, handler); +} + +export function removeEvent(element, eventType, handler) { + if (globalEvents[eventType].get(element) === handler) { + globalEvents[eventType].delete(element); + } +} + +window.__myEventListeners = globalEvents; From 51a666e9df68676343065bbeba4df4219c0f4cb9 Mon Sep 17 00:00:00 2001 From: JunmanChoi Date: Wed, 25 Dec 2024 02:13:26 +0900 Subject: [PATCH 07/25] =?UTF-8?q?feat:=20renderElement=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20*=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=EC=9D=BC?= =?UTF-8?q?=20=EC=88=98=20=EB=8F=84=20=EC=9E=88=EC=9C=BC=EB=8B=88,=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=A0=84=EB=B6=80=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=ED=95=98=EA=B3=A0=20=EC=8B=9C=EC=9E=91=20*=20containe?= =?UTF-8?q?r=EB=A5=BC=20=EB=B9=84=EC=9A=B0=EA=B3=A0=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=20append=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/eventManager.js | 6 +++++- src/lib/renderElement.js | 29 ++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/lib/eventManager.js b/src/lib/eventManager.js index 0ba84b4..9ae1f96 100644 --- a/src/lib/eventManager.js +++ b/src/lib/eventManager.js @@ -7,7 +7,7 @@ * } */ -const globalEvents = {}; +let globalEvents = {}; export function setupEventListeners(root) { Object.entries(globalEvents).forEach(([eventType, eventsMap]) => { @@ -34,4 +34,8 @@ export function removeEvent(element, eventType, handler) { } } +export function clearEvents() { + globalEvents = {}; +} + window.__myEventListeners = globalEvents; diff --git a/src/lib/renderElement.js b/src/lib/renderElement.js index 0429572..b3815b4 100644 --- a/src/lib/renderElement.js +++ b/src/lib/renderElement.js @@ -1,10 +1,29 @@ -import { setupEventListeners } from "./eventManager"; import { createElement } from "./createElement"; +import { clearEvents, setupEventListeners } from "./eventManager"; import { normalizeVNode } from "./normalizeVNode"; -import { updateElement } from "./updateElement"; +/** + * @description VNode를 실제 DOM 엘리먼트로 렌더링하는 함수 + * @param {*} vNode createVNode 함수를 통해 생성된 VNode + * @param {HTMLElement} container 실제 DOM 엘리먼트(해당 VNode를 렌더링할 대상) + */ export function renderElement(vNode, container) { - // 최초 렌더링시에는 createElement로 DOM을 생성하고 - // 이후에는 updateElement로 기존 DOM을 업데이트한다. - // 렌더링이 완료되면 container에 이벤트를 등록한다. + // step0. 이벤트 초기화 + clearEvents(); + + // step1. vNode 정규화 + if (!vNode) { + return; + } + const normalizedVNode = normalizeVNode(vNode); + + // step2. 정규화된 vNode를 기반으로 DOM 생성 또는 업데이트 + const element = createElement(normalizedVNode); + + // step3. DOM 초기화 및 업데이트 + container.innerHTML = ""; // container 초기화 + container.append(element); // container에 DOM 추가 + + // step4. 이벤트 핸들러 등록 + setupEventListeners(container); } From 0ff32e7165a165fdb57a5bfccc5d49986305cffc Mon Sep 17 00:00:00 2001 From: JunmanChoi Date: Wed, 25 Dec 2024 02:15:48 +0900 Subject: [PATCH 08/25] =?UTF-8?q?docs:-----------------=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=20=EB=B0=8F=201-1=20=EA=B3=BC=EC=A0=9C=20=ED=86=B5=EA=B3=BC---?= =?UTF-8?q?--------------------?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 5f80994..47e8e9c 100644 --- a/README.md +++ b/README.md @@ -1 +1,6 @@ [8팀 최준만] Chapter 1-2. 프레임워크 없이 SPA 만들기 Part 2 + +------------------24.12.25 02:14--------------------- + +basic 테스트 코드 통과 +챕터 1-1 테스트 코드 통과 From 245951ebf03c9916bdf09d36ba583e1eabd78696 Mon Sep 17 00:00:00 2001 From: JunmanChoi Date: Wed, 25 Dec 2024 16:28:53 +0900 Subject: [PATCH 09/25] =?UTF-8?q?feat:=20diff=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20*=20todo=20:=20=EC=86=8D=EC=84=B1=20?= =?UTF-8?q?=EA=B5=90=EC=B2=B4=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?*=20MuataionObserver=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=B4=20e?= =?UTF-8?q?lement=20=EC=98=B5=EC=A0=80=EB=B9=99=20=EB=B0=8F=20childList=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20=EB=8F=99=EC=9E=91=20=ED=8F=AC=EC=B0=A9=20?= =?UTF-8?q?=EC=8B=9C=20oldVNode=20=EC=B4=88=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/elementObserver.js | 18 +++++++++++++ src/lib/renderElement.js | 27 +++++++++++++++++--- src/lib/updateElement.js | 52 +++++++++++++++++++++++++++++++++++--- 3 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 src/lib/elementObserver.js diff --git a/src/lib/elementObserver.js b/src/lib/elementObserver.js new file mode 100644 index 0000000..4540d24 --- /dev/null +++ b/src/lib/elementObserver.js @@ -0,0 +1,18 @@ +export function createElementChildRemoveObserver(elements, helper) { + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === "childList") { + mutation.removedNodes.forEach((node) => { + console.log(node, "removed"); + helper(node); + }); + } + }); + }); + + for (const element of elements) { + observer.observe(element, { childList: true, subtree: false }); + } + + return observer; +} diff --git a/src/lib/renderElement.js b/src/lib/renderElement.js index b3815b4..724870d 100644 --- a/src/lib/renderElement.js +++ b/src/lib/renderElement.js @@ -1,6 +1,10 @@ import { createElement } from "./createElement"; +import { createElementChildRemoveObserver } from "./elementObserver"; import { clearEvents, setupEventListeners } from "./eventManager"; import { normalizeVNode } from "./normalizeVNode"; +import { updateElement } from "./updateElement"; + +let oldVNode = null; /** * @description VNode를 실제 DOM 엘리먼트로 렌더링하는 함수 @@ -18,12 +22,27 @@ export function renderElement(vNode, container) { const normalizedVNode = normalizeVNode(vNode); // step2. 정규화된 vNode를 기반으로 DOM 생성 또는 업데이트 - const element = createElement(normalizedVNode); + if (!oldVNode) { + // step3. DOM 추가 + const element = createElement(normalizedVNode); + container.append(element); // 첫 렌더링 때만 container에 DOM 추가 + // 예외처리 : DOM 삭제 감지 + createElementChildRemoveObserver([container, document.body], () => { + initOldVNode(); + clearEvents(); + }); + } - // step3. DOM 초기화 및 업데이트 - container.innerHTML = ""; // container 초기화 - container.append(element); // container에 DOM 추가 + // step3. DOM 업데이트 + oldVNode && updateElement(container, normalizedVNode, oldVNode); // step4. 이벤트 핸들러 등록 setupEventListeners(container); + + // step5. oldVNode 업데이트 + oldVNode = normalizedVNode; +} + +function initOldVNode() { + oldVNode = null; } diff --git a/src/lib/updateElement.js b/src/lib/updateElement.js index ac32186..caf55aa 100644 --- a/src/lib/updateElement.js +++ b/src/lib/updateElement.js @@ -1,6 +1,52 @@ -import { addEvent, removeEvent } from "./eventManager"; +// import { addEvent, removeEvent } from "./eventManager"; +import { checkNullishExceptZero } from "../utils/commonUtils.js"; import { createElement } from "./createElement.js"; -function updateAttributes(target, originNewProps, originOldProps) {} +export function updateElement(parentElement, newNode, oldNode, index = 0) { + if (!checkNullishExceptZero(newNode) || typeof newNode === "boolean") { + return; + } -export function updateElement(parentElement, newNode, oldNode, index = 0) {} + // 텍스트 노드인 경우 + if (typeof newNode === "string" || typeof newNode === "number") { + if (parentElement.textContent !== newNode) { + parentElement.textContent = newNode; + } + return; + } + + // oldNode가 없는 경우 새로운 element를 생성해 append + if (!oldNode) { + parentElement.appendChild(createElement(newNode)); + return; + } + + // newNode가 oldNode와 다른 타입인 경우 + if (newNode.type !== oldNode.type) { + parentElement.replaceChild( + createElement(newNode), + parentElement.childNodes[index], + ); + return; + } + + // 타입이 같은 경우 props 및 children 비교 + // updateAttributes(parentElement, newNode.props, oldNode.props); + + // children이 없는 경우 return + if (Math.max(newNode.children.length, oldNode.children.length) === 0) { + return; + } + + const maxLength = Math.max(newNode.children.length, oldNode.children.length); + for (let i = 0; i < maxLength; i++) { + updateElement( + parentElement?.childNodes[index] || parentElement, + newNode.children[i], + oldNode.children[i], + i, + ); + } +} + +// function updateAttributes(target, originNewProps, originOldProps) {} From 05b5ecc92fe3e9c428821c12ed860b457b1e216f Mon Sep 17 00:00:00 2001 From: JunmanChoi Date: Wed, 25 Dec 2024 17:41:43 +0900 Subject: [PATCH 10/25] =?UTF-8?q?feat:=20className->=20class=20=EB=B3=80?= =?UTF-8?q?=ED=99=98=ED=95=A8=EC=88=98=20=EC=99=B8=EB=B6=80=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/createElement.js | 14 ++++---------- src/utils/commonUtils.js | 9 +++++++++ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/lib/createElement.js b/src/lib/createElement.js index 07fec30..d30807c 100644 --- a/src/lib/createElement.js +++ b/src/lib/createElement.js @@ -1,4 +1,7 @@ -import { checkNullishExceptZero } from "../utils/commonUtils"; +import { + checkNullishExceptZero, + replaceIfPropIsClass, +} from "../utils/commonUtils"; import { addEvent } from "./eventManager"; // import { addEvent } from "./eventManager"; @@ -56,12 +59,3 @@ function updateAttributes($el, props) { $el.setAttribute(replaceIfPropIsClass(prop), props[prop]); } } - -/** - * @description prop이 className인 경우 class로 변경 - * @param {object} prop - * @returns {string} - */ -function replaceIfPropIsClass(prop) { - return prop === "className" ? "class" : prop; -} diff --git a/src/utils/commonUtils.js b/src/utils/commonUtils.js index 622dace..9281cd1 100644 --- a/src/utils/commonUtils.js +++ b/src/utils/commonUtils.js @@ -5,3 +5,12 @@ export function checkNullishExceptZero(value) { if (value === 0) return true; return Boolean(value); } + +/** + * @description prop이 className인 경우 class로 변경 + * @param {object} prop + * @returns {string} + */ +export function replaceIfPropIsClass(prop) { + return prop === "className" ? "class" : prop; +} From e209f2baf93029ea25f53b45eb5428e4dd09e954 Mon Sep 17 00:00:00 2001 From: JunmanChoi Date: Wed, 25 Dec 2024 23:10:00 +0900 Subject: [PATCH 11/25] =?UTF-8?q?feat:=20=EC=86=8D=EC=84=B1=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/renderElement.js | 5 +-- src/lib/updateElement.js | 76 +++++++++++++++++++++++++++++++++------- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/lib/renderElement.js b/src/lib/renderElement.js index 724870d..5fd70df 100644 --- a/src/lib/renderElement.js +++ b/src/lib/renderElement.js @@ -12,8 +12,9 @@ let oldVNode = null; * @param {HTMLElement} container 실제 DOM 엘리먼트(해당 VNode를 렌더링할 대상) */ export function renderElement(vNode, container) { - // step0. 이벤트 초기화 - clearEvents(); + if (!container.children[0]) { + initOldVNode(); + } // step1. vNode 정규화 if (!vNode) { diff --git a/src/lib/updateElement.js b/src/lib/updateElement.js index caf55aa..182c849 100644 --- a/src/lib/updateElement.js +++ b/src/lib/updateElement.js @@ -1,9 +1,16 @@ -// import { addEvent, removeEvent } from "./eventManager"; -import { checkNullishExceptZero } from "../utils/commonUtils.js"; +import { replaceIfPropIsClass } from "../utils/commonUtils.js"; import { createElement } from "./createElement.js"; +import { addEvent, removeEvent } from "./eventManager.js"; export function updateElement(parentElement, newNode, oldNode, index = 0) { - if (!checkNullishExceptZero(newNode) || typeof newNode === "boolean") { + if (newNode && !oldNode) { + parentElement.appendChild(createElement(newNode)); + return; + } + + // newNode가 없는 경우 oldNode 제거 + if (oldNode && !newNode) { + parentElement.removeChild(parentElement.childNodes[index]); return; } @@ -15,12 +22,6 @@ export function updateElement(parentElement, newNode, oldNode, index = 0) { return; } - // oldNode가 없는 경우 새로운 element를 생성해 append - if (!oldNode) { - parentElement.appendChild(createElement(newNode)); - return; - } - // newNode가 oldNode와 다른 타입인 경우 if (newNode.type !== oldNode.type) { parentElement.replaceChild( @@ -31,7 +32,11 @@ export function updateElement(parentElement, newNode, oldNode, index = 0) { } // 타입이 같은 경우 props 및 children 비교 - // updateAttributes(parentElement, newNode.props, oldNode.props); + updateAttributes( + parentElement.childNodes[index], + newNode.props, + oldNode.props, + ); // children이 없는 경우 return if (Math.max(newNode.children.length, oldNode.children.length) === 0) { @@ -41,7 +46,7 @@ export function updateElement(parentElement, newNode, oldNode, index = 0) { const maxLength = Math.max(newNode.children.length, oldNode.children.length); for (let i = 0; i < maxLength; i++) { updateElement( - parentElement?.childNodes[index] || parentElement, + parentElement.childNodes[index], newNode.children[i], oldNode.children[i], i, @@ -49,4 +54,51 @@ export function updateElement(parentElement, newNode, oldNode, index = 0) { } } -// function updateAttributes(target, originNewProps, originOldProps) {} +/** + * + * @param {HTMLElement} target + * @param {*} originNewProps + * @param {*} originOldProps + */ +function updateAttributes(target, originNewProps, originOldProps) { + const newPropsArr = originNewProps ? Object.entries(originNewProps) : []; + const oldPropsArr = originOldProps ? Object.entries(originOldProps) : []; + + for (const [prop, value] of oldPropsArr) { + console.log(prop); + if (prop?.startsWith("on")) { + removeEvent(target, prop.slice(2).toLowerCase(), value); + continue; + } + + // 새로운 props에 없는 경우 제거 + if (!originNewProps || !(prop in originNewProps)) { + target.removeAttribute(prop); + } + // 값이 다른 경우 업데이트 + if (value !== originNewProps[prop]) { + if (prop?.startsWith("on")) { + addEvent(target, prop.slice(2).toLowerCase(), originNewProps[prop]); + continue; + } + target.setAttribute(replaceIfPropIsClass(prop), originNewProps[prop]); + } + + if (value === originNewProps[prop]) { + if (prop?.startsWith("on")) { + addEvent(target, prop.slice(2).toLowerCase(), originNewProps[prop]); + continue; + } + } + } + // 새로운 props에 추가 + for (const [prop, value] of newPropsArr) { + if (!originOldProps[prop]) { + if (prop?.startsWith("on")) { + addEvent(target, prop.slice(2).toLowerCase(), value); + continue; + } + target.setAttribute(replaceIfPropIsClass(prop), value); + } + } +} From 96c087fdb09dd84d3cd6ce4015505d929f8e7437 Mon Sep 17 00:00:00 2001 From: JunmanChoi Date: Wed, 25 Dec 2024 23:10:51 +0900 Subject: [PATCH 12/25] =?UTF-8?q?fix:=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=EB=84=88=20remove=EC=8B=9C=20=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=20=EC=A3=BC=EC=86=8C=20=EC=83=81=EC=9D=B4=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EC=A0=9C=EA=B1=B0=20=EC=95=88=EB=90=98=EB=8D=98=20?= =?UTF-8?q?=EC=9D=B4=EC=8A=88=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/eventManager.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/lib/eventManager.js b/src/lib/eventManager.js index 9ae1f96..478bd59 100644 --- a/src/lib/eventManager.js +++ b/src/lib/eventManager.js @@ -9,19 +9,26 @@ let globalEvents = {}; +// 콜백으로 넣으면 add할때의 remove할때의 콜백이 다를 수 있음 +/** + * + * @param {HTMLElement} root + */ export function setupEventListeners(root) { - Object.entries(globalEvents).forEach(([eventType, eventsMap]) => { - root.addEventListener(eventType, (e) => handleGlobalEvents(e, eventsMap)); + Object.entries(globalEvents).forEach(([eventType]) => { + root.removeEventListener(eventType, handleGlobalEvents); + root.addEventListener(eventType, handleGlobalEvents); }); } -export function handleGlobalEvents(e, eventsMap) { - if (eventsMap.has(e.target)) { - eventsMap.get(e.target)(e); +export function handleGlobalEvents(e) { + if (globalEvents[e.type].has(e.target)) { + globalEvents[e.type].get(e.target)(e); } } export function addEvent(element, eventType, handler) { + console.log("in?"); if (!element || typeof handler !== "function") return; globalEvents[eventType] = globalEvents[eventType] || new WeakMap(); From 2e255fc4cf8e00d538ec90f80a54dc54d5e69ac6 Mon Sep 17 00:00:00 2001 From: JunmanChoi Date: Thu, 26 Dec 2024 01:44:56 +0900 Subject: [PATCH 13/25] =?UTF-8?q?fix:=20attr=20=ED=95=A0=EB=8B=B9=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=9E=AC=20=EC=9E=91=EC=84=B1=EC=9D=84=20?= =?UTF-8?q?=ED=86=B5=ED=95=9C=20=EC=9D=B4=EC=8A=88=20=ED=95=B4=EA=B2=B0(?= =?UTF-8?q?=EC=9B=90=EC=9D=B8=20=ED=8C=8C=EC=95=85=20=ED=95=84=EC=9A=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/updateElement.js | 60 +++++++++++++++++++++++++--------------- src/utils/commonUtils.js | 27 ++++++++++++++++++ 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/src/lib/updateElement.js b/src/lib/updateElement.js index 182c849..fa8fe0d 100644 --- a/src/lib/updateElement.js +++ b/src/lib/updateElement.js @@ -1,4 +1,8 @@ -import { replaceIfPropIsClass } from "../utils/commonUtils.js"; +import { + isClass, + isEventProp, + replaceEventProp, +} from "../utils/commonUtils.js"; import { createElement } from "./createElement.js"; import { addEvent, removeEvent } from "./eventManager.js"; @@ -34,8 +38,8 @@ export function updateElement(parentElement, newNode, oldNode, index = 0) { // 타입이 같은 경우 props 및 children 비교 updateAttributes( parentElement.childNodes[index], - newNode.props, - oldNode.props, + newNode.props || {}, + oldNode.props || {}, ); // children이 없는 경우 return @@ -65,40 +69,50 @@ function updateAttributes(target, originNewProps, originOldProps) { const oldPropsArr = originOldProps ? Object.entries(originOldProps) : []; for (const [prop, value] of oldPropsArr) { - console.log(prop); - if (prop?.startsWith("on")) { - removeEvent(target, prop.slice(2).toLowerCase(), value); - continue; + let newPropValue = originNewProps[prop]; + + // 이벤트 프로퍼티인 경우 이벤트 제거하고 시작 + if (isEventProp(prop)) { + removeEvent(target, replaceEventProp(prop), value); + newPropValue = originNewProps[replaceEventProp(prop)]; } - // 새로운 props에 없는 경우 제거 - if (!originNewProps || !(prop in originNewProps)) { + // 새로 들어올 프로퍼티 값이 없는 경우 제거 + if (newPropValue === undefined) { target.removeAttribute(prop); + continue; } - // 값이 다른 경우 업데이트 - if (value !== originNewProps[prop]) { - if (prop?.startsWith("on")) { - addEvent(target, prop.slice(2).toLowerCase(), originNewProps[prop]); + + // 새로 들어올 프로퍼티 값이 있는 경우 업데이트 + if (value !== newPropValue) { + if (isClass(prop)) { + target.classList = newPropValue; + continue; + } + if (isEventProp(prop)) { + addEvent(target, replaceEventProp(prop), newPropValue); continue; } - target.setAttribute(replaceIfPropIsClass(prop), originNewProps[prop]); + + target.setAttribute(prop, newPropValue); + continue; } + // 새로 들어올 프로퍼티 값이 같은 경우 if (value === originNewProps[prop]) { - if (prop?.startsWith("on")) { - addEvent(target, prop.slice(2).toLowerCase(), originNewProps[prop]); - continue; - } + continue; } } - // 새로운 props에 추가 + for (const [prop, value] of newPropsArr) { - if (!originOldProps[prop]) { - if (prop?.startsWith("on")) { - addEvent(target, prop.slice(2).toLowerCase(), value); + // 이전 프로퍼티에 없는 프로퍼티인 경우 추가 + if (oldPropsArr[prop] === undefined) { + if (isEventProp(prop)) addEvent(target, replaceEventProp(prop), value); + if (isClass(prop)) { + target.classList = value; continue; } - target.setAttribute(replaceIfPropIsClass(prop), value); + target.setAttribute(prop, value); } } } diff --git a/src/utils/commonUtils.js b/src/utils/commonUtils.js index 9281cd1..eacc22c 100644 --- a/src/utils/commonUtils.js +++ b/src/utils/commonUtils.js @@ -14,3 +14,30 @@ export function checkNullishExceptZero(value) { export function replaceIfPropIsClass(prop) { return prop === "className" ? "class" : prop; } + +/** + * + * @param {string} prop + * @returns {boolean} + */ +export function isEventProp(prop) { + return prop.startsWith("on"); +} + +/** + * @description 이벤트 프로퍼티인 경우 on 제거하고 소문자로 변경 + * @param {string} prop + * @returns {string} + */ +export function replaceEventProp(prop) { + return prop.replace("on", "").toLowerCase(); +} + +/** + * + * @param {string} prop + * @returns {boolean} + */ +export function isClass(prop) { + return prop === "className"; +} From e26bbb58056047461b11516400a20c51f309a4bf Mon Sep 17 00:00:00 2001 From: JunmanChoi Date: Thu, 26 Dec 2024 01:46:49 +0900 Subject: [PATCH 14/25] =?UTF-8?q?fix:=20=EC=83=9D=EC=84=B1=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=9C=EC=84=9C=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F?= =?UTF-8?q?=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EC=BD=94=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=EB=A5=BC=20=ED=86=B5=ED=95=B4=201-1=20=EC=9D=B4?= =?UTF-8?q?=EC=8A=88=20=ED=95=B4=EA=B2=B0(=EC=9B=90=EC=9D=B8=20=ED=8C=8C?= =?UTF-8?q?=EC=95=85=20=ED=95=84=EC=9A=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/renderElement.js | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/src/lib/renderElement.js b/src/lib/renderElement.js index 5fd70df..083e519 100644 --- a/src/lib/renderElement.js +++ b/src/lib/renderElement.js @@ -1,6 +1,5 @@ import { createElement } from "./createElement"; -import { createElementChildRemoveObserver } from "./elementObserver"; -import { clearEvents, setupEventListeners } from "./eventManager"; +import { setupEventListeners } from "./eventManager"; import { normalizeVNode } from "./normalizeVNode"; import { updateElement } from "./updateElement"; @@ -12,38 +11,27 @@ let oldVNode = null; * @param {HTMLElement} container 실제 DOM 엘리먼트(해당 VNode를 렌더링할 대상) */ export function renderElement(vNode, container) { - if (!container.children[0]) { - initOldVNode(); - } - - // step1. vNode 정규화 if (!vNode) { return; } - const normalizedVNode = normalizeVNode(vNode); + // step1. vNode 정규화 + const newVNode = normalizeVNode(vNode); // step2. 정규화된 vNode를 기반으로 DOM 생성 또는 업데이트 if (!oldVNode) { // step3. DOM 추가 - const element = createElement(normalizedVNode); + const element = createElement(newVNode); container.append(element); // 첫 렌더링 때만 container에 DOM 추가 - // 예외처리 : DOM 삭제 감지 - createElementChildRemoveObserver([container, document.body], () => { - initOldVNode(); - clearEvents(); - }); + } else { + if (container.firstChild) { + updateElement(container, newVNode, oldVNode); + } else { + const element = createElement(newVNode); + container.append(element); + } } - - // step3. DOM 업데이트 - oldVNode && updateElement(container, normalizedVNode, oldVNode); - // step4. 이벤트 핸들러 등록 setupEventListeners(container); - // step5. oldVNode 업데이트 - oldVNode = normalizedVNode; -} - -function initOldVNode() { - oldVNode = null; + oldVNode = newVNode; } From 0a3d138abba56014ec9718f231b70c108752aacd Mon Sep 17 00:00:00 2001 From: JunmanChoi Date: Thu, 26 Dec 2024 01:47:30 +0900 Subject: [PATCH 15/25] =?UTF-8?q?feat:=20=EC=BD=94=EB=93=9C=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/eventManager.js | 5 ++--- src/lib/normalizeVNode.js | 10 ++-------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/lib/eventManager.js b/src/lib/eventManager.js index 478bd59..aa18da4 100644 --- a/src/lib/eventManager.js +++ b/src/lib/eventManager.js @@ -15,10 +15,10 @@ let globalEvents = {}; * @param {HTMLElement} root */ export function setupEventListeners(root) { - Object.entries(globalEvents).forEach(([eventType]) => { + for (const eventType in globalEvents) { root.removeEventListener(eventType, handleGlobalEvents); root.addEventListener(eventType, handleGlobalEvents); - }); + } } export function handleGlobalEvents(e) { @@ -28,7 +28,6 @@ export function handleGlobalEvents(e) { } export function addEvent(element, eventType, handler) { - console.log("in?"); if (!element || typeof handler !== "function") return; globalEvents[eventType] = globalEvents[eventType] || new WeakMap(); diff --git a/src/lib/normalizeVNode.js b/src/lib/normalizeVNode.js index 3b83dca..19a67ff 100644 --- a/src/lib/normalizeVNode.js +++ b/src/lib/normalizeVNode.js @@ -25,14 +25,8 @@ export function normalizeVNode(vNode) { type: vNode.type, props: vNode.props, children: vNode.children - .map((child) => { - // 자식 노드 정규화 - return normalizeVNode(child); - }) - .filter((child) => { - // 0을 제외한 falsy 값 제거 - return checkNullishExceptZero(child); - }), + .map(normalizeVNode) + .filter(checkNullishExceptZero), }; } } From 11e904a47228ce571701e20512f0eb4474b7a768 Mon Sep 17 00:00:00 2001 From: JunmanChoi Date: Thu, 26 Dec 2024 03:14:48 +0900 Subject: [PATCH 16/25] =?UTF-8?q?fix:=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=85=B8=EB=93=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EC=8B=9C=20parentElement=EC=9D=98=20textContent=EC=97=90=20?= =?UTF-8?q?=EB=B0=94=EB=A1=9C=20=EC=82=BD=EC=9E=85=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=EB=90=98=EB=A9=B4=20=EC=88=9C=EC=84=9C=20=EB=B3=B4=EC=9E=A5?= =?UTF-8?q?=EC=9D=B4=EC=95=88=EB=90=98=EA=B3=A0=20=EB=8D=AE=EC=96=B4?= =?UTF-8?q?=EC=94=8C=EC=9B=8C=EC=A7=80=EB=8A=94=20=EC=9D=B4=EC=8A=88?= =?UTF-8?q?=EA=B0=80=20=EC=9E=88=EC=9D=8C=20-=20=ED=98=84=EC=9E=AC?= =?UTF-8?q?=EB=85=B8=EB=93=9C=EA=B0=80=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/updateElement.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/updateElement.js b/src/lib/updateElement.js index fa8fe0d..db9d9e1 100644 --- a/src/lib/updateElement.js +++ b/src/lib/updateElement.js @@ -20,8 +20,8 @@ export function updateElement(parentElement, newNode, oldNode, index = 0) { // 텍스트 노드인 경우 if (typeof newNode === "string" || typeof newNode === "number") { - if (parentElement.textContent !== newNode) { - parentElement.textContent = newNode; + if (oldNode !== newNode) { + parentElement.childNodes[index].textContent = newNode; } return; } From c7c7541e29349a1324fdea366d691c6cd88a9599 Mon Sep 17 00:00:00 2001 From: JunmanChoi Date: Thu, 26 Dec 2024 03:15:18 +0900 Subject: [PATCH 17/25] =?UTF-8?q?feat:=20=ED=8F=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/posts/Post.jsx | 11 ++++++++++- src/components/posts/PostForm.jsx | 22 ++++++++++++++++++++++ src/lib/createElement.js | 2 +- src/pages/HomePage.jsx | 2 +- src/stores/globalStore.js | 25 +++++++++++++++++++++---- 5 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src/components/posts/Post.jsx b/src/components/posts/Post.jsx index 67af756..8d882b4 100644 --- a/src/components/posts/Post.jsx +++ b/src/components/posts/Post.jsx @@ -1,14 +1,22 @@ /** @jsx createVNode */ -import { createVNode } from "../../lib"; +import { addEvent, createVNode } from "../../lib"; +import { globalStore } from "../../stores/globalStore.js"; import { toTimeFormat } from "../../utils/index.js"; export const Post = ({ + id, author, time, content, likeUsers, activationLike = false, }) => { + const { like } = globalStore.actions; + + const onClickLike = () => { + like(id); + }; + return (
@@ -21,6 +29,7 @@ export const Post = ({
좋아요 {likeUsers.length} diff --git a/src/components/posts/PostForm.jsx b/src/components/posts/PostForm.jsx index 36a2513..68f9179 100644 --- a/src/components/posts/PostForm.jsx +++ b/src/components/posts/PostForm.jsx @@ -1,7 +1,28 @@ /** @jsx createVNode */ import { createVNode } from "../../lib"; +import { globalStore } from "../../stores"; export const PostForm = () => { + const { currentUser, loggedIn, posts } = globalStore.getState(); + + if (!loggedIn) { + return null; + } + + const onClickAddPost = () => { + globalStore.setState({ + posts: [ + { + id: posts[0].id + 1, + author: currentUser.username, + time: Date.now(), + content: document.getElementById("post-content").value, + likeUsers: [], + }, + ...posts, + ], + }); + }; return (