Skip to content

[1팀 신희원] Chapter 1-2. 프레임워크 없이 SPA 만들기 #37

Open
Amelia-Shin wants to merge 18 commits into
hanghae-plus:mainfrom
Amelia-Shin:main
Open

[1팀 신희원] Chapter 1-2. 프레임워크 없이 SPA 만들기 #37
Amelia-Shin wants to merge 18 commits into
hanghae-plus:mainfrom
Amelia-Shin:main

Conversation

@Amelia-Shin
Copy link
Copy Markdown

@Amelia-Shin Amelia-Shin commented Jul 14, 2025

과제 체크포인트

배포 링크

기본과제

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

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

이벤트 위임

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

심화 과제

Diff 알고리즘 구현

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

과제 셀프회고

VirtualDOM 에 대해 특징과 개념을 알게 되었다.
이벤트 위임하는 부분이 진짜 어려웠는데... 어찌저찌 했다... 다시 공부가 필요할 것 같다.
이번에도 많은 개념들을 배웠다. (weakmap , set , 평탄화/정규화)

기술적 성장

createVNode
평탄화를 왜 해주는가?
: 중첩된 배열을 단일 배열로 펼쳐서 다루기 쉽게 만들기 위해서

아래 코드를 보면

안에 자식 노드가 있다.
map은 배열을 반환하고

<div>
   <div>hello</div>
   Array.map((name) => { <div>${name}</div> });
</div>
----
위와 같은 코드
<div>
   <div>hello</div>
   <button>name1</button>
   <button>name2</button>
</div>
---
[{ type: div , props: '', children: 'hello'}, [{.type: button, props: '', children: name1 }, { .type: button, props: '', children: name2  }]]

nomalizeVNode
정규화를 해주는 이유 : 객체/배열 구조 데이터를 평탄하고 효율적으로 구성하는 것

// 정규화 전 (비정규화 상태)
// company 정보가 사용자마다 중복되어 있다. 이렇게 되면 수정 시 모든 곳을 찾아서 바꿔야 한다.
const users = [
  {
    id: 1,
    name: "Alice",
    company: { id: 100, name: "OpenAI" }
  },
  {
    id: 2,
    name: "Bob",
    company: { id: 100, name: "OpenAI" }
  }
];
// 회사 정보가 한 곳에만 있어서 변경, 추적이 쉽고 실수도 줄어든다.
const users = {
  1: { id: 1, name: "Alice", companyId: 100 },
  2: { id: 2, name: "Bob", companyId: 100 }
};

const companies = {
  100: { id: 100, name: "OpenAI" }
};

createElement
entries 사용하여 attr , value 로 배열화하여 element 요소 넣어주기.

function updateAttributes($el, props) {
    Object.entries(props).forEach(([attr, value]) => {
      if (attr.startsWith("on") && typeof value === "function") {
        const eventType = attr.toLowerCase().slice(2);
        addEvent($el, eventType, value);
      } else if (["checked", "disabled", "selected", "readOnly"].includes(attr)) {
        $el[attr] = Boolean(value);
      } else if (attr === "className") {
        $el.setAttribute("class", value);
      } else if (attr === "style" && typeof value === "object") {
        Object.assign($el.style, value);
      } else {
        $el.setAttribute(attr, value);
      }
    });
  }

eventManager

  • 이벤트 위임
    Map, eventMap
    JavaScript에서 키-값 쌍을 저장하는 자료구조지만, 사용 목적과 내부 동작 방식에 큰 차이

📊 Map vs WeakMap 차이 요약

항목 Map WeakMap
키 타입 객체, 원시값 모두 가능 객체만 가능
반복 가능 불가능
크기 확인 .size 가능 불가능
GC 영향 수동 제거 필요 자동 제거 (키가 없으면 값도 제거됨)
사용 목적 일반적인 키-값 저장 프라이빗 데이터나 메모리 누수 방지 등
  • WeakMap 을 사용하는 이유
    메모리 누수 방지 (가비지 컬렉션)
    DOM 요소가 삭제되면 WeakMap의 키(= 요소)에 대한 참조도 사라짐
    그러면 JavaScript 엔진이 해당 entry를 자동으로 메모리에서 제거함
const el = document.createElement("div");
addEvent(el, "click", handler);
document.body.removeChild(el); // el DOM에서 제거됨

만약 Map을 사용했다면 eventMap이 el을 계속 참조 → 메모리 누수 발생
하지만 WeakMap은 el이 참조되지 않으면 자동으로 해당 데이터 제거됨

updateElement

  • diffing 알고리즘
    두 데이터 구조 간의 차이점(Difference)을 찾아내는 알고리즘
  • 주로 DOM을 효율적으로 업데이트하기 위해 사용
  • 성능 최적화를 위해 필요 (전체 DOM 대신 변경된 부분만 업데이트)

renderElement

  • 이전 Node 와 현재 Node
const vNodeMap = new WeakMap();

