-
Notifications
You must be signed in to change notification settings - Fork 65
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
} | ||
}); | ||
} | ||
// 2. 자식 요소들을 순회하면서 재귀적으로 처리 | ||
vNode.children.forEach((child) => { | ||
// 각 자식 요소도 createElement를 통해 실제 DOM 요소로 변환 | ||
// 변환된 자식 요소를 부모 요소에 추가 | ||
$el.appendChild(createElement(child)); | ||
}); | ||
return $el; | ||
} | ||
|
||
// function updateAttributes($el, props) {} |
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 }; | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)), | ||
}; | ||
} |
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); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updateAttributes 함수에 넣어서 속성 처리를 해줄 수 있을 것 같습니다!
(사실.. 저도 템플릿에 있길래 함수에 넣어서 구현한거라 좋은지, 안좋은지 잘은 모르겠지만..)