Skip to content

[8팀 현지수] Chapter 1-2. 프레임워크 없이 SPA 만들기#47

Open
hyunzsu wants to merge 13 commits into
hanghae-plus:mainfrom
hyunzsu:main
Open

[8팀 현지수] Chapter 1-2. 프레임워크 없이 SPA 만들기#47
hyunzsu wants to merge 13 commits into
hanghae-plus:mainfrom
hyunzsu:main

Conversation

@hyunzsu
Copy link
Copy Markdown

@hyunzsu hyunzsu commented Jul 16, 2025

과제 체크포인트

배포 링크

https://hyunzsu.github.io/front_6th_chapter1-2/

기본과제

가상돔을 기반으로 렌더링하기

  • createVNode 함수를 이용하여 vNode를 만든다.
  • normalizeVNode 함수를 이용하여 vNode를 정규화한다.
  • createElement 함수를 이용하여 vNode를 실제 DOM으로 만든다.
  • 결과적으로, JSX를 실제 DOM으로 변환할 수 있도록 만들었다.

이벤트 위임

  • 노드를 생성할 때 이벤트를 직접 등록하는게 아니라 이벤트 위임 방식으로 등록해야 한다
  • 동적으로 추가된 요소에도 이벤트가 정상적으로 작동해야 한다
  • 이벤트 핸들러가 제거되면 더 이상 호출되지 않아야 한다

심화 과제

Diff 알고리즘 구현

  • 초기 렌더링이 올바르게 수행되어야 한다
  • diff 알고리즘을 통해 변경된 부분만 업데이트해야 한다
  • 새로운 요소를 추가하고 불필요한 요소를 제거해야 한다
  • 요소의 속성만 변경되었을 때 요소를 재사용해야 한다
  • 요소의 타입이 변경되었을 때 새로운 요소를 생성해야 한다

과제 셀프회고

기술적 성장

  • JSX Transform 구현: Classic Transform 방식과 createVNode 함수로 React jsx-runtime.js 역할을 직접 구현

    • JSX Transform 방식 비교 학습: Classic Transform(React 17 이전)과 Automatic Transform(React 17 이후) 차이점 이해
    • 빌드 타임 vs 런타임 처리: esbuild가 JSX를 JavaScript로 변환하는 과정과 런타임에서 createVNode가 실행되는 과정 구분
  • 가상 DOM 구현: React 내부 동작 원리를 직접 구현해보며 개념 이해

    • 6단계 정규화 프로세스: normalizeVNode의 null/boolean → 원시타입 → 배열 → 객체 → 함수컴포넌트 → HTML엘리먼트 처리 과정
    • 타입 안전성 구현: 다양한 vNode 타입(문자열, 숫자, 배열, 객체, 함수)을 안전하게 처리하는 메커니즘
  • 이벤트 위임 시스템: eventManager를 통한 동적 요소들의 효율적인 이벤트 처리 메커니즘 구현

    • 3단계 저장소 구조: element → eventType → handlers Set 구조로 이벤트 관리
    • 버블링 탐색 알고리즘: event.target부터 루트까지 올라가며 핸들러를 찾는 시스템
  • DOM 업데이트 최적화: updateElement 함수로 가상 DOM 비교 및 DOM 패칭 알고리즘 구현

    • 6단계 diff 알고리즘: 노드 추가 → 제거 → 텍스트 → 배열 → 타입변경 → 속성업데이트 순서로 처리
    • DocumentFragment 활용: 여러 DOM 요소를 한 번에 조작하여 성능 최적화

코드 품질

updateAttributes 함수 성능 개선

  • 변경사항: (updateElement.js)

  • 문제점
    기존 updateAttributes 함수는 속성 처리가 2단계로 나뉘어져 불필요한 중복 순회가 발생했습니다.
    또한 이벤트 핸들러, className, Boolean 속성 처리 로직이 제거 단계와 추가 단계에서 각각 중복 구현되어 있어 특정 속성 처리 방식을 수정할 때 여러 곳을 동시에 고쳐야 하는 문제가 있었습니다.

  • 개선 의도
    이러한 문제들을 해결하기 위해 함수를 역할별로 분리했습니다. updateAttributes는 전체적인 흐름만 관리하고,
    실제 속성 제거는 removeAttribute가, 설정은 setAttribute가 각각 담당하도록 구조를 개선했습니다.

// 개선 후: 메인 함수 - 전체 플로우만 관리
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) {
  // ... 설정 로직 구현
}