export function renderElement(vNode, container) {
  const normalizedVNode = normalizeVNode(vNode);
  const oldNode = vNodeMap.get(container);
  
  if (!oldNode) {
    // 최초 렌더링시에는 createElement로 DOM을 생성하고
    const element = createElement(normalizedVNode);
    container.appendChild(element);
  } else {
    // 이후에는 updateElement로 기존 DOM을 업데이트한다.
    updateElement(container, normalizedVNode, oldNode, 0);
  }

  // 렌더링이 완료되면 container에 이벤트를 등록한다.
  setupEventListeners(container);
  vNodeMap.set(container, normalizedVNode);
}

코드 품질

코드 리팩토링 필요한 부분

  • flat 메서드 사용 (코드가 더 간결해짐)
  • flat 메서드의 존재를 모르고 AI를 통해 함수로 작성하였다.
export function createVNode(type, props, ...children) {
  // Helper to flatten deeply nested arrays
  function flatten(arr) {
    return arr.reduce((acc, val) => {
      if (Array.isArray(val)) {
        acc.push(...flatten(val));
      } else {
        acc.push(val);
      }
      return acc;
    }, []);
  }

  // Remove null, undefined, boolean (except 0/number)
  function filterValid(child) {
    return !(child === null || child === undefined || typeof child === "boolean");
  }

  // Flatten and filter children
  const flatChildren = flatten(children).filter(filterValid);

  return {
    type,
    props,
    children: flatChildren,
  };
}
// children.flat(Infinity) 사용
// flat(depth)는 배열을 얼마나 깊이까지 평탄화할지를 지정하는데, Infinity를 넣으면 모든 깊이까지 완전히 펼치라는 의미
export function createVNode(type, props, ...children) {
  return {
    type,
    props,
    children: children.flat(Infinity).filter((value) => value === 0 || Boolean(value)),
  };

학습 효과 분석

  • 이벤트 위임, 직접 바인딩의 차이점
    addEvent (이벤트 위임) 과 addEventListener (직접 바인딩) 의 차이 요약표
항목 addEvent addEventListener
리스너 수 1개 (이벤트 종류별 1개) 요소마다 1개씩
메모리 사용량 적음 많아질 수 있음
동적 요소 대응 자동 수동 등록 필요
이벤트 타겟 정확성 event.target 기반, 부모 탐색 필요 명확하게 해당 요소
이벤트 종류 제한 기본적으로 버블링되는 이벤트만 가능 캡처링 포함 모든 이벤트 가능
성능 (많은 요소일 때) 우수 성능 저하 가능
구현 난이도 약간 복잡 간단함

과제 피드백

createElement.js 파일에서 createDocumentFragment 사용 이유 알아보기..!

  • VirtualDOM 을 직접 구현해보면서 VirtualDOM에 대해 알아갈 수 있어 좋은 기회가 된거 같습니다. [바뀐 부분만 업데이트 해주어서 효율적]

리뷰 받고 싶은 내용

  1. 평탄화를 해줄 때 "" 으로 return 해주는 이유와 falsy한 값을 왜 필터링 해주는 지 궁금합니다.
export function normalizeVNode(vNode) {  
  if (vNode == null || vNode == undefined || typeof(vNode) == "boolean") {
    return "";
  }
  
  if (typeof(vNode) == "number" || typeof(vNode) == "string") {
    return String(vNode);
  }
  
  if (typeof vNode.type === "function") {
    return normalizeVNode(vNode.type({ ...vNode.props, children : vNode.children }));
  }
 
  return {
    type: vNode.type,
    props: vNode.props || null,
    children: vNode.children.map(normalizeVNode).filter(Boolean),
  };
}
  1. handleEvent 함수에서 elementEvents 를 디버그 찍어보면 아래 내용이 나오는데, 어떤 것인지 궁금합니다.
    [Function: spy] { 안에 있는 내용은 이벤트 핸들러들인가요 ? 무엇인지 궁금합니다. }
Map(1) {
  'click' => Set(1) {
    [Function: spy] {
      getMockName: [Function (anonymous)],
      mockName: [Function (anonymous)],
      mockClear: [Function (anonymous)],
      mockReset: [Function (anonymous)],
      mockRestore: [Function (anonymous)],
      getMockImplementation: [Function (anonymous)],
      mockImplementation: [Function (anonymous)],
      mockImplementationOnce: [Function (anonymous)],
      withImplementation: [Function: withImplementation],
      mockReturnThis: [Function (anonymous)],
      mockReturnValue: [Function (anonymous)],
      mockReturnValueOnce: [Function (anonymous)],
      mockResolvedValue: [Function (anonymous)],
      mockResolvedValueOnce: [Function (anonymous)],
      mockRejectedValue: [Function (anonymous)],
      mockRejectedValueOnce: [Function (anonymous)],
      [Symbol(nodejs.dispose)]: [Function (anonymous)]
    }
  }
}
function handleEvent(event) {
    let target = event.target;

    while (target && target !== rootElement) {
        const elementEvents = eventMap.get(target);
        if (elementEvents) {
            const handlers = elementEvents.get(event.type);
            if (handlers) {
                handlers.forEach((handler) => handler(event));
                return;
            }
        }
        target = target.parentElement;
    }
}

@ckdwns9121
Copy link
Copy Markdown
Member

고생하셧습니다 희원님 해낼줄 알앗다고요~

Comment thread src/lib/eventManager.js
Comment on lines +32 to +61
export function addEvent(element, eventType, handler) {
if (!eventMap.has(element)) {
eventMap.set(element, new Map());
}

const elementEvents = eventMap.get(element);
if (!elementEvents.has(eventType)) {
elementEvents.set(eventType, new Set());
}

elementEvents.get(eventType).add(handler);
/**
* elementEvents 형태
* Map(1) {
* 'click' => Set(1) {
* [Function: spy] {
* getMockName: [Function (anonymous)],
* mockName: [Function (anonymous)],
* ...
*/

if (!delegatedEvents.has(eventType)) {
delegatedEvents.add(eventType);

if (rootElement) {
rootElement.removeEventListener(eventType, handleEvent);
rootElement.addEventListener(eventType, handleEvent);
}
}
}
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.

WeakMap, Map , Set을 사용하였는데 적절히 잘 사용한 것인지

Comment thread src/lib/createElement.js
Comment on lines +31 to +47
function updateAttributes($el, props) {
Object.entries(props).forEach(([attr, value]) => {
if (attr.startsWith("on") && typeof value === "function") {
const eventType = attr.toLowerCase().slice(2);
addEvent($el, eventType, value);
} else if (["checked", "disabled", "selected", "readOnly"].includes(attr)) {
$el[attr] = Boolean(value);
} else if (attr === "className") {
$el.setAttribute("class", value);
} else if (attr === "style" && typeof value === "object") {
Object.assign($el.style, value);
} else {
$el.setAttribute(attr, value);
}
});
}
}
Copy link
Copy Markdown

