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

[10팀 이정은] [Chapter 1-2] 프레임워크 없이 SPA 만들기 - 작성중 #56

Open
wants to merge 4 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
46 changes: 44 additions & 2 deletions src/lib/createElement.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
import { addEvent } from "./eventManager";

export function createElement(vNode) {}
export function createElement(vNode) {
//1. 기본값 처리 (null, undefined, boolean)
if (vNode === null || vNode === undefined || typeof vNode === "boolean") {
return document.createTextNode("");
}

function updateAttributes($el, props) {}
//2. 문자열이나 숫자 처리
if (typeof vNode === "string" || typeof vNode === "number") {
return document.createTextNode(vNode);
}

//3. 배열 처리
if (Array.isArray(vNode)) {
const fragment = document.createDocumentFragment();
vNode.forEach((child) => {
fragment.appendChild(createElement(child));
});

return fragment;
}

//4. 객체 처리(Babel이 JSX를 변환한 결과로 객체가 들어오기 때문에)
const $el = document.createElement(vNode.type);
//5. props 처리. 여기 아직 이해 다 못함
if (vNode.props) {
Object.entries(vNode.props).forEach(([key, value]) => {
if (key === "className") {
$el.setAttribute("class", value);
} else if (key.startsWith("on")) {
addEvent($el, key.toLowerCase().slice(2), value);
} else {
$el.setAttribute(key, value);
}
});
}
Comment on lines +27 to +37

Choose a reason for hiding this comment

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

updateAttributes 함수에 넣어서 속성 처리를 해줄 수 있을 것 같습니다!
(사실.. 저도 템플릿에 있길래 함수에 넣어서 구현한거라 좋은지, 안좋은지 잘은 모르겠지만..)

// 2. 자식 요소들을 순회하면서 재귀적으로 처리
vNode.children.forEach((child) => {
// 각 자식 요소도 createElement를 통해 실제 DOM 요소로 변환
// 변환된 자식 요소를 부모 요소에 추가
$el.appendChild(createElement(child));
});
return $el;
}

// function updateAttributes($el, props) {}
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);
//1. children이 조건부 (예를 들어 true && <div>Shown</div>)이면 true일 때의 children만 return
//2. children에 null, undefined가 있으면 제거하고 return
const filteredChildren = flatChildren.filter(
(child) => child !== null && child !== undefined && child !== false,
);
return { type, props, children: filteredChildren };
}
69 changes: 66 additions & 3 deletions src/lib/eventManager.js

Choose a reason for hiding this comment

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

예상되는 리스너들을 미리 등록해놓고 구현하신 부분이 이런 방법도 있구나 알게되었습니다!!

Original file line number Diff line number Diff line change
@@ -1,5 +1,68 @@
export function setupEventListeners(root) {}
//TODO: 여기 코드 다시 살펴보기

export function addEvent(element, eventType, handler) {}
// 전역 이벤트 맵
const eventMap = new WeakMap();
// 이벤트 리스너가 설정된 요소들을 추적
const initializedElements = new WeakSet();

export function removeEvent(element, eventType, handler) {}
export function setupEventListeners(root) {
// 이미 초기화된 요소라면 다시 설정하지 않음
if (initializedElements.has(root)) {
return;
}

const eventHandler = (event) => {
let target = event.target;

while (target && target !== root) {
const handlers = eventMap.get(target);
if (handlers && handlers[event.type]) {
handlers[event.type](event);
if (event.cancelBubble) break;
}
target = target.parentNode;
}
};

// 이벤트 리스너 등록
root.addEventListener("click", eventHandler);
root.addEventListener("input", eventHandler);
root.addEventListener("change", eventHandler);
root.addEventListener("focus", eventHandler);
root.addEventListener("blur", eventHandler);
root.addEventListener("keydown", eventHandler);
root.addEventListener("keyup", eventHandler);
root.addEventListener("keypress", eventHandler);
root.addEventListener("mouseover", eventHandler);
root.addEventListener("mouseout", eventHandler);
root.addEventListener("mousemove", eventHandler);
root.addEventListener("mouseup", eventHandler);
root.addEventListener("mousedown", eventHandler);

Comment on lines +28 to +40
Copy link

Choose a reason for hiding this comment

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

eventType을 가져와서 활용하시는 것도 좋았을 것 같아요! 아마 WeakMap을 활용하시려고 이 방법을 택하신거겠죠? WeakMap은 색인이 어려워서 루프 도는게 안되서 이 방법말고는 저도 따로 생각나는게 없더라구요. 그래서 Map으로 변경했습니다 ㅎㅎ..

// 초기화된 요소로 표시
initializedElements.add(root);
}

export function addEvent(element, eventType, handler) {
if (!eventMap.has(element)) {
eventMap.set(element, {});
}
const handlers = eventMap.get(element);
handlers[eventType] = handler;
}

