티스토리 뷰

이번 주차 목표 (프레임워크 없이 SPA 만들기)

(1) 가상 DOM을 정의하고 사용할 수 있다.

(2) 가상 DOM을 이용하여 이벤트 관리를 최적화할 수 있다.

(3) diff 알고리즘을 이용하여 불필요한 렌더링을 최소화할 수 있다.

 

 

1. 문제 (과제, 프로젝트를 진행하면서 부딪혔던 기술적인 문제)

- 전체적인 가상 DOM 구현

가상돔 개념은 Vue React가 이를 기반으로 만들어진 프레임워크라서 익숙했다. 아무래도 기술면접에서도 자주 나오는 질문 사항이니깐.
직접적으로 구현하는 건 처음이었는데, 개념적으로 이해하는 것과 실제로 구현하는 것의 차이를 실감했다... 진짜 어려웠다.

 

- 이벤트 핸들러 구현

createElement updateElement 내부에서 이벤트 핸들러를 다루는 부분이 까다로웠다. 가능하면 AI 도구를 사용하지 않고 구현하려 했지만, VNode를 정규화하고 생성하는 과정은 비교적 수월했어도, 요소를 실제로 생성하거나 업데이트 하는 과정에선 동작 방식이 명확히 이해되지 않아 결국 AI의 도움을 받았다. 이 점이 아쉬움으로 남는다.

 

 

2. 시도

1. 컴포넌트 정규화 하는 과정에서 children이 자꾸 {}로 나온다.

Object {
  "children": Array [],
  "props": null,
  "type": [Function TestComponent],
}

 

 

함수형 컴포넌트를 처리하는 분기에서 잘못되었다라고 판단했다.

vNode가 함수형 컴포넌트일 때의 코드를 수정하고, normalizeVNode 함수에서의 return 방식이 잘못되었을 거라 생각해, map()과 flat()을 계속 수정해보고... child를 보여주는 로직을 계속해서 수정하며 디버깅을 시도했다.

// 기존 코드
if (typeof vNode === "function") {
  const renderedVNode = vNode.type(vNode.props);
  return normalizeVNode(renderedVNode);
}

 

2. '네비게이션의 링크 클릭에서 이벤트 전파를 막았을 때 아무 일도 일어나지 않아야 한다' 테스트 코드 오류 개선

 

링크 클릭하는 함수에서 이슈가 있었던 건가 싶어서 e.stopPropagation()을 안 붙여서 그런건가 수정해보고, 실제로 이벤트 전파를 차단하는 부분들. 특히 <nav> 태그 쪽을 의심해서 수정해보았다.

이 과정에서 eventManager 쪽 이벤트 위임 로직을 함께 검토했다.

 

3. EventManager 코드 오류 개선

 

초기에는 const delegatedEvents = ["click", "mouseover", "focus", "keydown"];와 같은 정해진 이벤트만 root에 등록해놓고 썼었다. 왜냐하면, 테스트코드에 저 4개만 등장했기에... 그런데 동적으로 추가 되는 이벤트에 대해서는 대응을 할 수가 없었다.

export function setupEventListeners(root) {
  const delegatedEvents = ["click", "mouseover", "focus", "keydown"];

  delegatedEvents.forEach((eventType) => {
    root.addEventListener(eventType, (event) => {
      let target = event.target;
      while (target && target !== root) {
        // 타겟 요소에서 이벤트 핸들러 찾기
        if (eventMap.has(target) && eventMap.get(target)[eventType]) {
          eventMap.get(target)[eventType](event); // 핸들러 실행
        }
        target = target.parentElement; // 부모 요소로 올라가기
      }
    });
  });
}

 

 

delegatedEvents를 없애고 이벤트 등록 시점에 동적으로 eventType을 추적할 수 있도록 수정을 시도해야겠다.

혹은 새로운 이벤트가 발생할 때마다 setupEventListeners가 자동으로 인식하게 할 수는 없을까?를 고민해보았다.

 

 

3. 해결

1. 컴포넌트 정규화 하는 과정에서 children이 자꾸 {}로 나온다.

// 변경된 코드
if (typeof vNode.type === "function") {
  const renderedVNode = vNode.type({
    ...vNode.props,
    children: vNode.children,
  });
  return normalizeVNode(renderedVNode); // 렌더링된 VNode를 다시 정규화
}

 

기존 코드에서는, vNode 자체가 함수형 컴포넌트인지 체크하고, props만 넘겼다. 그 결과, 일부 컴포넌트에서 children이 유실 되는 문제가 발생했다.

그래서 vNode.type이 함수형 컴포넌트인지 체크하고, props children을 둘 다 넘겨주도록 수정했다. 이제 함수형 컴포넌트에서도 children이 정상적으로 유지되었다. 결과적으로, 모든 함수형 컴포넌트가 children을 올바르게 전달받고 렌더링할 수 있게 되었다.

 

