Skip to content

[4팀 오하늘] Chapter 1-2. 프레임워크 없이 SPA 만들기 #60

Open
eveneul wants to merge 20 commits into
hanghae-plus:mainfrom
eveneul:main
Open

[4팀 오하늘] Chapter 1-2. 프레임워크 없이 SPA 만들기 #60
eveneul wants to merge 20 commits into
hanghae-plus:mainfrom
eveneul:main

Conversation

@eveneul
Copy link
Copy Markdown

@eveneul eveneul commented Jul 17, 2025

과제 체크포인트

배포 링크

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

기본과제

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

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

이벤트 위임

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

심화 과제

Diff 알고리즘 구현

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

과제 셀프회고

기술적 성장

🗺️ map VS Weakmap

  • 과제를 시작하기 전, 학습메이트분께서 어디에선가 Weakmap을 사용할 일이 있다고 힌트를 주셨는데, eventManager.js 구현에 필요하다는 것을 알게 되었습니다.
  • 처음 접하는 Weakmap에 대해 먼저 공부하고, 일반 Object와 map, 그리고 weakmap의 차이점을 먼저 습득하고 eventManager.js 구현에 나섰습니다.
  • 그럼, 이벤트 데이터를 어떻게 구조화하면 효율적으로 저장하고 꺼낼 수 있을까 고민하게 되었고, 문득 준일 코치님께서 이벤트 위임을 사용할 경우, 보통 최상위 엘리먼트(root container) 하나에 이벤트 리스너를 등록하고, 내부의 자식 요소들에서 발생하는 이벤트 버블링을 통하여 감지하고 처리한다라는 말씀을 하신 게 생각이 났고,
  • 이를 바탕으로 eventType(click, blur, change...)을 키로 삼고, { element, handler } 형태로 구성해 보려고 했지만, Weakmap은 반복문 사용이 불가능하다는 점을 알게 되어, map으로 대체하게 되었습니다.
  • 최종적으로 Map을 기반으로 한 eventStore를 만들었고, 각 eventType에 대해 내부적으로 또 다른 Map을 사용해 element와 handler를 저장했습니다.
  • 이벤트 핸들러는 Set을 사용해 중복 등록을 방지했습니다.
export function addEvent(element, eventType, handler) {
  if (!eventStore.has(eventType)) {
    // 스토어에 이벤트타입이 없으면 set
    // 내부 맵은 어떤 element에 어떤 handler가 등록되어 있는지를 저장
    eventStore.set(eventType, new Map());
  }

  // eventStore에서 이벤트 타입에 맞는 값을 뽑아온 후
  const elementMap = eventStore.get(eventType);

  // 뽑아오려고 했는데 없으면 set
  if (!elementMap.has(element)) {
    // 핸들러의 중복 방지를 위해 set
    elementMap.set(element, new Set());
  }

  elementMap.get(element).add(handler);
}

export function removeEvent(element, eventType, handler) {
  const elementMap = eventStore.get(eventType); // evenType

  if (!elementMap) return;
  const handlers = elementMap.get(element); // element에 맞는 handler들이 추출됨

  if (!handlers) return;

  // 인자로 받은 핸들러를 삭제
  handlers.delete(handler);

  if (handlers.size === 0) elementMap.delete(element); // 핸들러가 아예 없으면 (element에 등록된 이벤트가 아무것도 없으면) elementMap(eventStore)에서 삭제
  if (elementMap.size === 0) eventStore.delete(eventType); // 이벤트 타입에 등록된 element가 아예 없으면 이벤트 타입 자체를 스토어에서 삭제
}