export function removeEvent(element, eventType) {
if (eventMap.has(element)) {
const handlers = eventMap.get(element);
delete handlers[eventType];

if (Object.keys(handlers).length === 0) {
eventMap.delete(element);
initializedElements.delete(element);
}
}
}

export function cleanupEventListeners(root) {
initializedElements.delete(root);
}
32 changes: 31 additions & 1 deletion src/lib/normalizeVNode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
export function normalizeVNode(vNode) {
return vNode;
if (
vNode === null ||
vNode === undefined ||
vNode === true ||
vNode === false
) {
return "";
}
//문자열과 숫자는 문자열로 변환되어야 한다
if (typeof vNode === "string" || typeof vNode === "number") {
return vNode.toString();
}
//컴포넌트 정규화
if (typeof vNode.type === "function") {
const result = vNode.type({ ...vNode.props, children: vNode.children });
return normalizeVNode(result);
}
//Falsy값(null, undefined, false)은 자식 노드에서 제거되어야 한다.
const filteredChildren = vNode.children.filter(
(child) =>
child !== null &&
child !== undefined &&
child !== false &&
child !== true, // 이것 까지 추가하면 Falsy값이 아니지 않나? boolean을 따로 처리해줘야하나?
Comment on lines +24 to +25
Copy link

Choose a reason for hiding this comment

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

이부분은 null을 제외한 나머지를 typeof로 변환하시면 조금 더 깔끔하게 작성하실 수 있을거에요

);

return {
type: vNode.type,
props: vNode.props,
children: filteredChildren.map((child) => normalizeVNode(child)),
};
}
15 changes: 12 additions & 3 deletions src/lib/renderElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@ import { normalizeVNode } from "./normalizeVNode";
import { updateElement } from "./updateElement";

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

// 기존 요소가 있다면 업데이트하고, 없다면 새로 생성
if (container.firstChild) {
updateElement(container.firstChild, normalizedNode);
return container.firstChild;
} else {
const element = createElement(normalizedNode);
container.appendChild(element);
setupEventListeners(container);
return element;
}
}
75 changes: 71 additions & 4 deletions src/lib/updateElement.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,73 @@
import { addEvent, removeEvent } from "./eventManager";
import { createElement } from "./createElement.js";
import { createElement } from "./createElement";
import { setupEventListeners } from "./eventManager";

function updateAttributes(target, originNewProps, originOldProps) {}
export function updateElement(oldElement, newVNode) {
// 타입이 다르면 전체 교체
if (oldElement.nodeName.toLowerCase() !== newVNode.type?.toLowerCase()) {
const newElement = createElement(newVNode);
oldElement.parentNode.replaceChild(newElement, oldElement);
setupEventListeners(newElement.parentNode);
return newElement;
}

export function updateElement(parentElement, newNode, oldNode, index = 0) {}
// 텍스트 노드 업데이트
if (typeof newVNode.children === "string") {
if (oldElement.textContent !== newVNode.children) {
oldElement.textContent = newVNode.children;
}
return oldElement;
}

// props 업데이트
const oldProps = Array.from(oldElement.attributes).reduce((props, attr) => {
props[attr.name] = attr.value;
return props;
}, {});

// 이전 props 제거
Object.keys(oldProps).forEach((name) => {
if (!(name in newVNode.props)) {
if (name.startsWith("on")) {
// 이벤트 핸들러 제거
const eventName = name.toLowerCase().slice(2); // 'onClick' -> 'click'
oldElement.removeAttribute(name);
oldElement[`_${eventName}Handler`] = null; // 저장된 핸들러 제거
} else {
oldElement.removeAttribute(name);
}
}
});

// 새로운 props 설정
Object.entries(newVNode.props || {}).forEach(([name, value]) => {
if (oldProps[name] !== value) {
if (name.startsWith("on")) {
// 이벤트 핸들러는 eventManager에서 처리하도록 속성만 설정
oldElement.setAttribute(name.toLowerCase(), "");
} else {
oldElement.setAttribute(name, value);
}
}
Copy link

Choose a reason for hiding this comment

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

className => class로 변경하는 부분이 빠진 것 같아요!

});

// 자식 요소 재귀적 업데이트
const oldChildren = Array.from(oldElement.childNodes);
const newChildren = Array.isArray(newVNode.children) ? newVNode.children : [];

for (let i = 0; i < Math.max(oldChildren.length, newChildren.length); i++) {
if (!oldChildren[i] && newChildren[i]) {
// 새로운 자식 추가
oldElement.appendChild(createElement(newChildren[i]));
} else if (!newChildren[i]) {
// 더 이상 필요없는 자식 제거
oldElement.removeChild(oldChildren[i]);
} else {
// 기존 자식 업데이트
updateElement(oldChildren[i], newChildren[i]);
}
}

// 이벤트 리스너 재설정

return oldElement;
}
Loading