From 37a3cff80b207f6f258d6013c3ca492c20ee5c55 Mon Sep 17 00:00:00 2001 From: uuyeong Date: Thu, 26 Dec 2024 15:28:59 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat:=201=EC=B0=A8=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/errors/InvalidVNodeTypeError.js | 7 ++++ src/lib/createElement.js | 49 +++++++++++++++++++++- src/lib/createVNode.js | 14 ++++++- src/lib/eventManager.js | 59 +++++++++++++++++++++++++-- src/lib/normalizeVNode.js | 28 ++++++++++++- src/lib/renderElement.js | 13 ++++-- src/lib/updateElement.js | 63 +++++++++++++++++++++++++++-- src/utils/domUtils.js | 3 ++ src/utils/eventUtils.js | 4 ++ vite.config.js | 8 ++++ 10 files changed, 233 insertions(+), 15 deletions(-) create mode 100644 src/errors/InvalidVNodeTypeError.js create mode 100644 src/utils/domUtils.js diff --git a/src/errors/InvalidVNodeTypeError.js b/src/errors/InvalidVNodeTypeError.js new file mode 100644 index 0000000..a4ecadf --- /dev/null +++ b/src/errors/InvalidVNodeTypeError.js @@ -0,0 +1,7 @@ +export class InvalidVNodeTypeError extends Error { + static MESSAGE = "InvalidVNodeTypeError"; + + constructor() { + super(InvalidVNodeTypeError.MESSAGE); + } +} diff --git a/src/lib/createElement.js b/src/lib/createElement.js index 5d39ae7..7d603fb 100644 --- a/src/lib/createElement.js +++ b/src/lib/createElement.js @@ -1,5 +1,50 @@ +import { InvalidVNodeTypeError } from "../errors/InvalidVNodeTypeError"; +import { isRenderableVNode, isTextNode } from "../utils/domUtils"; +import { getEventTypeFromProps } from "../utils/eventUtils"; import { addEvent } from "./eventManager"; -export function createElement(vNode) {} +export function createElement(vNode) { + if (typeof vNode === "function") { + throw new InvalidVNodeTypeError(); + } -function updateAttributes($el, props) {} + if (!isRenderableVNode(vNode)) { + return document.createTextNode(""); + } + + if (isTextNode(vNode)) { + return document.createTextNode(vNode); + } + + if (Array.isArray(vNode)) { + const docFragment = document.createDocumentFragment(); + const childNodes = vNode.map(createElement); + childNodes.forEach((childNode) => docFragment.appendChild(childNode)); + return docFragment; + } + + const $element = document.createElement(vNode.type); + for (let [key, value] of Object.entries(vNode.props || {})) { + addAttirbutes($element, key, value); + } + const childNodes = (vNode.children || []).map(createElement); + childNodes.forEach((childNode) => $element.appendChild(childNode)); + return $element; +} + +export const addAttirbutes = (element, key, value) => { + if (key.startsWith("on")) { + addEvent(element, getEventTypeFromProps(key), value); + return; + } + if (key === "className") { + element.setAttribute("class", value); + return; + } + if (key === "children") { + return; + } + element.setAttribute(key, value); + + return element; +}; diff --git a/src/lib/createVNode.js b/src/lib/createVNode.js index 9991337..bd7327f 100644 --- a/src/lib/createVNode.js +++ b/src/lib/createVNode.js @@ -1,3 +1,13 @@ -export function createVNode(type, props, ...children) { - return {}; +import { isRenderableVNode } from "../utils/domUtils"; + +export function createVNode(type, props, ...childrens) { + const flatChildren = childrens + .flat(Infinity) + .filter((children) => isRenderableVNode(children)); + + return { + type, + props, + children: flatChildren, + }; } diff --git a/src/lib/eventManager.js b/src/lib/eventManager.js index 24e4240..8c2cadd 100644 --- a/src/lib/eventManager.js +++ b/src/lib/eventManager.js @@ -1,5 +1,58 @@ -export function setupEventListeners(root) {} +const eventManager = () => { + let events = []; + let rootElement = null; -export function addEvent(element, eventType, handler) {} + const eventWrapper = (addEvent) => { + return (e) => { + const target = e.target; + if (target !== addEvent.element) return; + addEvent.handler(e); + }; + }; + const setupEventListeners = (root) => { + rootElement = root; -export function removeEvent(element, eventType, handler) {} + events.forEach((event) => { + rootElement.addEventListener(event.eventType, event.handler); + }); + }; + + const addEvent = (element, eventType, handler) => { + const existingEvent = events.find( + (event) => + event.element === element && + event.eventType === eventType && + event.originalHandler === handler, + ); + if (existingEvent) return; + const convertEventHandler = eventWrapper({ element, eventType, handler }); + events.push({ + element, + eventType, + handler: convertEventHandler, + originalHandler: handler, + }); + }; + const removeEvent = (element, eventType, handler) => { + const sameEvent = events.filter( + (prevEvent) => + prevEvent.element === element && + prevEvent.eventType === eventType && + prevEvent.originalHandler === handler, + ); + + sameEvent.forEach(({ eventType, handler: convertHandler }) => { + rootElement.removeEventListener(eventType, convertHandler); + }); + + events = events.filter( + (prevEvent) => + prevEvent.element !== element || + prevEvent.eventType !== eventType || + prevEvent.originalHandler !== handler, + ); + }; + + return { setupEventListeners, addEvent, removeEvent }; +}; +export const { setupEventListeners, addEvent, removeEvent } = eventManager(); diff --git a/src/lib/normalizeVNode.js b/src/lib/normalizeVNode.js index 7dc6f17..9d85af3 100644 --- a/src/lib/normalizeVNode.js +++ b/src/lib/normalizeVNode.js @@ -1,3 +1,29 @@ +import { isRenderableVNode, isTextNode } from "../utils/domUtils"; + export function normalizeVNode(vNode) { - return vNode; + if (!isRenderableVNode(vNode)) { + return ""; + } + + if (isTextNode(vNode)) { + return String(vNode); + } + + if (typeof vNode.type === "function") { + const newVNode = normalizeVNode( + vNode.type({ ...vNode.props, children: vNode.children }), + ); + return newVNode; + } + + if (Array.isArray(vNode)) { + const vNodoes = vNode.map(normalizeVNode); + return vNodoes; + } + + const childNodes = vNode.children.map(normalizeVNode); + return { + ...vNode, + children: childNodes, + }; } diff --git a/src/lib/renderElement.js b/src/lib/renderElement.js index 0429572..9171394 100644 --- a/src/lib/renderElement.js +++ b/src/lib/renderElement.js @@ -4,7 +4,14 @@ import { normalizeVNode } from "./normalizeVNode"; import { updateElement } from "./updateElement"; export function renderElement(vNode, container) { - // 최초 렌더링시에는 createElement로 DOM을 생성하고 - // 이후에는 updateElement로 기존 DOM을 업데이트한다. - // 렌더링이 완료되면 container에 이벤트를 등록한다. + const normalizedNode = normalizeVNode(vNode); + + if (container.oldVNode) { + updateElement(container, normalizedNode, container.oldVNode); + } else { + const newNode = createElement(normalizedNode); + container.appendChild(newNode); + } + setupEventListeners(container); + container.oldVNode = vNode; } diff --git a/src/lib/updateElement.js b/src/lib/updateElement.js index ac32186..e983b15 100644 --- a/src/lib/updateElement.js +++ b/src/lib/updateElement.js @@ -1,6 +1,61 @@ -import { addEvent, removeEvent } from "./eventManager"; -import { createElement } from "./createElement.js"; +import { isTextNode } from "../utils/domUtils.js"; +import { getEventTypeFromProps } from "../utils/eventUtils.js"; +import { addAttirbutes, createElement } from "./createElement.js"; +import { removeEvent } from "./eventManager.js"; -function updateAttributes(target, originNewProps, originOldProps) {} +function updateAttributes(element, newProps, oldProps) { + for (let [key, value] of Object.entries(oldProps)) { + if (key in newProps) return; -export function updateElement(parentElement, newNode, oldNode, index = 0) {} + element.removeAttribute(key); + + if (key.startsWith("on")) { + removeEvent(element, getEventTypeFromProps(key), value); + return element; + } + } + for (let [key, value] of Object.entries(newProps)) { + addAttirbutes(element, key, value); + } +} + +export function updateElement(parentElement, newNode, oldNode, index = 0) { + const oldElement = parentElement.childNodes[index]; + + //oldNode만 있는 경우 + if (oldNode && !newNode) { + parentElement.removeChild(oldElement); + return; + } + + //newNode만 있는 경우 + if (newNode && !oldNode) { + const newElement = createElement(newNode); + parentElement.appendChild(newElement); + return; + } + //oldNode와 newNode 모두 text 타입일 경우 + if (isTextNode(newNode) && isTextNode(oldNode) && oldNode !== newNode) { + const newTextElement = document.createTextNode(newNode); + parentElement.replaceChild(newTextElement, oldElement); + return; + } + //oldNode와 newNode의 태그 이름(type)이 다를 경우 + if (newNode.type !== oldNode.type) { + parentElement.removeChild(oldElement); + parentElement.appendChild(createElement(newNode)); + return; + } + + if (newNode.type === oldNode.type) { + updateAttributes(oldElement, newNode.props || {}, oldNode.props || {}); + } + + const oldChildren = oldNode.children || []; + const newChildren = newNode.children || []; + const maxChildrenLength = Math.max(oldChildren.length, newChildren.length); + + for (let i = 0; i < maxChildrenLength; i++) { + updateElement(oldElement, newChildren[i], oldChildren[i], i); + } +} diff --git a/src/utils/domUtils.js b/src/utils/domUtils.js new file mode 100644 index 0000000..ee7c021 --- /dev/null +++ b/src/utils/domUtils.js @@ -0,0 +1,3 @@ +export const isTextNode = (node) => ["string", "number"].includes(typeof node); +export const isRenderableVNode = (vNode) => + vNode != undefined && vNode != null && typeof vNode !== "boolean"; diff --git a/src/utils/eventUtils.js b/src/utils/eventUtils.js index 18f608f..05801ec 100644 --- a/src/utils/eventUtils.js +++ b/src/utils/eventUtils.js @@ -33,3 +33,7 @@ export const addEvent = (eventType, selector, handler) => { } eventHandlers[eventType][selector] = handler; }; + +export const getEventTypeFromProps = (eventProps) => { + return eventProps.toLowerCase().slice(2, eventProps.length); +}; diff --git a/vite.config.js b/vite.config.js index fdbb6d5..12d6f48 100644 --- a/vite.config.js +++ b/vite.config.js @@ -20,5 +20,13 @@ export default mergeConfig( setupFiles: "./src/setupTests.js", exclude: ["**/e2e/**", "**/*.e2e.spec.js", "**/node_modules/**"], }, + resolve: { + alias: [ + { + find: "@", + replacement: "/src", + }, + ], + }, }), ); From 7eeca0952f201a7e233dd78b881a619c44a106b6 Mon Sep 17 00:00:00 2001 From: uuyeong Date: Thu, 26 Dec 2024 17:59:09 +0900 Subject: [PATCH 2/9] =?UTF-8?q?fix=20:=20update=20attribute=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=20=EC=B2=98=EB=A6=AC=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 | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib/updateElement.js b/src/lib/updateElement.js index e983b15..c79adcf 100644 --- a/src/lib/updateElement.js +++ b/src/lib/updateElement.js @@ -5,13 +5,12 @@ import { removeEvent } from "./eventManager.js"; function updateAttributes(element, newProps, oldProps) { for (let [key, value] of Object.entries(oldProps)) { - if (key in newProps) return; + if (key in newProps) continue; element.removeAttribute(key); if (key.startsWith("on")) { removeEvent(element, getEventTypeFromProps(key), value); - return element; } } for (let [key, value] of Object.entries(newProps)) { @@ -42,8 +41,7 @@ export function updateElement(parentElement, newNode, oldNode, index = 0) { } //oldNode와 newNode의 태그 이름(type)이 다를 경우 if (newNode.type !== oldNode.type) { - parentElement.removeChild(oldElement); - parentElement.appendChild(createElement(newNode)); + parentElement.replaceChild(createElement(newNode), oldElement); return; } From 9f825336a410ddaccb0666e0704f3026388339e9 Mon Sep 17 00:00:00 2001 From: uuyeong Date: Thu, 26 Dec 2024 19:57:16 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=ED=8F=AC=EC=8A=A4=ED=8C=85=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94,=20=EC=B6=94=EA=B0=80=20=EA=B8=B0?= =?UTF-8?q?=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 | 3 ++ src/components/posts/PostForm.jsx | 3 +- src/pages/HomePage.jsx | 54 +++++++++++++++++++++++++++++-- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/components/posts/Post.jsx b/src/components/posts/Post.jsx index 67af756..95b1a11 100644 --- a/src/components/posts/Post.jsx +++ b/src/components/posts/Post.jsx @@ -7,7 +7,9 @@ export const Post = ({ time, content, likeUsers, + id, activationLike = false, + onUpdateLike, }) => { return (
@@ -20,6 +22,7 @@ export const Post = ({

{content}

좋아요 {likeUsers.length} diff --git a/src/components/posts/PostForm.jsx b/src/components/posts/PostForm.jsx index 36a2513..771e7b2 100644 --- a/src/components/posts/PostForm.jsx +++ b/src/components/posts/PostForm.jsx @@ -1,7 +1,7 @@ /** @jsx createVNode */ import { createVNode } from "../../lib"; -export const PostForm = () => { +export const PostForm = ({ onSubmit }) => { return (