2. '네비게이션의 링크 클릭에서 이벤트 전파를 막았을 때 아무 일도 일어나지 않아야 한다' 테스트 코드 오류 개선

 

eventManager 코드의 개별 단위 테스트에서는 이벤트가 정상적으로 동작해서 문제가 없다고 생각했는데... 꽤나 애먹은 부분이었다.

  • 단위 테스트가 통과한다고 전체 흐름이 항상 올바르다고 착각하면 안 된다.
  • 특히 이벤트 흐름은 단일 요소가 아니라 DOM 구조 전체를 고려해 추적해야 하며, 이벤트 위임 구조에 대한 깊은 이해가 중요하다는 것을 절실히 느꼈다.

디버깅 잘하는 습관을 길러야겠다... 아직 서툰 디버깅...

 

3. EventManager 코드 오류 개선

const eventTypes = new Set(); // 등록된 이벤트 타입 저장소

// 변경한 코드
export function setupEventListeners(root) {
  eventTypes.forEach((eventType) => {
    root.addEventListener(eventType, handleEvent);
  });
}


function handleEvent(event) {
  let target = event.target;

  // 이벤트가 발생한 요소에서 부모 방향으로 탐색하며 처리 가능한 핸들러를 찾음
  while (target) {
    const elementEvents = eventMap.get(target);

    // 해당 요소에 등록된 이벤트 핸들러가 있는지 확인
    if (elementEvents && elementEvents.has(event.type)) {
      elementEvents.get(event.type).call(target, event);
      break; // 이벤트가 처리되었으면 중단
    }
    target = target.parentElement;
  }
}

 

그래서 addEvent가 호출될 때마다 eventTypes(Set)에 이벤트 타입을 추가하도록 변경했다. setupEventListeners에서 eventTypes에 있는 모든 이벤트를 root에 등록하였다.

동적 이벤트도 정상적으로 작동하게 되었고 테스트 코드 오류도 해소되었다. 또한, call()을 사용하면서 this가 정확하게 target을 가리키도록 보장하였다.

 

 

4. 알게된 것

- 가상 DOM의 동작 방식을 단순히 이해하는 데 그치지 않고, 직접 구현까지 해보면서 어떻게 비교하고, 어떻게 업데이트되는지까지 흐름을 잡을 수 있게 되었음.

- e.preventDefault()를 언제 사용해야 하는지, 언제 사용하지 말아야 하는지 그 기준이 명확해졌다. 무조건 넣는 게 아니라 맥락에 따라 판단할 수 있게 됨!

- WeakMap에 대해 새롭게 알게 되었고, 객체를 키로 사용할 수 있다는 점과 가비지 컬렉션에 영향을 주지 않아 메모리 관리에 유리하다는 사실이 흥미로웠다.

- createElement와 updateElement 과정에서 이벤트 핸들링이 어떤 방식으로 적용되고 업데이트되는지 흐름을 따라가며 정확히 이해할 수 있었다. 직접 손으로 따라 치며 분석한 게 정말 도움이 됐다.

 

 

---

Keep : 현재 만족하고 계속 유지할 부분

주석 꼼꼼하게 작성하고 PR 열심히 쓰기

 

이해가 되지 않는 부분들을 스스로 이해하려고 주석을 꼼꼼하게 작성해보았다. 특히... Diff 코드를 적용한 UpdateElement와 EventManager 부분은 유독 어려웠는데, 주석을 작성하며 흐름을 하나하나 따라가다 보니 자연스럽게 코드가 머릿속에 들어왔다. 예전엔 그저 읽고 넘어가곤 했는데, 직접 설명해보려 하니까 훨씬 이해가 쉬웠다.
또한, 이번 주에는 PR을 꼼꼼하게 작성해보았다. 지난주에는 바빠서 대충 넘어갔었는데, PR을 정리하는 과정도 공부가 된다는 걸 새삼 느꼈다. 문서화가 습관이 되도록 계속 유지하고 싶다!

Problem : 개선이 필요하다고 생각하는 문제점

수면 시간을 늘릴 필요가 있다.

 

저번주에 이어서 이번주에도 지키지 못한 게 있다. 바로 수면 시간.이게 항해를 하면서 느끼는 본질적인 문제이기도 한데... 코딩을 하다 보면 "벌써 1시?!" "이제 2시야?!"이런 느낌이 든다. 특히 애매한 시간대... 11시 반이나, 12시 반에서는 "아 좀만 더 하면 될 거 같은데!"라는 생각에 30분을 훌쩍 넘기곤 한다... 잘못 됐어. 😥

Try : 문제점을 해결하기 위해 시도해야 할 것

12시 45분까지는 꼭 눕는 습관 들이기

 

분명 수면의 질에도 영향을 주고 다음날 회사에서도 기진맥진 상태인데... 진짜 진짜 못해도 12시 45분에는 눕는 습관을 들여야겠다. 그래야 내일 더 나은 모습으로 공부를 열심히 할 수 있으니깐! 더 효율적인 하루를 만들 수 있을 거라 믿는다 😊