Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions 404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>상품 쇼핑몰</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/src/styles.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: "#3b82f6",
secondary: "#6b7280"
}
}
}
};
</script>
</head>
<body class="bg-gray-50">
<div id="root"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
46 changes: 23 additions & 23 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>상품 쇼핑몰</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/src/styles.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: "#3b82f6",
secondary: "#6b7280"
}
}
}
};
</script>
</head>
<body class="bg-gray-50">
<div id="root"></div>
<script type="module" src="/src/main.js"></script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>상품 쇼핑몰</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/src/styles.css" />
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: "#3b82f6",
secondary: "#6b7280",
},
},
},
};
</script>
</head>
<body class="bg-gray-50">
<div id="root"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
39 changes: 37 additions & 2 deletions src/lib/createElement.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
import { addEvent } from "./eventManager";

export function createElement(vNode) {}
export function createElement(vNode) {
if (vNode === null || vNode === undefined || typeof vNode === "boolean") {
return document.createTextNode("");
}

function updateAttributes($el, props) {}
if (typeof vNode === "string" || typeof vNode === "number") {
return document.createTextNode(String(vNode));
}

if (Array.isArray(vNode)) {
const fragment = document.createDocumentFragment();

vNode.forEach((node) => fragment.appendChild(createElement(node)));

return fragment;
}

const el = document.createElement(vNode.type);
updateAttributes(el, vNode.props ?? {});

el.append(...vNode.children.map(createElement));

return el;
}

function updateAttributes($el, props) {
Object.entries(props).forEach(([attribute, value]) => {
if (/^on[A-Z]/.test(attribute) && typeof value === "function") {
addEvent($el, attribute.toLowerCase().substring(2), value);
} else if (attribute === "className") {
$el.setAttribute("class", value);
} else if (typeof value === "boolean") {
$el[attribute] = value;
} else {
$el.setAttribute(attribute, value);
}
});
}
8 changes: 7 additions & 1 deletion src/lib/createVNode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export function createVNode(type, props, ...children) {
return {};
const flatChildren = children.flat(Infinity).filter((child) => child === 0 || Boolean(child));

return {
type,
props,
children: flatChildren,
};
}
42 changes: 39 additions & 3 deletions src/lib/eventManager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,41 @@
export function setupEventListeners(root) {}
const eventMap = new WeakMap();
const delegatedEvents = new Set();

export function addEvent(element, eventType, handler) {}
export function setupEventListeners(root) {
delegatedEvents.forEach((eventType) => {
root.removeEventListener(eventType, handleDelegatedEvent);
root.addEventListener(eventType, handleDelegatedEvent);
});
}

export function removeEvent(element, eventType, handler) {}
export function addEvent(element, eventType, handler) {
if (!eventMap.has(element)) {
eventMap.set(element, new Map());
}

const elementEvents = eventMap.get(element);
elementEvents.set(eventType, handler);

delegatedEvents.add(eventType);
}

export function removeEvent(element, eventType) {
const elementEvents = eventMap.get(element);
elementEvents.delete(eventType);
}

function handleDelegatedEvent(event) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleDelegatedEvent 함수를 setupEventListeners 내부에 선언하면 다른 테스트 코드는 다 통과되나,
"동적으로 추가된 요소에도 이벤트가 정상적으로 작동해야 한다" 테스트만 통과되지 않습니다. 왜 내부에 선언하지 않고 외부에 선언해야 테스트가 통과되는지 이유가 궁금합니다

let target = event.target;

// NOTE: cancelBubble 속성은 deprecated이므로 다른 방법으로 구현해야 함
while (target && !event.cancelBubble) {
const elementEvents = eventMap.get(target);

if (elementEvents?.has(event.type)) {
const handler = elementEvents.get(event.type);
handler(event);
}

target = target.parentNode;
}
}
17 changes: 16 additions & 1 deletion src/lib/normalizeVNode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
export function normalizeVNode(vNode) {
return vNode;
if (vNode === null || vNode === undefined || typeof vNode === "boolean") {
return "";
}

if (typeof vNode === "string" || typeof vNode === "number") {
return String(vNode);
}

if (typeof vNode.type === "function") {
return normalizeVNode(vNode.type({ ...vNode.props, children: vNode.children }));
}

return {
...vNode,
children: vNode.children.map(normalizeVNode).filter(Boolean),
};
}
16 changes: 16 additions & 0 deletions src/lib/renderElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,24 @@ import { createElement } from "./createElement";
import { normalizeVNode } from "./normalizeVNode";
import { updateElement } from "./updateElement";

const vNodeMap = new WeakMap();

export function renderElement(vNode, container) {
// 최초 렌더링시에는 createElement로 DOM을 생성하고
// 이후에는 updateElement로 기존 DOM을 업데이트한다.
// 렌더링이 완료되면 container에 이벤트를 등록한다.

const oldVNode = vNodeMap.get(container);
const newVnode = normalizeVNode(vNode);

if (!oldVNode) {
const element = createElement(newVnode);
container.appendChild(element);
} else {
updateElement(container, newVnode, oldVNode);
}

vNodeMap.set(container, newVnode);

setupEventListeners(container);
}
111 changes: 109 additions & 2 deletions src/lib/updateElement.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,113 @@
import { addEvent, removeEvent } from "./eventManager";
import { createElement } from "./createElement.js";

function updateAttributes(target, originNewProps, originOldProps) {}
export function updateAttributes(target, originNewProps, originOldProps) {
if (!originNewProps && !originOldProps) return;

export function updateElement(parentElement, newNode, oldNode, index = 0) {}
if (originOldProps) {
Object.entries(originOldProps).forEach(([key, value]) => {
if (key === "children") return;

if (key.startsWith("on")) {
const eventType = key.substring(2).toLowerCase();
removeEvent(target, eventType, originOldProps[key]);
} else if (!originNewProps || !(key in originNewProps)) {
if (key === "className") {
target.removeAttribute("class");
} else if (typeof value === "boolean") {
target[key] = false;
target.removeAttribute(key);
} else {
target.removeAttribute(key);
}
}
});
}

if (originNewProps) {
Object.entries(originNewProps).forEach(([key, value]) => {
if (key === "children") return;

if (key === "className") {
if (value) {
target.setAttribute("class", value);
} else {
target.removeAttribute("class");
}
return;
}

if (key.startsWith("on")) {
const eventType = key.substring(2).toLowerCase();
addEvent(target, eventType, value);
return;
}

if (typeof value === "boolean") {
target[key] = value;
return;
}

if (value != null && (!originOldProps || originOldProps[key] !== value)) {
target.setAttribute(key, String(value));
}
});
}
}

export function updateElement(parentElement, newNode, oldNode, index = 0) {
if (!newNode && oldNode) {
if (parentElement.childNodes[index]) {
parentElement.removeChild(parentElement.childNodes[index]);
}
return;
}

if (newNode && !oldNode) {
parentElement.appendChild(createElement(newNode));
return;
}

if (typeof newNode === "string" || typeof newNode === "number") {
if (newNode !== oldNode) {
const newTextNode = document.createTextNode(String(newNode));
if (parentElement.childNodes[index]) {
parentElement.replaceChild(newTextNode, parentElement.childNodes[index]);
} else {
parentElement.appendChild(newTextNode);
}
}
return;
}

if (newNode.type !== oldNode.type) {
if (parentElement.childNodes[index]) {
parentElement.replaceChild(createElement(newNode), parentElement.childNodes[index]);
} else {
parentElement.appendChild(createElement(newNode));
}
return;
}

const childNode = parentElement.childNodes[index];
if (childNode) {
updateAttributes(childNode, newNode.props || {}, oldNode.props || {});

const newChildren = newNode.children || [];
const oldChildren = oldNode.children || [];
const maxLength = Math.max(newChildren.length, oldChildren.length);

for (let i = 0; i < maxLength; i++) {
updateElement(childNode, newChildren[i], oldChildren[i], i);
}

if (oldChildren.length > newChildren.length) {
for (let i = oldChildren.length - 1; i >= newChildren.length; i--) {
const extraChild = childNode.childNodes[i];
if (extraChild) {
childNode.removeChild(extraChild);
}
}
}
}
}