-
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
[3팀 허원영] [Chapter 1-2] 프레임워크 없이 SPA 만들기 #48
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,53 @@ | ||
import { addEvent } from "./eventManager"; | ||
|
||
export function createElement(vNode) {} | ||
/** | ||
* @typedef {Object} VNode | ||
* @property {string|function} type - 요소의 타입 | ||
* @property {Object | null} props - 요소의 속성 | ||
* @property {Array<*>} children - 요소의 자식 노드 | ||
*/ | ||
|
||
function updateAttributes($el, props) {} | ||
export function createElement(vNode) { | ||
if ( | ||
Comment on lines
+3
to
+11
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. 원영님 코드를 보면 코드 자체도 읽기 편한데 주석도 굉장히 잘 작성되어 있네요...!👍 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. 감사합니다 ㅎㅎ |
||
vNode === null || | ||
typeof vNode === "undefined" || | ||
typeof vNode === "boolean" | ||
) { | ||
return document.createTextNode(""); | ||
} | ||
|
||
if (typeof vNode === "string" || typeof vNode === "number") { | ||
return document.createTextNode(vNode); | ||
} | ||
|
||
// vNode가 배열이면 DocumentFragment를 생성하고 각 자식에 대해 createElement를 재귀 호출하여 추가합니다. | ||
if (vNode instanceof Array) { | ||
const fragment = document.createDocumentFragment(); | ||
vNode.forEach((child) => { | ||
fragment.appendChild(createElement(child)); | ||
}); | ||
return fragment; | ||
} | ||
|
||
const $el = document.createElement(vNode.type); | ||
updateAttributes($el, vNode.props); | ||
vNode.children.forEach((child) => { | ||
$el.appendChild(createElement(child)); | ||
}); | ||
|
||
return $el; | ||
} | ||
function updateAttributes($el, props) { | ||
if (props === null) return; | ||
|
||
Object.entries(props).forEach(([key, value]) => { | ||
if (key === "className") { | ||
$el.setAttribute("class", value); | ||
} else if (key.startsWith("on") && typeof value === "function") { | ||
const eventType = key.toLowerCase().substring(2); | ||
addEvent($el, eventType, value); | ||
} else { | ||
$el.setAttribute(key, value); | ||
} | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,26 @@ | ||
export function createVNode(type, props, ...children) { | ||
return {}; | ||
const flatChildren = children | ||
.flatMap((child) => (child instanceof Array ? child.flat() : child)) | ||
.filter( | ||
(child) => | ||
child !== false && | ||
child !== null && | ||
child !== undefined && | ||
child !== "" && | ||
child !== true, | ||
); | ||
|
||
return { | ||
type, | ||
props, | ||
children: flatChildren, | ||
}; | ||
} | ||
|
||
// <div id="test" className="old"> | ||
// Hello | ||
// </div> | ||
|
||
// const test = createVNode("div", { id: "test", className: "old" }, "Hello"); | ||
|
||
// console.log(test); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,55 @@ | ||
export function setupEventListeners(root) {} | ||
/** | ||
* idea | ||
* 이벤트가 걸려있는 요소를 저장하여 위임된 이벤트를 처리한다. | ||
*/ | ||
|
||
export function addEvent(element, eventType, handler) {} | ||
/** | ||
* eventHandlers Map | ||
* [eventType: WeakMap | ||
* element: handler | ||
* ] | ||
*/ | ||
|
||
export function removeEvent(element, eventType, handler) {} | ||
export const eventMap = new Map(); | ||
function handleEvent(e) { | ||
const eventType = e.type; | ||
const elementEventMap = eventMap.get(eventType); | ||
|
||
if (!elementEventMap || !elementEventMap.has(e.target)) { | ||
return; | ||
} | ||
const handler = elementEventMap.get(e.target); | ||
handler(e); | ||
} | ||
|
||
export function setupEventListeners(root) { | ||
if (!eventMap.size) { | ||
return; | ||
} | ||
|
||
eventMap.forEach((elementEventMap, eventType) => { | ||
root.addEventListener(eventType, handleEvent); | ||
}); | ||
} | ||
|
||
export function addEvent(element, eventType, handler) { | ||
if (!eventMap.has(eventType)) { | ||
eventMap.set(eventType, new WeakMap()); | ||
} | ||
|
||
const elementEventMap = eventMap.get(eventType); | ||
elementEventMap.set(element, handler); | ||
} | ||
|
||
export function removeEvent(element, eventType) { | ||
const elementEventMap = eventMap.get(eventType); | ||
if (!elementEventMap || !elementEventMap.has(element)) { | ||
return; | ||
} | ||
|
||
elementEventMap.delete(element); | ||
|
||
if (elementEventMap.size === 0) { | ||
eventMap.delete(eventType); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,25 @@ | ||
export function normalizeVNode(vNode) { | ||
return vNode; | ||
if ( | ||
vNode === null || | ||
typeof vNode === "undefined" || | ||
typeof vNode === "boolean" | ||
) { | ||
return ""; | ||
} | ||
|
||
if (typeof vNode === "string" || typeof vNode === "number") { | ||
return vNode.toString(); | ||
} | ||
|
||
if (typeof vNode.type === "function") { | ||
const temp = vNode.type({ children: vNode.children, ...vNode.props }); | ||
return normalizeVNode(temp); | ||
} | ||
|
||
const children = vNode.children.map((child) => normalizeVNode(child)); | ||
|
||
return { | ||
...vNode, | ||
children, | ||
}; | ||
} |
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 |
---|---|---|
|
@@ -2,9 +2,18 @@ import { setupEventListeners } from "./eventManager"; | |
import { createElement } from "./createElement"; | ||
import { normalizeVNode } from "./normalizeVNode"; | ||
import { updateElement } from "./updateElement"; | ||
|
||
export function renderElement(vNode, container) { | ||
// 최초 렌더링시에는 createElement로 DOM을 생성하고 | ||
// 이후에는 updateElement로 기존 DOM을 업데이트한다. | ||
// 렌더링이 완료되면 container에 이벤트를 등록한다. | ||
if (!container._oldVNode) { | ||
container._oldVNode = null; | ||
} | ||
|
||
const normalizedVNode = normalizeVNode(vNode); | ||
if (!container._oldVNode) { | ||
container.appendChild(createElement(normalizedVNode)); | ||
} else { | ||
updateElement(container, normalizedVNode, container._oldVNode); | ||
} | ||
|
||
setupEventListeners(container); | ||
container._oldVNode = normalizedVNode; | ||
Comment on lines
+6
to
+18
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. 저는 변수를 따로 만들어둔 다음에 oldVNode를 저장해뒀는데, container에 _oldVNode를 프로터피 바인딩을 사용하는 방식이 더 깔끔해보이는 것 같아요! |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,82 @@ | ||
import { addEvent, removeEvent } from "./eventManager"; | ||
import { createElement } from "./createElement.js"; | ||
|
||
function updateAttributes(target, originNewProps, originOldProps) {} | ||
function updateAttributes(target, originNewProps, originOldProps) { | ||
const newProps = originNewProps || {}; | ||
const oldProps = originOldProps || {}; | ||
|
||
export function updateElement(parentElement, newNode, oldNode, index = 0) {} | ||
for (const key in newProps) { | ||
if (oldProps[key] !== newProps[key]) { | ||
if (key === "className") { | ||
target.setAttribute("class", newProps[key]); | ||
} else if (key.startsWith("on") && typeof newProps[key] === "function") { | ||
const eventName = key.toLowerCase().substring(2); | ||
// removeEvent(target, eventName); | ||
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. 저는 주석은 항상 지워두는 편인데, 예전에 다니던 회사에서는 남겨두는 경우도 많이 본 것 같아요..! 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. 사실 저런 주석은 지우는게 맞는거 같아요 ㅋㅋ 코치님도 사용하지 않는 주석을 지우라고 피드백 주셨더라구요... |
||
addEvent(target, eventName, newProps[key]); | ||
} else { | ||
target.setAttribute(key, newProps[key]); | ||
} | ||
} | ||
} | ||
|
||
// a만 있고 b에는 없는 속성 제거 | ||
for (const key in oldProps) { | ||
if (!(key in newProps)) { | ||
if (key === "className") { | ||
target.removeAttribute("class"); | ||
} else if (key.startsWith("on") && typeof oldProps[key] === "function") { | ||
const eventName = key.toLowerCase().substring(2); | ||
removeEvent(target, eventName); | ||
} else { | ||
target.removeAttribute(key); | ||
} | ||
} | ||
} | ||
} | ||
|
||
export function updateElement(parentElement, newNode, oldNode, index = 0) { | ||
if (!newNode) { | ||
parentElement.removeChild(parentElement.children[index]); | ||
return; | ||
} | ||
if (!oldNode) { | ||
parentElement.appendChild(createElement(newNode)); | ||
return; | ||
} | ||
|
||
if (typeof newNode === "string" || typeof newNode === "number") { | ||
if (newNode !== oldNode) { | ||
parentElement.childNodes[index].nodeValue = newNode; // 변경된 코드 | ||
} | ||
return; | ||
} | ||
|
||
if (newNode.type !== oldNode.type) { | ||
parentElement.replaceChild( | ||
createElement(newNode), | ||
parentElement.children[index], | ||
); | ||
return; | ||
} | ||
|
||
updateAttributes(parentElement.children[index], newNode.props, oldNode.props); | ||
|
||
const newChildren = newNode.children || []; | ||
const oldChildren = oldNode.children || []; | ||
|
||
for (let i = 0; i < newChildren.length; i++) { | ||
updateElement( | ||
parentElement.children[index], | ||
newChildren[i], | ||
oldChildren[i], | ||
i, | ||
); | ||
} | ||
if (oldChildren.length > newChildren.length) { | ||
for (let i = newChildren.length; i < oldChildren.length; i++) { | ||
parentElement.children[index].removeChild( | ||
parentElement.children[index].children[newChildren.length], | ||
); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,5 +53,26 @@ export const globalStore = createStore( | |
userStorage.reset(); | ||
return { ...state, currentUser: null, loggedIn: false }; | ||
}, | ||
likePost(state, postId) { | ||
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. 이번주 수고하셨습니다. 좋아요 기능을 여기서 관리하는게 깔끔하군요 |
||
const post = state.posts.find((post) => post.id === postId); | ||
if (post.likeUsers.includes(state.currentUser.username)) { | ||
post.likeUsers = post.likeUsers.filter( | ||
(username) => username !== state.currentUser.username, | ||
); | ||
} else { | ||
post.likeUsers.push(state.currentUser.username); | ||
} | ||
return { ...state, posts: [...state.posts] }; | ||
}, | ||
addPost(state, content) { | ||
const newPost = { | ||
id: state.posts.length + 1, | ||
author: state.currentUser.username, | ||
time: Date.now(), | ||
content, | ||
likeUsers: [], | ||
}; | ||
return { ...state, posts: [newPost, ...state.posts] }; | ||
}, | ||
}, | ||
); |
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.
createStore에 actions도 따로 정의가 되어 있었군요! 🤩