학습 효과 분석 (상세 문서화 작성하며 학습한 핵심 개념들)

리뷰 받고 싶은 내용

1. Virtual DOM diff 알고리즘의 성능과 개선 방향

현재 updateElement 함수는 인덱스 기반 비교로 구현되어 있어, 리스트 순서 변경 시 성능 이슈가 있을 것 같습니다.

// 현재 구현 방식의 문제점
// Before: <li>Apple</li><li>Banana</li><li>Cherry</li>
// After:  <li>Banana</li><li>Apple</li><li>Cherry</li>
// → Apple과 Banana의 텍스트 내용을 모두 교체 (비효율적)

// 이상적인 방식: key 기반 비교로 DOM 요소 이동만 수행

질문:

  1. 현재 과제 수준에서 키 기반 diff 알고리즘 구현이 필요한지, 아니면 인덱스 기반으로도 충분한지 궁금합니다.
  2. 만약 키 기반 구현이 필요하다면, 어떤 우선순위로 접근하는 것이 좋을까요?

2. updateAttributes 함수 리팩토링의 설계 방향성

성능 개선을 위해 updateAttributes 함수를 헬퍼 함수로 분리했습니다:

// 개선 후: 메인 함수(30줄) + removeAttribute(30줄) + setAttribute(35줄)

질문:

  1. 이런 함수 분리 방식이 적절한지 궁금해요!

3. 이벤트 위임 시스템의 확장성

현재 구현한 이벤트 위임 시스템에서 성능과 메모리 효율성에 대해 궁금합니다:

// 현재 구현: 3단계 저장소 구조
eventMap = Map {
  element => Map {
    eventType => Set { handlers }
  }
}

질문:

  1. 대규모 애플리케이션에서 eventMap이 메모리 누수의 원인이 될 가능성은 없을까요?
  2. WeakMap 사용을 고려해야 하는 상황이 있을까요?

4. Boolean 속성 처리의 브라우저 호환성

updateAttributes에서 Boolean 속성을 처리할 때 checked/selected는 Property만, readOnly는 Attribute+Property, 일반 Boolean은 둘 다 설정하는 방식으로 구현했는데:

if (key === 'checked' || key === 'selected') {
  $el[key] = value; // Property만
} else if (key === 'readOnly') {
  // Attribute + Property 둘 다
} else {
  // 일반 Boolean: Attribute + Property 둘 다
}

질문:

  1. 이런 케이스별 다른 처리 방식이 모든 브라우저에서 안전한지 궁금합니다.
  2. 더 일관성 있는 처리 방법이 있을까요?

@hyunzsu hyunzsu changed the title [8팀 현지수] Chapter 1-2. 프레임워크 없이 SPA 만들기 (2) [8팀 현지수] Chapter 1-2. 프레임워크 없이 SPA 만들기 Jul 17, 2025
Comment thread src/lib/createElement.js
Comment on lines +83 to +112
// 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;
}
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

updateAttributes에서 Boolean 속성을 처리할 때 checked/selected는 Property만, readOnly는 Attribute+Property, 일반 Boolean은 둘 다 설정하는 방식으로 구현했는데 더 일관성 있는 처리 방법이 있을까요?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

이런식으로 테이블을 만든다음에

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;
}

이렇게 일괄처리하는 방법도 있다고 하네요

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@devchangjun 오 좋은 방법이네요! 감사해요~!!

Comment thread src/lib/eventManager.js
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

이벤트 위임 패턴 + 3단계 저장소 구조로 설계했습니다.

  • 중앙 집중식 관리: 루트 요소에만 리스너를 등록하고 모든 하위 이벤트를 처리
  • 버블링 탐색: event.target부터 시작해서 부모로 올라가며 핸들러 검색
  • 3단계 저장소: element → eventType → handlers Set 구조
  • 메모리 효율성: Set으로 중복 방지 + 빈 구조 자동 정리

동작 흐름
루트 이벤트 발생
→ handleDelegatedEvent 호출
→ event.target부터 부모로 버블링 탐색
→ eventMap에서 핸들러 찾기
→ 핸들러 실행 + stopPropagation

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

구조 설계 너무 좋네요

