From da7b1c9462e7c06a697a38bfaa2bdbc77e4ddcc2 Mon Sep 17 00:00:00 2001 From: suzy Date: Tue, 24 Dec 2024 00:31:00 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20createElement,=20updateElement=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/createElement.js | 42 ++++++++++++++++++++- src/lib/createVNode.js | 8 +++- src/lib/normalizeVNode.js | 27 ++++++++++++- src/lib/renderElement.js | 15 ++++++++ src/lib/updateElement.js | 79 ++++++++++++++++++++++++++++++++++++++- 5 files changed, 165 insertions(+), 6 deletions(-) diff --git a/src/lib/createElement.js b/src/lib/createElement.js index 5d39ae7..36f90ca 100644 --- a/src/lib/createElement.js +++ b/src/lib/createElement.js @@ -1,5 +1,43 @@ import { addEvent } from "./eventManager"; -export function createElement(vNode) {} +export function createElement(vNode) { + if ( + typeof vNode === "undefined" || + vNode === null || + typeof vNode === "boolean" + ) { + return document.createTextNode(""); + } -function updateAttributes($el, props) {} + if (typeof vNode === "string" || typeof vNode === "number") { + return document.createTextNode(vNode); + } + + if (Array.isArray(vNode)) { + const fragment = document.createDocumentFragment(); + vNode.forEach((child) => fragment.appendChild(createElement(child))); + + 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(([attr, value]) => { + if (attr.startsWith("on") && typeof value === "function") { + const eventType = attr.toLowerCase().slice(2); + addEvent($el, eventType, value); + } else if (attr === "className") { + $el.setAttribute("class", value); + } else { + $el.setAttribute(attr, value); + } + }); +} diff --git a/src/lib/createVNode.js b/src/lib/createVNode.js index 9991337..8547243 100644 --- a/src/lib/createVNode.js +++ b/src/lib/createVNode.js @@ -1,3 +1,9 @@ export function createVNode(type, props, ...children) { - return {}; + return { + type, + props, + children: children + .flat(Infinity) + .filter((value) => value === 0 || Boolean(value)), + }; } diff --git a/src/lib/normalizeVNode.js b/src/lib/normalizeVNode.js index 7dc6f17..4bd50ef 100644 --- a/src/lib/normalizeVNode.js +++ b/src/lib/normalizeVNode.js @@ -1,3 +1,28 @@ export function normalizeVNode(vNode) { - return vNode; + if ( + typeof vNode === "undefined" || + vNode === null || + 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 }), + ); + } + + if (Array.isArray(vNode)) { + return vNode.map(normalizeVNode).join(""); + } + + return { + ...vNode, + children: vNode.children.map(normalizeVNode).filter(Boolean), + }; } diff --git a/src/lib/renderElement.js b/src/lib/renderElement.js index 0429572..254d6c0 100644 --- a/src/lib/renderElement.js +++ b/src/lib/renderElement.js @@ -3,8 +3,23 @@ import { createElement } from "./createElement"; import { normalizeVNode } from "./normalizeVNode"; import { updateElement } from "./updateElement"; +const OldNodeMap = new WeakMap(); + export function renderElement(vNode, container) { // 최초 렌더링시에는 createElement로 DOM을 생성하고 // 이후에는 updateElement로 기존 DOM을 업데이트한다. // 렌더링이 완료되면 container에 이벤트를 등록한다. + + const oldNode = OldNodeMap.get(container); + const newNode = normalizeVNode(vNode); + + if (!oldNode) { + const element = createElement(newNode); + container.appendChild(element); + } else { + updateElement(container, newNode, oldNode); + } + + OldNodeMap.set(container, newNode); + setupEventListeners(container); } diff --git a/src/lib/updateElement.js b/src/lib/updateElement.js index ac32186..f7d2b35 100644 --- a/src/lib/updateElement.js +++ b/src/lib/updateElement.js @@ -1,6 +1,81 @@ import { addEvent, removeEvent } from "./eventManager"; import { createElement } from "./createElement.js"; -function updateAttributes(target, originNewProps, originOldProps) {} +function updateAttributes(target, originNewProps, originOldProps) { + const oldProps = originOldProps || {}; + const newProps = originNewProps || {}; -export function updateElement(parentElement, newNode, oldNode, index = 0) {} + for (const attr in oldProps) { + if (!(attr in newProps)) { + if (attr.startsWith("on") && typeof oldProps[attr] === "function") { + const eventType = attr.toLowerCase().slice(2); + removeEvent(target, eventType, oldProps[attr]); + } else { + target.removeAttribute(attr); + } + } + } + + for (const attr in newProps) { + if (oldProps[attr] !== newProps[attr]) { + if (attr.startsWith("on") && typeof newProps[attr] === "function") { + const eventType = attr.toLowerCase().slice(2); + if (typeof oldProps[attr] === "function") { + removeEvent(target, eventType, oldProps[attr]); + } + addEvent(target, eventType, newProps[attr]); + } else if (attr === "className") { + target.className = newProps[attr]; + } + } + } +} + +export function updateElement(parentElement, newNode, oldNode, index = 0) { + // oldNode만 있는 경우: oldNode를 parentElement에서 제거한다. + if (!newNode && oldNode) { + return parentElement.removeChild(parentElement.childNodes[index]); + } + + // newNode만 있는 경우: newNode를 parentElement에 추가한다. + if (!oldNode && newNode) { + return parentElement.appendChild(createElement(newNode)); + } + + // oldNode와 newNode 모두 string인 경우: oldNode와 newNode 내용이 다르다면, newNode 내용으로 교체한다. + if (typeof oldNode === "string" || typeof newNode === "string") { + if (newNode === oldNode) return; + return parentElement.replaceChild( + createElement(newNode), + parentElement.childNodes[index], + ); + } + + // oldeNode와 newNode의 태그 이름(type)이 다를 경우: oldNode를 제거하고 해당 위치에 newNode를 추가한다. + if (oldNode.type !== newNode.type) { + return parentElement.replaceChild( + createElement(newNode), + parentElement.childNodes[index], + ); + } + + // oldNode와 newNode의 태그 이름(type)이 같을 경우: newNode와 oldNode의 속성을 비교하여 변경된 부분만 반영한다. + if (oldNode.type === newNode.type) { + updateAttributes( + parentElement.childNodes[index], + newNode.props || {}, + oldNode.props || {}, + ); + } + + // oldNode와 newNode를 순회하며, 앞에 조건식을 반복한다. + 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], + ); + } +} From c8166744d9ab6e66e8719a118ef26434bb57fa90 Mon Sep 17 00:00:00 2001 From: suzy Date: Fri, 27 Dec 2024 00:34:35 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20chapter=201-2=20test=20=ED=86=B5?= =?UTF-8?q?=EA=B3=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/eventManager.js | 70 ++++++++++++++++++++++++++++++++++++++-- src/lib/renderElement.js | 1 + src/lib/updateElement.js | 54 +++++++++++++++---------------- 3 files changed, 95 insertions(+), 30 deletions(-) diff --git a/src/lib/eventManager.js b/src/lib/eventManager.js index 24e4240..acd8091 100644 --- a/src/lib/eventManager.js +++ b/src/lib/eventManager.js @@ -1,5 +1,69 @@ -export function setupEventListeners(root) {} +const eventListeners = new Map(); +let $root = null; -export function addEvent(element, eventType, handler) {} +function handleEvent(e) { + let target = e.target; -export function removeEvent(element, eventType, handler) {} + while (target && target !== $root) { + const handlers = eventListeners.get(e.type)?.get(target); + if (handlers) { + handlers.forEach((handler) => handler(e)); + } + target = target.parentElement; + } +} + +export function setupEventListeners(root) { + if ($root) return; + $root = root; + + eventListeners.forEach((handlers, eventType) => { + $root.removeEventListener(eventType, handleEvent); + $root.addEventListener(eventType, handleEvent); + }); + + // const supportedEvents = ["click", "input", "change", "submit"]; + + // supportedEvents.forEach((eventType) => { + // if (!eventListeners.has(eventType)) { + // eventListeners.set(eventType, new Map()); + // } + + // root.addEventListener(eventType, handleEvent, true); + // }); +} + +export function addEvent(element, eventType, handler) { + if (!eventListeners.has(eventType)) { + eventListeners.set(eventType, new WeakMap()); + } + + const handlers = eventListeners.get(eventType); + if (!handlers.has(element)) { + handlers.set(element, new Set()); + } + + handlers.get(element).add(handler); +} + +export function removeEvent(element, eventType, handler) { + const handlers = eventListeners.get(eventType); + + if (!handlers) return; + + const handlerList = handlers.get(element); + if (handlerList) { + handlerList.delete(handler); + if (handlerList.size === 0) { + handlers.delete(element); + } + } + + if (eventListeners.size === 0) { + eventListeners.delete(eventType); + if ($root && $root._listeners?.has(eventType)) { + $root.removeEventListener(eventType, handleEvent, true); + $root._listeners.delete(eventType); + } + } +} diff --git a/src/lib/renderElement.js b/src/lib/renderElement.js index 254d6c0..a4b0cc3 100644 --- a/src/lib/renderElement.js +++ b/src/lib/renderElement.js @@ -9,6 +9,7 @@ export function renderElement(vNode, container) { // 최초 렌더링시에는 createElement로 DOM을 생성하고 // 이후에는 updateElement로 기존 DOM을 업데이트한다. // 렌더링이 완료되면 container에 이벤트를 등록한다. + console.log(`renderElement ${JSON.stringify(vNode)}, ${container}`); const oldNode = OldNodeMap.get(container); const newNode = normalizeVNode(vNode); diff --git a/src/lib/updateElement.js b/src/lib/updateElement.js index f7d2b35..5536c2e 100644 --- a/src/lib/updateElement.js +++ b/src/lib/updateElement.js @@ -7,8 +7,8 @@ function updateAttributes(target, originNewProps, originOldProps) { for (const attr in oldProps) { if (!(attr in newProps)) { - if (attr.startsWith("on") && typeof oldProps[attr] === "function") { - const eventType = attr.toLowerCase().slice(2); + if (attr.startsWith("on") && typeof newProps[attr] === "function") { + const eventType = attr.slice(2).toLowerCase(); removeEvent(target, eventType, oldProps[attr]); } else { target.removeAttribute(attr); @@ -19,63 +19,63 @@ function updateAttributes(target, originNewProps, originOldProps) { for (const attr in newProps) { if (oldProps[attr] !== newProps[attr]) { if (attr.startsWith("on") && typeof newProps[attr] === "function") { - const eventType = attr.toLowerCase().slice(2); + const eventType = attr.slice(2).toLowerCase(); if (typeof oldProps[attr] === "function") { removeEvent(target, eventType, oldProps[attr]); } addEvent(target, eventType, newProps[attr]); } else if (attr === "className") { target.className = newProps[attr]; + } else if (attr === "style" && typeof newProps[attr] === "object") { + Object.entries(newProps[attr]).forEach(([key, value]) => { + target.style[key] = value; + }); + } else { + target.setAttribute(attr, newProps[attr]); } } } } export function updateElement(parentElement, newNode, oldNode, index = 0) { + const existingNode = parentElement.childNodes[index]; + // oldNode만 있는 경우: oldNode를 parentElement에서 제거한다. if (!newNode && oldNode) { - return parentElement.removeChild(parentElement.childNodes[index]); + parentElement.removeChild(existingNode); + return; } // newNode만 있는 경우: newNode를 parentElement에 추가한다. if (!oldNode && newNode) { - return parentElement.appendChild(createElement(newNode)); + parentElement.appendChild(createElement(newNode)); + return; } // oldNode와 newNode 모두 string인 경우: oldNode와 newNode 내용이 다르다면, newNode 내용으로 교체한다. if (typeof oldNode === "string" || typeof newNode === "string") { - if (newNode === oldNode) return; - return parentElement.replaceChild( - createElement(newNode), - parentElement.childNodes[index], - ); + if (newNode !== oldNode) { + parentElement.replaceChild(createElement(newNode), existingNode); + } + return; } // oldeNode와 newNode의 태그 이름(type)이 다를 경우: oldNode를 제거하고 해당 위치에 newNode를 추가한다. if (oldNode.type !== newNode.type) { - return parentElement.replaceChild( - createElement(newNode), - parentElement.childNodes[index], - ); + parentElement.replaceChild(createElement(newNode), existingNode); + return; } // oldNode와 newNode의 태그 이름(type)이 같을 경우: newNode와 oldNode의 속성을 비교하여 변경된 부분만 반영한다. - if (oldNode.type === newNode.type) { - updateAttributes( - parentElement.childNodes[index], - newNode.props || {}, - oldNode.props || {}, - ); - } + updateAttributes(existingNode, newNode?.props, oldNode?.props); // oldNode와 newNode를 순회하며, 앞에 조건식을 반복한다. - const maxLength = Math.max(newNode.children.length, oldNode.children.length); + const newChildren = newNode.children || []; + const oldChildren = oldNode.children || []; + + const maxLength = Math.max(newChildren.length, oldChildren.length); for (let i = 0; i < maxLength; i++) { - updateElement( - parentElement.childNodes[index], - newNode.children[i], - oldNode.children[i], - ); + updateElement(existingNode, newChildren[i], oldChildren[i], i); } } From 7c04ac7a489bfa79d9b274007aae225a8bfdf916 Mon Sep 17 00:00:00 2001 From: suzy Date: Fri, 27 Dec 2024 03:13:13 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20chapter=201-1,=201-2=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=EA=B3=BC=EC=A0=9C=20=ED=86=B5=EA=B3=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/posts/Post.jsx | 45 +++++++++++++++++++++ src/components/posts/PostForm.jsx | 24 ++++++++++++ src/lib/eventManager.js | 65 +++++++++++++++++++++++++------ src/lib/normalizeVNode.js | 14 +++++-- src/lib/renderElement.js | 2 +- src/lib/updateElement.js | 7 ++-- src/pages/HomePage.jsx | 27 +++++++++---- 7 files changed, 157 insertions(+), 27 deletions(-) diff --git a/src/components/posts/Post.jsx b/src/components/posts/Post.jsx index 67af756..c559fe2 100644 --- a/src/components/posts/Post.jsx +++ b/src/components/posts/Post.jsx @@ -1,14 +1,58 @@ /** @jsx createVNode */ import { 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 { getState, setState } = globalStore; + const { currentUser, loggedIn, posts } = getState(); + + const handleLike = () => { + if (!loggedIn) { + window.alert("로그인 후 이용해주세요."); + return; + } + + const targetPost = posts.find((post) => post.id === id); + const username = currentUser.username; + + // 좋아요 상태 업데이트 + const updatedLikeUsers = targetPost.likeUsers.includes(username) + ? targetPost.likeUsers.filter((user) => user !== username) + : [...targetPost.likeUsers, username]; + + // 새 포스트 업데이트 + const updatedPost = { ...targetPost, likeUsers: updatedLikeUsers }; + const updatedPosts = posts.map((post) => + post.id === id ? updatedPost : post, + ); + + setState({ posts: updatedPosts }); + + // const targetPost = posts.find((post) => post.id === id); + // const prevLikeUsers = targetPost.likeUsers; + // const username = currentUser.username; + // const likeUsers = prevLikeUsers.includes(username) + // ? prevLikeUsers.filter((user) => user !== username) + // : [...prevLikeUsers, username]; + // const newPost = { ...targetPost, likeUsers }; + + // updatePost(newPost); + }; + + // const updatePost = (newPost) => { + // const { id } = newPost; + // const newPosts = posts.map((post) => (post.id === id ? newPost : post)); + // setState({ posts: newPosts }); + // }; + return (
@@ -20,6 +64,7 @@ export const Post = ({

{content}

좋아요 {likeUsers.length} diff --git a/src/components/posts/PostForm.jsx b/src/components/posts/PostForm.jsx index 36a2513..7222e0a 100644 --- a/src/components/posts/PostForm.jsx +++ b/src/components/posts/PostForm.jsx @@ -1,7 +1,30 @@ /** @jsx createVNode */ import { createVNode } from "../../lib"; +import { globalStore } from "../../stores/globalStore"; export const PostForm = () => { + const { getState, setState } = globalStore; + const { currentUser, posts } = getState(); + + const submitPost = () => { + const contentEl = document.querySelector("#post-content"); + const content = contentEl.value.trim(); + + if (!currentUser || !content) return; + + const newPosts = [ + ...posts, + { + id: posts.length + 1, + author: currentUser?.username, + time: new Date().getTime(), + content, + likeUsers: [], + }, + ]; + + setState({ posts: newPosts }); + }; return (