-
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
[2팀 노정우] [Chapter 1-2] 프레임워크 없이 SPA 만들기 Part 2 #6
base: main
Are you sure you want to change the base?
Changes from all commits
f3459ea
a72ddc0
793b227
4b6883e
b37568b
2fed4f2
b8da73b
bd13e3d
a10d3f0
eb16bf2
e3f68cc
06f124b
d165e44
345e995
77fd3d9
3792281
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 |
---|---|---|
@@ -0,0 +1,91 @@ | ||
## 과제 체크포인트 | ||
|
||
### 기본과제 | ||
|
||
#### 가상돔을 기반으로 렌더링하기 | ||
|
||
- [x] createVNode 함수를 이용하여 vNode를 만든다. | ||
- [x] normalizeVNode 함수를 이용하여 vNode를 정규화한다. | ||
- [x] createElement 함수를 이용하여 vNode를 실제 DOM으로 만든다. | ||
- [x] updateElement 함수를 이용하여 vNode를 실제 DOM에 업데이트. | ||
- [x] 결과적으로, JSX를 실제 DOM으로 변환할 수 있도록 만들었다. | ||
|
||
#### 이벤트 위임 | ||
|
||
- [x] 노드를 생성할 때 이벤트를 직접 등록하는게 아니라 이벤트 위임 방식으로 등록해야 한다 | ||
- [x] 동적으로 추가된 요소에도 이벤트가 정상적으로 작동해야 한다 | ||
- [x] 이벤트 핸들러가 제거되면 더 이상 호출되지 않아야 한다 | ||
|
||
### 심화 과제 | ||
|
||
#### 1) Diff 알고리즘 구현 | ||
|
||
- [x] 초기 렌더링이 올바르게 수행되어야 한다 | ||
- [x] diff 알고리즘을 통해 변경된 부분만 업데이트해야 한다 | ||
- [x] 새로운 요소를 추가하고 불필요한 요소를 제거해야 한다 | ||
- [x] 요소의 속성만 변경되었을 때 요소를 재사용해야 한다 | ||
- [x] 요소의 타입이 변경되었을 때 새로운 요소를 생성해야 한다 | ||
|
||
#### 2) 포스트 추가/좋아요 기능 구현 | ||
|
||
- [ ] 비사용자는 포스트 작성 폼이 보이지 않는다 | ||
- [ ] 비사용자는 포스트에 좋아요를 클릭할 경우, 경고 메세지가 발생한다. | ||
- [ ] 사용자는 포스트 작성 폼이 보인다. | ||
- [ ] 사용자는 포스트를 추가할 수 있다. | ||
- [ ] 사용자는 포스트에 좋아요를 클릭할 경우, 좋아요가 토글된다. | ||
|
||
## 과제 셀프회고 | ||
|
||
<!-- 과제에 대한 회고를 작성해주세요 --> | ||
|
||
### 기술적 성장 | ||
|
||
<!-- 예시 | ||
- 새로 학습한 개념 | ||
- 기존 지식의 재발견/심화 | ||
- 구현 과정에서의 기술적 도전과 해결 | ||
--> | ||
|
||
### 코드 품질 | ||
|
||
<!-- 예시 | ||
- 특히 만족스러운 구현 | ||
- 리팩토링이 필요한 부분 | ||
- 코드 설계 관련 고민과 결정 | ||
--> | ||
|
||
### 학습 효과 분석 | ||
|
||
<!-- 예시 | ||
- 가장 큰 배움이 있었던 부분 | ||
- 추가 학습이 필요한 영역 | ||
- 실무 적용 가능성 | ||
--> | ||
|
||
### 과제 피드백 | ||
|
||
<!-- 예시 | ||
- 과제에서 모호하거나 애매했던 부분 | ||
- 과제에서 좋았던 부분 | ||
--> | ||
|
||
## 리뷰 받고 싶은 내용 | ||
|
||
<!-- | ||
피드백 받고 싶은 내용을 구체적으로 남겨주세요 | ||
모호한 요청은 피드백을 남기기 어렵습니다. | ||
|
||
참고링크: https://chatgpt.com/share/675b6129-515c-8001-ba72-39d0fa4c7b62 | ||
|
||
모호한 요청의 예시) | ||
- 코드 스타일에 대한 피드백 부탁드립니다. | ||
- 코드 구조에 대한 피드백 부탁드립니다. | ||
- 개념적인 오류에 대한 피드백 부탁드립니다. | ||
- 추가 구현이 필요한 부분에 대한 피드백 부탁드립니다. | ||
|
||
구체적인 요청의 예시) | ||
- 현재 함수와 변수명을 보면 직관성이 떨어지는 것 같습니다. 함수와 변수를 더 명확하게 이름 지을 수 있는 방법에 대해 조언해주실 수 있나요? | ||
- 현재 파일 단위로 코드가 분리되어 있지만, 모듈화나 계층화가 부족한 것 같습니다. 어떤 기준으로 클래스를 분리하거나 모듈화를 진행하면 유지보수에 도움이 될까요? | ||
- MVC 패턴을 따르려고 했는데, 제가 구현한 구조가 MVC 원칙에 맞게 잘 구성되었는지 검토해주시고, 보완할 부분을 제안해주실 수 있을까요? | ||
- 컴포넌트 간의 의존성이 높아져서 테스트하기 어려운 상황입니다. 의존성을 낮추고 테스트 가능성을 높이는 구조 개선 방안이 있을까요? | ||
--> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,58 @@ | ||
import { normalizeVNode } from "./normalizeVNode"; | ||
import { addEvent } from "./eventManager"; | ||
|
||
export function createElement(vNode) {} | ||
export function createElement(vNode) { | ||
// 컴포넌트 타입 체크 (정규화 전에 먼저 체크) | ||
if (vNode && typeof vNode === "object" && typeof vNode.type === "function") { | ||
throw new Error("Component should be normalized before createElement"); | ||
} | ||
|
||
function updateAttributes($el, props) {} | ||
// vNode 정규화 | ||
vNode = normalizeVNode(vNode); | ||
|
||
// 배열 처리 | ||
if (Array.isArray(vNode)) { | ||
const fragment = document.createDocumentFragment(); | ||
vNode.forEach((child) => { | ||
fragment.appendChild(createElement(child)); | ||
}); | ||
return fragment; | ||
} | ||
|
||
// 문자열이나 숫자인 경우 텍스트 노드 생성 | ||
if (typeof vNode === "string" || typeof vNode === "number") { | ||
return document.createTextNode(vNode); | ||
} | ||
|
||
// HTML 요소 생성 | ||
const $el = document.createElement(vNode.type); | ||
|
||
// 속성 업데이트 | ||
updateAttributes($el, vNode.props); | ||
|
||
// 이벤트 속성 처리 | ||
Object.entries(vNode.props || {}).forEach(([key, value]) => { | ||
if (key.startsWith("on")) { | ||
addEvent($el, key.toLowerCase().slice(2), value); | ||
} | ||
}); | ||
|
||
// 자식 요소들을 재귀적으로 생성하고 추가 | ||
vNode.children?.forEach((child) => { | ||
$el.appendChild(createElement(child)); | ||
}); | ||
|
||
return $el; | ||
} | ||
|
||
function updateAttributes($el, props = {}) { | ||
Object.entries(props || {}).forEach(([key, value]) => { | ||
if (key === "className") { | ||
$el.setAttribute("class", value); | ||
} else if (key === "htmlFor") { | ||
$el.setAttribute("for", value); | ||
Comment on lines
+52
to
+53
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. 디테일이네요.. htmlFor일 경우 for로 대체해주어야 하는군요 ! 궁금해서 블로그도 찾아봤네요 :) 👍 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. 오.. 감사합니다 |
||
} else if (!key.startsWith("on")) { | ||
$el.setAttribute(key, value); | ||
} | ||
}); | ||
} |
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 !== null && child !== undefined && child !== false, | ||
); // falsy 값 제거 | ||
|
||
return { type, props, children: flatChildren }; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,83 @@ | ||
export function setupEventListeners(root) {} | ||
// 이벤트 타입별 핸들러 저장 | ||
const eventHandlers = new Map(); | ||
let rootElement = null; | ||
|
||
export function addEvent(element, eventType, handler) {} | ||
function delegateEvent(event) { | ||
const eventType = event.type; | ||
|
||
export function removeEvent(element, eventType, handler) {} | ||
const handlers = eventHandlers.get(eventType); | ||
if (!handlers) return; | ||
|
||
let target = event.target; | ||
while (target && target !== rootElement?.parentElement) { | ||
const elementHandlers = handlers.get(target); | ||
if (elementHandlers) { | ||
elementHandlers.forEach((handler) => { | ||
handler.call(target, event); | ||
}); | ||
if (!event.bubbles) break; | ||
} | ||
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. 오.. bubbles 프로퍼티를 사용하면 이렇게 버블링하지 않을 때 처리를 할 수 있군요!...👍 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. 저도 몰랐습니다ㅎㅎ 감사합니다 |
||
target = target.parentElement; | ||
} | ||
} | ||
|
||
export function setupEventListeners(root) { | ||
// 이전 root의 이벤트 리스너 제거 | ||
if (rootElement && rootElement !== root) { | ||
eventHandlers.forEach((_, eventType) => { | ||
rootElement.removeEventListener(eventType, delegateEvent); | ||
}); | ||
} | ||
|
||
// 새로운 root 설정 | ||
if (rootElement !== root) { | ||
rootElement = root; | ||
// 기존에 등록된 이벤트 타입들에 대해 root에 이벤트 리스너 등록 | ||
eventHandlers.forEach((_, eventType) => { | ||
rootElement.addEventListener(eventType, delegateEvent); | ||
}); | ||
} | ||
} | ||
|
||
export function addEvent(element, eventType, handler) { | ||
// 이벤트 타입에 대한 Map이 없으면 생성 | ||
if (!eventHandlers.has(eventType)) { | ||
eventHandlers.set(eventType, new WeakMap()); | ||
// root 엘리먼트에 이벤트 리스너 등록 | ||
if (rootElement) { | ||
rootElement.addEventListener(eventType, delegateEvent); | ||
} | ||
} | ||
|
||
// 엘리먼트의 핸들러 Set이 없으면 생성 | ||
const handlers = eventHandlers.get(eventType); | ||
if (!handlers.has(element)) { | ||
handlers.set(element, new Set()); | ||
} | ||
|
||
// 핸들러 추가 | ||
handlers.get(element).add(handler); | ||
} | ||
|
||
export function removeEvent(element, eventType, handler) { | ||
const handlers = eventHandlers.get(eventType); | ||
if (!handlers) return; | ||
|
||
const elementHandlers = handlers.get(element); | ||
if (!elementHandlers) return; | ||
|
||
// 핸들러 제거 | ||
elementHandlers.delete(handler); | ||
|
||
// 엘리먼트의 모든 핸들러가 제거되면 Map에서 제거 | ||
if (elementHandlers.size === 0) { | ||
handlers.delete(element); | ||
} | ||
|
||
// 해당 이벤트 타입의 모든 핸들러가 제거되었는지 확인 | ||
const handlersMap = eventHandlers.get(eventType); | ||
if (!handlersMap || handlersMap.size === 0) { | ||
rootElement?.removeEventListener(eventType, delegateEvent); | ||
eventHandlers.delete(eventType); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,46 @@ | ||
export function normalizeVNode(vNode) { | ||
// 1. null, undefined, boolean 처리 | ||
if (vNode == null || typeof vNode === "boolean") { | ||
return ""; | ||
} | ||
|
||
// 2. 문자열 또는 숫자 처리 | ||
if (typeof vNode === "string" || typeof vNode === "number") { | ||
return String(vNode); | ||
} | ||
|
||
// 3. 배열 처리 | ||
if (Array.isArray(vNode)) { | ||
return vNode | ||
.map((child) => normalizeVNode(child)) | ||
.filter((child) => child !== "" && child != null && child !== false); | ||
} | ||
|
||
// 4. 함수형 컴포넌트 처리 | ||
if (typeof vNode.type === "function") { | ||
return normalizeVNode( | ||
vNode.type({ | ||
...vNode.props, | ||
children: vNode.children, | ||
}), | ||
); | ||
} | ||
|
||
// 5. 객체(vNode)가 아닌 경우 | ||
if (typeof vNode !== "object") { | ||
return ""; | ||
} | ||
|
||
// 6. children 배열처리 | ||
if (Array.isArray(vNode.children)) { | ||
const normalizedChildren = vNode.children | ||
.map((child) => normalizeVNode(child)) | ||
.filter((child) => child !== "" && child != null && child !== false); | ||
|
||
return { | ||
...vNode, | ||
children: normalizedChildren, | ||
}; | ||
} | ||
return vNode; | ||
} |
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.
activationLike를 Post 내부에서 선언해주셨네요 👍
관심사 분리의 목적이었을까요? 의견이 궁금합니다 !