항해99 두번째 과제

장운서·2025년 4월 13일

항해99

목록 보기
3/8
post-thumbnail

Weekly I Learned

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

  • 이번 과제는 정말 쉽지 않았습니다.
    각 파일이 어떤 역할을 하는지, 함수들이 어떤 순서로 동작하는지에 대한 개인적인 생각으로 설명이 부족하다 보니, 전반적인 구조를 파악하는 데 큰 어려움을 겪었습니다. 특히 함수 간 체이닝 구조나 render → updateAttributes → eventManager 같은 흐름이 왜 그렇게 구성되어 있는지에 대한 의문이 계속 들었습니다.

  • 솔직히 말하면, GPT의 도움이 없었다면 구현 자체가 버거웠을 것 같습니다. 그만큼 제 실력의 한계를 느끼기도 했고, 동시에 앞으로 어떤 걸 더 공부해야 할지도 보였습니다. 항해가 끝난 뒤에는 제 코드뿐만 아니라 다른 사람들의 코드도 함께 리뷰해보며, SPA 구조에 대한 더 깊은 이해를 쌓고 싶습니다.

  • 비록 설명은 부족했다고 느껴졌지만, 직접 SPA의 구조를 짜보고, JSX를 DOM으로 렌더링하는 과정을 구현해본 건 분명 큰 수확이었습니다. 다음 과제부터는 미리 일요일에 착수해서, 여유 있게 고민하고 구현할 수 있도록 준비하려고 합니다.

2. 시도

특히 만족스러운 구현
SPA 렌더링 구조에서 핵심이 되는 updateAttributes 함수를 아래와 같이 직접 구현했다

function updateAttributes(target, originNewProps, originOldProps) {
  const newProps = { ...originNewProps };
  const oldProps = { ...originOldProps };

  // 이전 이벤트 핸들러 제거
  Object.keys(oldProps).forEach((propName) => {
    if (
      propName.startsWith("on") &&
      (!newProps[propName] || newProps[propName] !== oldProps[propName])
    ) {
      const eventType = propName.toLowerCase().substring(2);
      removeEvent(target, eventType, oldProps[propName]);
    }
  });

  // 새로운 속성 추가 및 변경
  Object.keys(newProps).forEach((propName) => {
    if (propName.startsWith("on")) {
      const eventType = propName.toLowerCase().substring(2);
      addEvent(target, eventType, newProps[propName]);
    } else if (propName === "className") {
      target.setAttribute("class", newProps[propName]);
    } else {
      target.setAttribute(propName, newProps[propName]);
    }
  });

  // 삭제된 속성 제거
  Object.keys(oldProps).forEach((propName) => {
    if (!(propName in newProps)) {
      target.removeAttribute(propName);
    }
  });
}

이 함수는 다음과 같은 흐름으로 구성되어 있습니다

-> 이전 이벤트 핸들러 제거
-> 새로운 속성 및 이벤트 핸들러 반영
-> 삭제된 속성 제거

렌더링된 요소의 실제 DOM 속성과 이벤트를 동기화하는 데 중요한 역할을 하며, 명확한 순서와 책임 분리가 명확히 표시되어 있습니다.
특히 className 처리나 이벤트 핸들러 비교 후 조건적 제거는 실질적인 유지보수 편의성을 고려한 부분인것같습니다.

리팩토링이 필요한 부분
현재는 setAttribute 를 조건 없이 호출하고 있어, 기존 값과 같은 속성도 매번 업데이트됩니다.
이는 성능상 불필요한 DOM 조작을 유발할 수 있으며, 추후 diff 비교를 통해 값이 실제로 바뀌었을 때만 변경하도록 개선이 필요합니다.
이벤트 핸들러 등록/제거 로직도 반복적으로 등장하기 때문에, 이 부분을 별도의 헬퍼 함수로 추출해 코드 중복을 줄이고 가독성을 높일 수 있는 리팩토링 여지가 있습니다.

3. 해결

  • 이벤트가 동작하지 않던 버그 해결
  • 라우터 전환 시 렌더링 오류 해결
  • HashRouter 도입 후 페이지 전환이 어색했던 부분을 해결하며 SPA의 라우팅 흐름을 직접 디버깅함.
  • 체이닝 구조에서 발생한 렌더 순서 오류 해결
  • renderElement에서 이벤트 바인딩이 안 되는 문제를 추적한 끝에, DOM 삽입과 이벤트 연결 순서를 조정해 문제를 해결함.

4. 알게된 것

  • JSX → DOM 변환 흐름 학습
  • renderElement.js를 직접 구현하며 JSX 구조를 실제 DOM으로 변환하는 과정을 경험함. 특히 createElement, updateAttributes, appendChildren 등 각 단계의 역할을 코드 레벨에서 이해하게 됨.
  • 가상 DOM diffing 개념 도입
  • 이전 DOM 상태와 새로운 상태를 비교하여 최소한의 변경만 반영하는 diff 로직을 처음 접하고, 이를 통해 리렌더링 최적화의 핵심 원리를 학습함.
  • SPA 구성 요소에 대한 실전 감각
  • 해시 라우팅을 활용한 SPA 기본 골조를 직접 구현하며 라우팅, 초기 렌더링, 상태 변화에 따른 화면 갱신 흐름을 실무처럼 익힘.
  • 함수형 사고와 체이닝 구조 이해
  • 여러 함수들이 서로의 결과를 이어받아 실행되는 구조(renderElement → updateAttributes → appendChildren → eventManager)를 통해 함수형 스타일의 흐름 제어를 경험함
  • JSX 기반 렌더링 로직 구현 경험
  • renderElement.js를 직접 구현하면서 가상 DOM과 이벤트 바인딩 흐름에 대한 개념을 체화함.
  • renderElement.js에서 JSX 렌더링 후 이벤트가 등록되지 않는 문제를 디버깅하며 addEventListener의 시점과 DOM 구조를 체크하는 능력을 기름.
  • 컴포넌트 테스트 작성 경험
  • Post 컴포넌트의 "추가", "좋아요 토글" 기능을 테스트하면서 사용자 행동 기반의 테스트 설계 감각을 익힘.
  • 이벤트 위임 구조의 중요성 체감
  • eventManager.js와 연계하여 동적 요소의 이벤트 핸들링을 구현하며 이벤트 위임 방식이 SPA에서 얼마나 강력한지 직접 체감함.
  • 속성 업데이트 로직의 본질 이해
  • updateAttributes 함수에서 기존 속성과 새로운 속성을 비교하고 동기화하는 로직을 디버깅하며, DOM의 상태 관리에 대한 감각을 느껴봄.
  • updateAttributes 디버깅을 통한 성능 개선
  • 속성 변경이 누락되거나 잘못 반영되는 버그를 수정하며, 속성 비교 시 불필요한 DOM 업데이트를 줄이는 조건 분기 로직을 직접 설계함.

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

  • 전체 앱 구조에서 컴포넌트 기반으로 Header/Footer/Layout을 잘 분리함
  • 로그인 상태와 사용자 정보를 auth 객체로 관리하는 방식
  • SPA 라우팅 구현을 이벤트 위임으로 처리한 접근

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

  • 현재 renderElement → updateAttributes → appendChildren → eventManager로 이어지는 구조에서, 책임 분리가 잘 이루어진 편인지 궁금합니다.

  • updateElement.js의 updateAttributes와 createElement.js의 updateAttributes의 함수명이 왜 같게 진행되는지 두함수의 체이닝관계를 이해하지 못했습니다

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

  • 제공해주신 코드에서 removeEvent가 제대로 작동하지 않는 이유는 조건부 분기에 문제가 있을 수 있습니다. key.startsWith("on")이라는 조건을 체크하지 않고 모든 키에 대해 이벤트를 제거하려고 시도하기 때문일 수 있습니다. 원래의 주석 처리된 코드처럼 이벤트와 일반 속성을 구분해야 합니다:
for (const [key, value] of Object.entries(oldProps)) {
  if (!(key in newProps)) {
    if (key.startsWith("on")) {
      const eventType = key.toLowerCase().slice(2);
      removeEvent(target, eventType, value); // 이벤트 핸들러만 제거
    } else {
      target.removeAttribute(key); // 일반 속성 제거
    }
  }
}
profile
성공을 위해선 과정만 있을 뿐이다

0개의 댓글