코드 품질

  • 특히 만족스러운 구현
  const newNodeChildren = Array.isArray(newNode.children) ? newNode.children : [];
  const oldNodeChildren = Array.isArray(oldNode.children) ? oldNode.children : [];
  const max = Math.max(newNodeChildren.length, oldNodeChildren.length);

  for (let i = 0; i < max; i++) {
    updateElement(targetElement, newNodeChildren[i], oldNodeChildren[i], i);
  }
  • newNodeChildren을 기준으로 비교하자니 삭제된 요소를 감지하지 못하고, oldNodeChildren기준으로 비교하자니 추가된 요소를 감지하지 못해서 둘 중 length가 더 긴 기준으로 비교하려고 Math.max()를 사용했습니다.
  • 리팩토링이 필요한 부분 + 코드 설계 관련 고민과 결정
    • createElement.. 이대로 괜찮은 건가 싶습니다. if문 떡칠입니다. 한 함수에 3개 이상 if문 들어가는 거 안 좋아하는데, 여러 타입에 대응해야 하다 보니 if문이 많이 쓰였습니다. 클린 코드 책을 미리 읽었으면 이런 참사도 없었겠지요..
  for (const [key, value] of Object.entries(props)) {
    if (key === "children") continue;

    if (key.startsWith("on") && typeof value === "function") {
      // 이벤트 처리 - 위임 방식으로 처리하므로 DOM 속성으로 설정하지 않음
      const eventType = key.slice(2).toLowerCase();
      addEvent($el, eventType, value);
      continue;
    }

    if (key === "style") {
      Object.assign($el.style, value);
    }

    if (key === "className") {
      $el.setAttribute("class", value);
      continue;
    }

    if (booleanAttr.includes(key)) {
      $el[key] = !!value;
      continue;
    }

학습 효과 분석

  • DOM에서 node는 브라우저가 실제로 화면에 렌더링하는 요소의 최소 단위라는 것을 명확히 이해했습니다. HTML 요소, 텍스트, 주석 등 모든 DOM 구성 요소가 Node 객체로 표현된다는 점이 재미있었다.
  • React.js에서는 어떻게 필요한 부분만 쏙 골라서 새로운 DOM으로 변경하는 것인지 이번 과제를 통해 배웠습니다. vNode(Virtual DOM Node)를 먼저 만들고, 그것을 원본과 비교하여 변화가 생긴 부분만 실제 DOM에 반영하는 방식으로 성능을 최적화한다는 점을 배웠습니다.
  • vNode를 비교해 실제 DOM과 비교해 반영하는 과정(diff 알고리즘 등)은 아직 낯설어서 추가 학습이 필요할 것 같습니다.

과제 피드백

  • 이번 기회가 아니었으면 리액트가 어떤 식으로 SPA를 구성했는지, 왜 리액트에서는 기존 자바스크립트에서 무엇을 해결하고자 했는지 배웠습니다.
  • 기본 과제에는 renderElement.js 항목만 테스트에 있어서 updateElement.js는 구현 안 해도 기본 과제는 통과하는 건가? 했는데...... 마지막 두 개의 테스트에서 막혔습니다. 그리고 e2e 테스트도 updateElement.js가 정상적으로 구현되어야 pass가 되어서 그냥 다 구현했습니다. 😭

리뷰 받고 싶은 내용

  • eventManager.js에서 저는 new Map()을 사용했지만, map이 아닌 weakmap을 사용한다면 어떤 구조로 작성을 했어야 할까요?
  • weakmap의 key는 오직 객체만 가능하고, 그 객체가 다른 곳에서 더 이상 참조되지 않으면 자동으로 가비지 컬렉션의 대상이 된다고 하지만, 반복문을 돌리지 못하는 것 때문에 구현에 어려움을 겪어 map을 사용했지만 다른 분들은 weakmap을 사용했다는 분들도 계셔서요. 코치님 같은 경우에는, 어떤 방식으로 eventManager.js를 구현하셨을지 궁금합니다.

eveneul added 20 commits July 13, 2025 22:29
- 인자 vNode가 null, undefined, boolean 타입일 때 빈값 리턴
- 인자 vNode가 string, number 타입이면 문자열로 리턴
- 인자 vNode가 function일 때 재귀로 펑션 리턴
- 인자 vNode가 함수형 컴포넌트일 때는 작업 중
- vNode가 null, undefined, boolean일 때 빈 문자열 리턴
- string, number 타입은 문자열로 변환하여 리턴
- 함수형 컴포넌트 처리 로직 추가 및 자식 요소 정리 기능 구현
- vNode가 null, undefined, boolean일 때 빈 텍스트 노드 리턴
- string, number 타입은 문자열로 변환하여 텍스트 노드 리턴
- vNode가 배열일 경우 DocumentFragment 생성 및 자식 요소 처리 추가 (진행 중)
- vNode가 객체일 때 type 속성을 기반으로 엘리먼트 생성 로직 추가 (진행 중)
- vNode가 배열일 경우 DocumentFragment 생성 및 자식 요소 처리 로직 추가
- vNode가 객체일 때 type 속성을 기반으로 엘리먼트 생성 및 속성 업데이트 로직 구현
- updateAttributes 함수 추가하여 props 처리 기능 구현
- 오류 처리 추가: vNode 처리 불가 시 에러 발생
- vNode가 객체일 때 type 속성이 함수인 경우 에러 발생 로직 추가
- updateAttributes 함수에 id 속성 처리 및 boolean 값 처리 로직 추가
- eventManager.js에 이벤트 위임 및 핸들러 추가/제거 기능 구현
- createElement.js에서 함수형 컴포넌트 처리 로직 추가
- basic.test.jsx에서 컴포넌트 정규화 관련 주석 추가
- renderElement 함수에서 최초 렌더링 및 업데이트 로직 명확화
- DOM 생성 및 이벤트 등록 과정에 대한 주석 추가
- normalizeVNode 호출 및 container._vnode 관리 로직 추가
- childNodes가 비어 있는 형태로 오는 (실제 DOM에 아무것도 없음) 경우 수정 중..
- basic.test.jsx에서 불필요한 console.log 주석 처리
- createElement.js에서 children 처리 로직 수정 및 주석 추가
- normalizeVNode.js에서 children 정리 로직 개선
- renderElement.js에서 DOM 업데이트 로직 주석 처리
- updateElement.js에서 children 접근 방식을 수정
- basic.test.jsx에서 불필요한 console.log 제거
- eventManager.js에서 이벤트 핸들러 등록 방식을 개선하여 중복 제거
- normalizeVNode.js에서 자식 노드 정리 로직 개선
- renderElement.js에서 DOM 업데이트 로직 간소화 및 주석 정리
- updateElement.js에서 속성 업데이트 로직 개선 및 불필요한 코드 제거
- eventManager.js에서 이벤트 핸들러를 Map으로 관리하여 중복 등록 방지 및 기존 핸들러 제거 기능 추가
- updateElement.js에서 속성 업데이트 로직 개선 및 불필요한 코드 제거, className 및 style 처리 방식 수정
- createElement.js에서 boolean 속성 처리 로직을 추가하여 속성 업데이트 시 boolean 값을 올바르게 설정하도록 개선
- renderElement.js에서 oldVNode 변수를 const로 변경하고 updateElement 호출 순서를 수정하여 코드 가독성 향상
- updateElement.js에서 boolean 속성 처리 로직을 추가하고 불필요한 console.log 제거
- createElement.js와 updateElement.js에서 boolean 속성 처리 방식을 수정하여 일관성을 유지
- updateAttributes 함수에서 className 및 일반 속성 처리 방식을 개선하고 불필요한 console.log 제거
- 주석을 추가하여 코드 가독성을 향상
- updateElement.js에서 속성 설정 시 함수 타입 체크 및 null 값 처리 개선
- className 및 boolean 속성 처리 로직 수정
- 중복 속성 설정 제거 및 안전성 체크 추가로 코드 안정성 향상
- 잉여 노드 제거 로직 추가로 DOM 관리 개선
Copy link
Copy Markdown

@2Estella 2Estella left a comment

Choose a reason for hiding this comment

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

하늘 공주님~ 2주차 과제 너무너무 고생하셨어요! ❤️💗💖💓💝
코드에서 고민하신 흔적과 풀어나간 것들이 보여서 코드 구경 잘 하고 가요~!
다음주차도 화이팅 😘🫶

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.

전반적으로 이벤트 위임의 핵심을 잘 이해하여 설계하고 고민을 많이 하신 코드 같아요! 특히 역할에 따라 eventStore와 eventHandlers를 분리하고 Map/Set을 활용해 데이터 구조를 깔끔하게 설계한 점이 인상적이에요!

제가 처음에 과제 이야기를 하면서 WeakMap을 사용했었단 얘기를 했었는데 이게 오히려 하늘님을 고민을 하게 만들었군요 ㅠㅠ (제 개인적으로는 깨달음을 얻게 된? 그런거라 한번 보셨으면 했어요 ㅎㅎ)

적용 방법 예시는 아래 코드에 남길게요!

Comment thread src/lib/eventManager.js
Comment on lines +31 to +49
export function addEvent(element, eventType, handler) {
if (!eventStore.has(eventType)) {
// 스토어에 이벤트타입이 없으면 set
// 내부 맵은 어떤 element에 어떤 handler가 등록되어 있는지를 저장
eventStore.set(eventType, new Map());
}

// eventStore에서 이벤트 타입에 맞는 값을 뽑아온 후
const elementMap = eventStore.get(eventType);

// 뽑아오려고 했는데 없으면 set
if (!elementMap.has(element)) {
// 핸들러의 중복 방지를 위해 set
// console.log(elementMap, "elementMap..");
elementMap.set(element, new Set());
}

elementMap.get(element).add(handler);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

저는 이 부분에서 WeakMap을 사용했는데요!
Map은 DOM에서 제거된 요소에 대한 강한 참조를 유지해 메모리 누수가 발생할 수 있다고 해요.
그래서 WeakMap을 사용하여 요소가 DOM에서 사라질 때 자동으로 GC 대상이 되어 메모리 관리가 더욱 안전해질 수 있도록 구현했었어요.

예를 들면 아래와 같이 변경할 수 있어요.

export function addEvent(el, type, handler) {
  let elementMap = eventStore.get(type);

  if (!elementMap) {
    elementMap = new WeakMap();
    eventStore.set(type, elementMap);
  }

  let handlers = elementMap.get(el);

  if (!handlers) {
    handlers = new Set();
    elementMap.set(el, handlers);
  }
  handlers.add(handler);
}

Comment thread src/lib/createElement.js
Comment on lines +43 to +48
if (typeof vNode === "object" && typeof vNode.type === "function") {
throw new Error("컴포넌트를 createElement로 처리할 수 없습니다");
}

throw new Error("컴포넌트를 createElement로 처리할 수 없습니다");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

함수형 컴포넌트 감지 후 throw와 마지막 throw 메시지가 동일한데, 혹시 두 곳 모두 필요한 이유가 있을까요?

Comment thread src/lib/createElement.js
Comment on lines +63 to +65
if (key === "style") {
Object.assign($el.style, value);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

style 분기 안에 continue가 누락된 것 같아요 😇

Comment thread src/lib/normalizeVNode.js
Comment on lines +34 to +43
if (typeof vNode.type === "function") {
const props = {
...vNode.props,
children: cleanChildren(vNode.children),
};

const normalizedChildren = cleanChildren(vNode.children);
const result = normalizeVNode(vNode.type({ ...props, children: normalizedChildren }));
return result;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

여기 cleanChildren(vNode.children)를 두 번 호출하고 계신데 혹시 두 번 분리해서 호출하신 특별한 이유가 있을까요?
아니면 한 번만 호출해서 const normalizedChildren = cleanChildren(vNode.children)처럼 재사용하는 편이 더 나을 것 같아요!

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.

전체적으로 각 함수가 책임 분리 원칙에 따라 깔끔하게 구성되어 있어 가독성이 좋아요!
diff 기반 업데이트 로직도 효과적으로 구현하신 것 같습니다.
다만 PR 올리시기 전에 console.log는 제거 하시는 게 좋을 것 같아요 ㅎㅎ(넘 많아잉)

@eveneul
Copy link
Copy Markdown
Author

eveneul commented Jul 19, 2025

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.

공주님들은 weakmap으로 구현했는지 묻고 싶습니다
이 부분에 대해서 나는 한 번도 배워 본 적 없는(다음 학기에 들을..) 자료구조가 나와서
많이 당황슨 당황슨 했는데.. 공주님들은 처음에 이것 보자마자 와! 이 구조다! 하면서 바바박. 작성하셨는지용

Copy link
Copy Markdown

@nimusmix nimusmix Jul 19, 2025

Choose a reason for hiding this comment

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

저는 weakMap으로 구현했어요!
송이님이 weakMap이라는 자료 구조가 유용할 거다. 어디 쓰일지 생각해봐라라고 힌트를 주신 적이 있어요.
그게 아니었으면 생각 못했을 것 같긴 한데,
저 말을 듣고 weakMap에 대해서 미리 공부했던 점이 해당 자료구조를 떠올릴 수 있게 해준 것 같아요.

eventHandler의 경우 remove 하는 것이 중요한 일이다 보니,
removeEvent를 만들 때 이걸 어떻게 하면 잘 remove할 수 있을까 라는 고민을 했거든요!
그 때 아 여기서 weakMap을 쓰면 되겠구나 라는 생각을 했습니다!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

저도 weakMap으로 구현했긴 한데요 사실 이런 자료구조가 있는줄도 몰랐지만...
송이님께서 힌트를 주셔서 쓰게 됐슴다!
Map 형태의 자료구조가 필요할것 같은데... > 어라 weakMap이 어디선가 쓰인다고 말씀하셨는데 이 타이밍인가?
요런 사고의 흐름으로 사용했슴다!

Copy link
Copy Markdown

@esoby esoby Jul 19, 2025

Choose a reason for hiding this comment

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

저두 이벤트 매니절에는 그냥 냅다 map으로 썼는데용
updateElement 구현할 때쯤 공주들의 weakmap 언급이 떠올라서 renderElement에서 써먹어봤어요!

컨테이너맵으로 이전에 렌더링 했던 vNode와 해당 vNode로부터 생성된 루트 요소를 가지고 있다가 create인지 update인지 판단하는데, 일반 map으로 구현하면 dom이 변경되고 컨테이너가 교체될 때마다 더이상 필요없는 dom 객체 정보도 가지고 있게 되니까 메모리 누수가 발생할 것 같아서 넣어봤듭니다.

이번에 찐하게 알게 되었으니 map 쓸 때마다 한번씩 떠올려볼 거 같아요!

Copy link
Copy Markdown

@adds9810 adds9810 Jul 19, 2025

Choose a reason for hiding this comment

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

저는 eventManager에서 ai의 제안으로 Map으로 구현했습니다. Map은 JavaScript에서 자주 사용하는 자료구조라서 이벤트 핸들러를 저장하는 데 자연스럽게 떠올랐어요.
나중에 다른분들의 코드를 보면서 송이님이 말씀하신 weakmap이 이렇게 사용할 수 있구나를 알게 되었습니다.ㅜ 다음에 비슷한 경우에 두 가지에 대해 생각해 볼 거 같습니다.

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.

이 if문 이렇게 두어도 괜찮은가..? 일단 전 한 함수 안에 if문 3개 이상이면 진절머리나는데요
if문으로 구현할 수 밖에 없는 구조라고 생각되기도 하고 너무너무 보기 안 좋은 것 같기도 하고..
공주님들은 어떻게 생각하시나요?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이 부분은 앞서 준일 코치님이 리뷰하실 때 dispatch 썼던 부분으로 해결하셔도 좋을 것 같고,

저는 아래처럼 해결했어요!
저도 코드를 보기 좋게 짜야하는 병이 있거든요,,
if문을 많이 써야 하는 상황에는 조건문을 보기 쉽게 만든다거나, (저는 isClassName, isEvent 등의 유틸 함수를 사용했어요)
if 문 내부에서 처리해야 하는 내용을 따로 빼기도 합니다. (updateAttribute에서는 그닥 어울리지 않는 것 같지만ㅎㅎ)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

개인적으로는 작성해주신 코드가 함수 안에 if 문이 여러개여도 각 if문 안의 로직 처리가 깔끔하고 독립적이어서 가독성이 떨어진다는 생각이 들지 않습니다!

이와 별개로 각 분기마다 continue 처리도 해주시는군요! 생각 못해봤는데 배워감니다아👍

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이렇게 깔끔하게 함수 분리는 못 했지만 ㅠ 저도 if문 싫어 인간이라 넘 공감합니다 ..........,,!

반복적으로 검사할 조건이 있거나 큰 틀이 보이면 스위치도 자주 사용하고,,
각 if문이 해야하는 역할들이 다채로우면 개별 함수로 분리해도 좋을 거 같아용

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