Comment thread src/lib/updateElement.js
Comment on lines +138 to +237
/**
* 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);
}
}
}
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

6단계 Virtual DOM 비교로 설계했습니다.

  • 단계별 분기: null체크 → 텍스트 → 배열 → 타입변경 → 속성업데이트 순서
  • 최소 DOM 조작: 변경된 부분만 찾아서 처리
  • 재귀적 구조: 자식 요소들도 동일한 방식으로 비교
  • 요소 재사용: 같은 타입이면 기존 DOM 요소 재활용

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

// 3. 텍스트 노드 처리
  if (typeof newNode === "string" || typeof newNode === "number") {

저는 노드가 normalize 되면서 number가 오는 케이스를 다룰 필요가 없다고 판단했는데 이점에서 차이점이 있었던것같아요!

Comment thread src/lib/updateElement.js
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

// 개선 후: 메인 함수 - 전체 플로우만 관리
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로 책임 분리

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

VNode 타입에 따른 분기, 속성 diff, 이벤트 등록/제거 흐름까지 잘 설계되어 있고, 특히 배열/텍스트 노드/불필요한 자식 제거 처리까지 전체적인 reconciliation 흐름을 잘 잡아주셨네요.

Comment thread src/lib/eventManager.js
// 1단계: 이미 설정된 루트인지 확인
if (initializedRoots.has(root)) return;

const eventTypes = ["click", "focus", "blur", "keydown", "keyup", "mouseover", "mouseout", "change", "input"];
Copy link
Copy Markdown

@annkimm annkimm Jul 19, 2025

Choose a reason for hiding this comment

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

addEvent로 어짜피 어떤 이벤트를 할지 들어오게 되는데 그걸로 배열이나 set으로 저장해서, eventTypes으로 지정된 배열로 만들어지 않고 addEvent에서 저장된 걸 사용하면 되지 않을까요?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

저도 같은 생각 입니다. eventTypes Set을 사용해서 중복을 허용하지 않겠다는 의도를 코드로 보여줌으로 다른 개발자가 코드를 한번 더 생각하지 않게 만들면 좋을 것 같습니다!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

저도 명시적으로 선언하면 다른 개발자들에게 코드의 가독성이 높혀져서 좋다고 생각이 드네요!!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@annkimm @k-sang-soo @unseoJang
오...놓친 부분이네요🫢 감사해요~!!

Copy link
Copy Markdown

@unseoJang unseoJang left a comment

Choose a reason for hiding this comment

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

지수님, 1-2과제 정말 수고 많으셨습니다!

전체적으로 AI를 적절히 잘 활용하셨고, 과제를 시작하기 전 큰 그림을 설계한 흔적이 코드 곳곳에서 느껴졌어요. 솔직히 1년 차 개발자라고 보기 어려울 만큼 구조가 굉장히 탄탄했습니다.

특히 가상 DOM과 관련된 로직에서 React의 동작 원리를 제대로 이해하고 구현하신 부분이 인상 깊었습니다. 저도 AI를 돌려보며 흠을 잡아보려고 했는데… 잘 짜여 있어서 딱히 지적할 부분이 없더라고요 ㅎㅎ

이번 과제 멋지게 마무리하신 만큼, 1-3과제도 충분히 잘 해내실 거라고 믿어요! 화이팅입니다 🙌

Comment thread src/lib/createElement.js
import { addEvent } from "./eventManager";

export function createElement(vNode) {}
/**
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

다들 주석을 잘 써주셧네요
각각 함수마다

Comment thread src/lib/createElement.js
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

VNode의 분기를 명확하게 잘 나눠주셨네요, 속성처리도 잘해주셨고 전체적으로 React 가 어떻게 동작하는지 알고 짜신 느낌이 드네요!

Comment thread src/lib/eventManager.js
// 1단계: 이미 설정된 루트인지 확인
if (initializedRoots.has(root)) return;

const eventTypes = ["click", "focus", "blur", "keydown", "keyup", "mouseover", "mouseout", "change", "input"];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

저도 명시적으로 선언하면 다른 개발자들에게 코드의 가독성이 높혀져서 좋다고 생각이 드네요!!

Comment thread src/lib/eventManager.js
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

구조 설계 너무 좋네요

Comment thread src/lib/normalizeVNode.js
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

입력된 모든 타입을 일관된 구조로 정규화함으로써 후속 렌더링 단계에서의 예외 처리를 최소화할 수 있는 구조예요. 특히 함수형 컴포넌트를 재귀로 펼치는 설계가 좋고, 0을 보존하는 로직도 명확해요

Comment thread src/lib/updateElement.js
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

VNode 타입에 따른 분기, 속성 diff, 이벤트 등록/제거 흐름까지 잘 설계되어 있고, 특히 배열/텍스트 노드/불필요한 자식 제거 처리까지 전체적인 reconciliation 흐름을 잘 잡아주셨네요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants