Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1팀 전명익] [Chapter 1-2] 프레임워크 없이 SPA 만들기 #46

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
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
136 changes: 0 additions & 136 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 43 additions & 2 deletions src/lib/createElement.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,46 @@
import { isValidVNode, isString, isNumber } from "./validCheck";
import { addEvent } from "./eventManager";

export function createElement(vNode) {}
export function createElement(vNode) {
if (!isValidVNode(vNode)) {
return document.createTextNode("");
}

function updateAttributes($el, props) {}
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;
}

if (typeof vNode.type === "function") {
throw new Error("Function Components are not supported.");
}

const $element = document.createElement(vNode.type);

updateAttributes($element, vNode.props ?? {});

$element.append(...vNode.children.map(createElement));

return $element;
}

/**
* DOM 요소에 속성 추가
*/
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);
}
});
}
17 changes: 16 additions & 1 deletion src/lib/createVNode.js
Original file line number Diff line number Diff line change
@@ -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),
};
}
55 changes: 52 additions & 3 deletions src/lib/eventManager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,54 @@
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;

// eventRegistry에서 matchingEvent를 제거 제거
const index = eventRegistry.indexOf(matchingEvent);
eventRegistry.splice(index, 1);

if (matchingEvent !== null) {
rootElement.removeEventListener(eventType, matchingEvent.handler);
}
}
28 changes: 28 additions & 0 deletions src/lib/normalizeVNode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
import { isValidVNode, isString, isNumber } from "./validCheck";

export function normalizeVNode(vNode) {
// null, undefined, falsy 의 경우 빈 텍스트 return
if (!isValidVNode(vNode)) {
return "";
}

// 문자열, 숫자일 경우 string 노드로 return
if (isString(vNode) || isNumber(vNode)) {
return String(vNode);
}

// 함수형 VNode 처리
if (typeof vNode.type === "function") {
const processedVNode = vNode.type({
...vNode.props,
children: vNode.children,
});
return normalizeVNode(processedVNode);
}

// children(자식요소들) 필터링
if (Array.isArray(vNode.children)) {
vNode.children = vNode.children
.map(normalizeVNode) // children을 재귀적으로 정규화
.filter((child) => isValidVNode(child) && child !== ""); // 유효한 children을 필터링
}

return vNode;
}
13 changes: 13 additions & 0 deletions src/lib/renderElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,21 @@ 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에 이벤트를 등록한다.

if (!container.firstChild) {
oldVNode = normalizeVNode(vNode);
container.appendChild(createElement(oldVNode));
} else {
const newVNode = normalizeVNode(vNode);
updateElement(container, newVNode, oldVNode, 0);
oldVNode = newVNode;
}

setupEventListeners(container);
}
Loading
Loading