@angielxx angielxx Jul 19, 2025

Choose a reason for hiding this comment

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

각 조건문에 early return을 해주면 조건문을 깔끔하게 쓸 수 있습니다!

AS-IS

if (attr.startsWith("on") && typeof value === "function") {
  const eventType = attr.toLowerCase().slice(2);
  addEvent($el, eventType, value);
} else if (["checked", "disabled", "selected", "readOnly"].includes(attr)) {
  $el[attr] = Boolean(value);
}

TO-BE

if (attr.startsWith("on") && typeof value === "function") {
  const eventType = attr.toLowerCase().slice(2);
  addEvent($el, eventType, value);
 return 
}

if (["checked", "disabled", "selected", "readOnly"].includes(attr)) {
  $el[attr] = Boolean(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.

오 ~!! 그러네요! 조건문이 더 깔끔해졌어용

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/createVNode.js Outdated
// Remove null, undefined, boolean (except 0/number)
function filterValid(child) {
return !(child === null || child === undefined || typeof child === "boolean");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

요런 헬퍼 함수들은 createVNode 밖에 정의하는 게 좋을 것 같습니다. 함수 지역 단위에서 기억해야하는 게 아니라면! createVNode가 실행될 때마다 내부 함수들이 계속 생성돼요. 파일 안에 정의하면 파일이 실행될 때 한번만 생성됩니다~

Comment thread src/lib/createElement.js

export function createElement(vNode) {}
export function createElement(vNode) {
if (vNode == null || vNode == undefined || typeof vNode == "boolean") {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

희원님! 이건 소소한 팁인데 null과 undefined를 느슨한 비교를 통해 동시에 비교하는 문법은 어떠신지 제안 드리려구 가져왔어요!!

toss/frontend-fundamentals#189

토스에서는 null과 undefined를 같이 걸러야 하는 케이스에서 대부분 느슨한 비교를 사용한다고 해요! 참고하십셔!
저는 코드가 보기에 심플해져서 개인적으로 느슨한 비교가 더 좋은 것 같아요

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.

오 이런 방법도 있었군요 !! 감사합니다 :)

Comment thread src/lib/renderElement.js
container.appendChild(element);
} else {
// 이후에는 updateElement로 기존 DOM을 업데이트한다.
updateElement(container, normalizedVNode, oldNode, 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

updateElement 함수 index의 기본값이 이미 0으로 들어가 있어서 여기서는 0을 빼도 같은 동작을 할 것 같아요!

Comment thread src/lib/createVNode.js
Comment on lines 1 to 7
export function createVNode(type, props, ...children) {
return {};
return {
type,
props,
children: children.flat(Infinity).filter((child) => child != null && child !== false),
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

flat 매서드를 쓰기전에 reduce로 구현하신것도 참신했어요! 결국 flat 매서드를 알아내셔서 이렇게 깔끔한 코드로 바뀐것을 보니 희원님은 성장의 폭이 가파른 개발자군용! :) !!

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