[8팀 현지수] Chapter 1-2. 프레임워크 없이 SPA 만들기#47
Conversation
| // 4. Boolean 속성 (checked, disabled, selected, readOnly) | ||
| if (typeof value === "boolean") { | ||
| if (key === "checked" || key === "selected") { | ||
| // JavaScript 속성만 사용 (true/false 모두 처리) | ||
| $el[key] = value; | ||
| } else if (key === "readOnly") { | ||
| // readOnly → readonly 변환 | ||
| if (value) { | ||
| $el.setAttribute("readonly", ""); | ||
| $el.readOnly = true; | ||
| } else { | ||
| $el.removeAttribute("readonly"); | ||
| $el.readOnly = false; | ||
| } | ||
| } else { | ||
| // 일반 Boolean 속성 (disabled, hidden 등) | ||
| if (value) { | ||
| $el.setAttribute(key, ""); | ||
| if (key in $el) { | ||
| $el[key] = true; | ||
| } | ||
| } else { | ||
| $el.removeAttribute(key); | ||
| if (key in $el) { | ||
| $el[key] = false; | ||
| } | ||
| } | ||
| } | ||
| return; | ||
| } |
There was a problem hiding this comment.
updateAttributes에서 Boolean 속성을 처리할 때 checked/selected는 Property만, readOnly는 Attribute+Property, 일반 Boolean은 둘 다 설정하는 방식으로 구현했는데 더 일관성 있는 처리 방법이 있을까요?
There was a problem hiding this comment.
이런식으로 테이블을 만든다음에
const booleanAttrConfig = {
checked: { attr: false, prop: true },
selected: { attr: false, prop: true },
readOnly: { attr: "readonly", prop: "readOnly" },
disabled: { attr: true, prop: true },
hidden: { attr: true, prop: true },
required: { attr: true, prop: true },
// 필요 시 추가
};if (typeof value === "boolean" && key in booleanAttrConfig) {
const config = booleanAttrConfig[key];
// HTML attribute 처리
const attrName = config.attr === true ? key : config.attr;
if (attrName) {
if (value) {
$el.setAttribute(attrName, "");
} else {
$el.removeAttribute(attrName);
}
}
// JavaScript property 처리
const propName = config.prop === true ? key : config.prop;
if (propName && propName in $el) {
$el[propName] = value;
}
return;
}이렇게 일괄처리하는 방법도 있다고 하네요
There was a problem hiding this comment.
@devchangjun 오 좋은 방법이네요! 감사해요~!!
There was a problem hiding this comment.
이벤트 위임 패턴 + 3단계 저장소 구조로 설계했습니다.
- 중앙 집중식 관리: 루트 요소에만 리스너를 등록하고 모든 하위 이벤트를 처리
- 버블링 탐색: event.target부터 시작해서 부모로 올라가며 핸들러 검색
- 3단계 저장소: element → eventType → handlers Set 구조
- 메모리 효율성: Set으로 중복 방지 + 빈 구조 자동 정리
동작 흐름
루트 이벤트 발생
→ handleDelegatedEvent 호출
→ event.target부터 부모로 버블링 탐색
→ eventMap에서 핸들러 찾기
→ 핸들러 실행 + stopPropagation
| /** | ||
| * Virtual DOM diff 및 업데이트 | ||
| * @param {HTMLElement} parentElement - 업데이트할 부모 DOM element | ||
| * @param {*} newNode - 새로운 vNode (문자열, 숫자, 객체, 배열, null/undefined 가능) | ||
| * @param {*} oldNode - 기존 vNode (문자열, 숫자, 객체, 배열, null/undefined 가능) | ||
| * @param {number} [index=0] - 부모 element 내에서의 위치 인덱스 | ||
| */ | ||
| export function updateElement(parentElement, newNode, oldNode, index = 0) { | ||
| // 1. oldNode가 없으면 새 노드 추가 | ||
| if (!oldNode) { | ||
| if (newNode && newNode !== "") { | ||
| const element = createElement(newNode); | ||
| const referenceNode = parentElement.childNodes[index]; | ||
| if (referenceNode) { | ||
| parentElement.insertBefore(element, referenceNode); | ||
| } else { | ||
| parentElement.appendChild(element); | ||
| } | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // 2. newNode가 없으면 기존 노드 제거 | ||
| if (!newNode || newNode === "") { | ||
| const childNode = parentElement.childNodes[index]; | ||
| if (childNode) { | ||
| parentElement.removeChild(childNode); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // 3. 텍스트 노드 처리 | ||
| if (typeof newNode === "string" || typeof newNode === "number") { | ||
| // 타입 체크 후 문자열 변환 | ||
| if (newNode !== oldNode) { | ||
| const newText = String(newNode); | ||
| const targetNode = parentElement.childNodes[index]; | ||
| if (targetNode && targetNode.nodeType === Node.TEXT_NODE) { | ||
| targetNode.textContent = newText; | ||
| } else { | ||
| const textNode = document.createTextNode(newText); | ||
| if (targetNode) { | ||
| parentElement.replaceChild(textNode, targetNode); | ||
| } else { | ||
| parentElement.appendChild(textNode); | ||
| } | ||
| } | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // 4. 배열 처리 | ||
| if (Array.isArray(newNode) || Array.isArray(oldNode)) { | ||
| const newChildren = Array.isArray(newNode) ? newNode : [newNode]; | ||
| const oldChildren = Array.isArray(oldNode) ? oldNode : [oldNode]; | ||
| const maxLength = Math.max(newChildren.length, oldChildren.length); | ||
|
|
||
| for (let i = 0; i < maxLength; i++) { | ||
| // 수정: index + i 대신 i를 사용 (배열 자체가 하나의 단위) | ||
| updateElement(parentElement, newChildren[i], oldChildren[i], i); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // 5. 노드 타입이 다르면 교체 | ||
| if (newNode.type !== oldNode.type) { | ||
| const newElement = createElement(newNode); | ||
| const targetNode = parentElement.childNodes[index]; | ||
| if (targetNode) { | ||
| parentElement.replaceChild(newElement, targetNode); | ||
| } else { | ||
| parentElement.appendChild(newElement); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // 6. 같은 타입이면 속성만 업데이트 | ||
| const targetElement = parentElement.childNodes[index]; | ||
| if (targetElement && targetElement.nodeType === Node.ELEMENT_NODE) { | ||
| // 얕은 비교로 속성이 다를 때만 업데이트 | ||
| if (newNode.props !== oldNode.props) { | ||
| updateAttributes(targetElement, newNode.props, oldNode.props); | ||
| } | ||
|
|
||
| // 7. 자식 노드들 재귀적으로 업데이트 | ||
| const newChildren = newNode.children || []; | ||
| const oldChildren = oldNode.children || []; | ||
| const maxLength = Math.max(newChildren.length, oldChildren.length); | ||
|
|
||
| for (let i = 0; i < maxLength; i++) { | ||
| updateElement(targetElement, newChildren[i], oldChildren[i], i); | ||
| } | ||
|
|
||
| // 불필요한 자식 노드 제거 | ||
| const excessCount = targetElement.childNodes.length - newChildren.length; | ||
| for (let i = 0; i < excessCount; i++) { | ||
| targetElement.removeChild(targetElement.lastChild); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
6단계 Virtual DOM 비교로 설계했습니다.
- 단계별 분기: null체크 → 텍스트 → 배열 → 타입변경 → 속성업데이트 순서
- 최소 DOM 조작: 변경된 부분만 찾아서 처리
- 재귀적 구조: 자식 요소들도 동일한 방식으로 비교
- 요소 재사용: 같은 타입이면 기존 DOM 요소 재활용
There was a problem hiding this comment.
// 3. 텍스트 노드 처리
if (typeof newNode === "string" || typeof newNode === "number") {
저는 노드가 normalize 되면서 number가 오는 케이스를 다룰 필요가 없다고 판단했는데 이점에서 차이점이 있었던것같아요!
There was a problem hiding this comment.
// 개선 후: 메인 함수 - 전체 플로우만 관리
function updateAttributes(target, newProps, oldProps) {
const oldKeys = Object.keys(oldProps || {});
const newKeys = Object.keys(newProps || {});
// 1. 제거 단계
oldKeys.forEach((key) => {
if (!(key in (newProps || {}))) {
removeAttribute(target, key, oldProps[key]);
}
});
// 2. 추가/변경 단계
newKeys.forEach((key) => {
const oldValue = oldProps?.[key];
const newValue = newProps[key];
if (oldValue === newValue) return; // 조기 반환으로 성능 최적화
setAttribute(target, key, newValue, oldValue);
});
}
/**
* 속성 제거 전용 헬퍼 함수
* @param {HTMLElement} target - 대상 DOM element
* @param {string} key - 제거할 속성 키
* @param {*} oldValue - 제거할 속성의 이전 값
*/
function removeAttribute(target, key, oldValue) {
// ... 제거 로직 구현
}
/**
* 속성 설정 전용 헬퍼 함수
* @param {HTMLElement} target - 대상 DOM element
* @param {string} key - 설정할 속성 키
* @param {*} newValue - 설정할 새 값
* @param {*} oldValue - 이전 값 (이벤트 핸들러 교체 시 필요)
*/
function setAttribute(target, key, newValue, oldValue) {
// ... 설정 로직 구현
}2단계 속성 업데이트 + 타입별 차등 처리로 설계되었습니다.
- 2단계 처리: 제거 → 추가/변경 순서로 안전한 업데이트
- 값 비교 최적화: oldValue === newValue 체크로 불필요한 DOM 조작 방지
- 타입별 특화: 이벤트/className/Boolean/일반 속성을 각각 다르게 처리
- 함수 분리: removeAttribute/setAttribute로 책임 분리
There was a problem hiding this comment.
VNode 타입에 따른 분기, 속성 diff, 이벤트 등록/제거 흐름까지 잘 설계되어 있고, 특히 배열/텍스트 노드/불필요한 자식 제거 처리까지 전체적인 reconciliation 흐름을 잘 잡아주셨네요.
| // 1단계: 이미 설정된 루트인지 확인 | ||
| if (initializedRoots.has(root)) return; | ||
|
|
||
| const eventTypes = ["click", "focus", "blur", "keydown", "keyup", "mouseover", "mouseout", "change", "input"]; |
There was a problem hiding this comment.
addEvent로 어짜피 어떤 이벤트를 할지 들어오게 되는데 그걸로 배열이나 set으로 저장해서, eventTypes으로 지정된 배열로 만들어지 않고 addEvent에서 저장된 걸 사용하면 되지 않을까요?
There was a problem hiding this comment.
저도 같은 생각 입니다. eventTypes Set을 사용해서 중복을 허용하지 않겠다는 의도를 코드로 보여줌으로 다른 개발자가 코드를 한번 더 생각하지 않게 만들면 좋을 것 같습니다!
There was a problem hiding this comment.
저도 명시적으로 선언하면 다른 개발자들에게 코드의 가독성이 높혀져서 좋다고 생각이 드네요!!
There was a problem hiding this comment.
@annkimm @k-sang-soo @unseoJang
오...놓친 부분이네요🫢 감사해요~!!
unseoJang
left a comment
There was a problem hiding this comment.
지수님, 1-2과제 정말 수고 많으셨습니다!
전체적으로 AI를 적절히 잘 활용하셨고, 과제를 시작하기 전 큰 그림을 설계한 흔적이 코드 곳곳에서 느껴졌어요. 솔직히 1년 차 개발자라고 보기 어려울 만큼 구조가 굉장히 탄탄했습니다.
특히 가상 DOM과 관련된 로직에서 React의 동작 원리를 제대로 이해하고 구현하신 부분이 인상 깊었습니다. 저도 AI를 돌려보며 흠을 잡아보려고 했는데… 잘 짜여 있어서 딱히 지적할 부분이 없더라고요 ㅎㅎ
이번 과제 멋지게 마무리하신 만큼, 1-3과제도 충분히 잘 해내실 거라고 믿어요! 화이팅입니다 🙌
| import { addEvent } from "./eventManager"; | ||
|
|
||
| export function createElement(vNode) {} | ||
| /** |
There was a problem hiding this comment.
VNode의 분기를 명확하게 잘 나눠주셨네요, 속성처리도 잘해주셨고 전체적으로 React 가 어떻게 동작하는지 알고 짜신 느낌이 드네요!
| // 1단계: 이미 설정된 루트인지 확인 | ||
| if (initializedRoots.has(root)) return; | ||
|
|
||
| const eventTypes = ["click", "focus", "blur", "keydown", "keyup", "mouseover", "mouseout", "change", "input"]; |
There was a problem hiding this comment.
저도 명시적으로 선언하면 다른 개발자들에게 코드의 가독성이 높혀져서 좋다고 생각이 드네요!!
There was a problem hiding this comment.
입력된 모든 타입을 일관된 구조로 정규화함으로써 후속 렌더링 단계에서의 예외 처리를 최소화할 수 있는 구조예요. 특히 함수형 컴포넌트를 재귀로 펼치는 설계가 좋고, 0을 보존하는 로직도 명확해요
There was a problem hiding this comment.
VNode 타입에 따른 분기, 속성 diff, 이벤트 등록/제거 흐름까지 잘 설계되어 있고, 특히 배열/텍스트 노드/불필요한 자식 제거 처리까지 전체적인 reconciliation 흐름을 잘 잡아주셨네요.
과제 체크포인트
배포 링크
https://hyunzsu.github.io/front_6th_chapter1-2/
기본과제
가상돔을 기반으로 렌더링하기
이벤트 위임
심화 과제
Diff 알고리즘 구현
과제 셀프회고
기술적 성장
JSX Transform 구현: Classic Transform 방식과 createVNode 함수로 React jsx-runtime.js 역할을 직접 구현
가상 DOM 구현: React 내부 동작 원리를 직접 구현해보며 개념 이해
이벤트 위임 시스템: eventManager를 통한 동적 요소들의 효율적인 이벤트 처리 메커니즘 구현
DOM 업데이트 최적화: updateElement 함수로 가상 DOM 비교 및 DOM 패칭 알고리즘 구현
코드 품질
updateAttributes 함수 성능 개선
변경사항: (updateElement.js)
문제점
기존 updateAttributes 함수는 속성 처리가 2단계로 나뉘어져 불필요한 중복 순회가 발생했습니다.
또한 이벤트 핸들러, className, Boolean 속성 처리 로직이 제거 단계와 추가 단계에서 각각 중복 구현되어 있어 특정 속성 처리 방식을 수정할 때 여러 곳을 동시에 고쳐야 하는 문제가 있었습니다.
개선 의도
이러한 문제들을 해결하기 위해 함수를 역할별로 분리했습니다. updateAttributes는 전체적인 흐름만 관리하고,
실제 속성 제거는 removeAttribute가, 설정은 setAttribute가 각각 담당하도록 구조를 개선했습니다.
학습 효과 분석 (상세 문서화 작성하며 학습한 핵심 개념들)
리뷰 받고 싶은 내용
1. Virtual DOM diff 알고리즘의 성능과 개선 방향
현재
updateElement함수는 인덱스 기반 비교로 구현되어 있어, 리스트 순서 변경 시 성능 이슈가 있을 것 같습니다.질문:
2. updateAttributes 함수 리팩토링의 설계 방향성
성능 개선을 위해
updateAttributes함수를 헬퍼 함수로 분리했습니다:// 개선 후: 메인 함수(30줄) + removeAttribute(30줄) + setAttribute(35줄)질문:
3. 이벤트 위임 시스템의 확장성
현재 구현한 이벤트 위임 시스템에서 성능과 메모리 효율성에 대해 궁금합니다:
질문:
4. Boolean 속성 처리의 브라우저 호환성
updateAttributes에서 Boolean 속성을 처리할 때 checked/selected는 Property만, readOnly는 Attribute+Property, 일반 Boolean은 둘 다 설정하는 방식으로 구현했는데:
질문: