Yangs1s 님의 상세페이지[5팀 양성진] Chapter 1-2. 프레임워크 없이 SPA 만들기 (2)

과제 체크포인트

배포 링크

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

기본과제

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

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

이벤트 위임

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

심화 과제

Diff 알고리즘 구현

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

과제 셀프회고

기술적 성장

개념관련 공부를 통해서 알게된 내용이 좀 생겼다.

  1. 브라우저의 로딩 과정
  • 전반적인 과정을 파악을 했고 그 흐름을 설명할수 있을거 같아요
  1. VirtualDom
  • 왜 가상돔을 이용하는지 이유를 정확하게 알게되었습니다.
  1. 당연하다고 생각한걸 이해해가는 과정이 좀 많았던 과제였던거 같아요
  • 브라우저의 로딩과정, 그리고 가상돔의 대한 내용을 공부를 하면서 리액트가 어떤 작동하는지, 이런것들은 약간 느껴보는 과제였던거 같습니다.
  • 그냥 리액트에서 당연하게 제공해주던 기능들이 복잡한 과정을 통해 구현되고 왜 그렇게 만들어졌는지 몸소 깨닫는?? 이런 생각을 많이 하게 되는 과제였던거 같아요.
  1. 구현 과정에서 추천해준 책을 읽어 봤지만 많이 읽어보진 못해서 해결에는 큰도움을 못받은거 같았습니다.
  2. flat, Map,set을 약간 알게되었습니다.
  • 진짜 이런걸 많이 사용해본적도, 알려준적도 없었는데, 하다보니까 과제에 필요할거 같아서, 사용하게 되었습니다.
  • cdn도 보고 활용이 잘 안되면 AI를 이용해서 작업을 했습니다.

코드 품질

export function normalizeVNode(vNode) {
  // 1. falsy 값 처리
  if (vNode === null || vNode === undefined || typeof vNode === "boolean") {
    return "";
  }
  // 2. 문자열 또는 숫자 처리
  if (typeof vNode === "string" || typeof vNode === "number") {
    return vNode.toString();
  }

  if (typeof vNode === "object" && typeof vNode.type === "function") {
    return normalizeComponent(vNode);
  }

  // false 값은 자식 노드에서 제거되어야 한다.
  if (vNode && typeof vNode === "object" && typeof vNode.type === "string") {
    return {
      type: vNode.type,
      props: vNode.props,
      children: (vNode.children || []).map(normalizeVNode).filter((child) => {
        return child !== "";
      }),
    };
  }

  return vNode;
}

const normalizeComponent = (componentVnode) => {
  const { type, props, children } = componentVnode;

  const propsWithChildren = { ...props, children: children };
  const normalized = type(propsWithChildren);
  return normalizeVNode(normalized);
};

처음에는 테스트코드를 맞춰서 통과만 하자는 식으로 만들었다가 좀더 범용적으로 만들어보자 했을때, 컴퍼넌트의 중첩의 뎁스나 구조에 구애받지않게 재귀적인 처리를 사용.

리팩토링이 되었으면 하는 부분 if문이 당연 필요한 내용들이 많았던걸로 생각하는데 너무 남발한거 같아서.. 코드가 너무 지져분 한거 같습니다.

학습 효과 분석

renderElement,updateElement 부분을 할떄 가장 배울게 많았던거 같아요. 완벽하게 알순 없지만 ,Diffing알고리즘의 기본적인 원리나, 재귀를 통해서 순회하는거나 Map,set과 같이 저의 기준에서 잘 사용해보지 않고 처음 접해본것들도 참 많고 배울거도 많구나 하는게 많았습니다. 그만큼 저의 기본적인 지식도 많이 부족하고 열심히 더 봐야겠다 하는 꺠달음을 얻는 효과?가 더 생겼습니다.

하지만 AI의존도는 줄여보고자 했는데 노력은 해봤지만 생각보다 잘 되지는 않네요

리뷰 받고 싶은 내용

// 가상 노드객체를 생성하는 함수
export function createVNode(type, props, ...children) {
  // 자바스크립트에서 0은 falsy값이라 테스트코드에서는 출력이 안될수 있으므로
  // 예외처리를 해줘야할거 같음

  return {
    type,
    props,
    children: flattenChildren(children),
  };
}

function flattenChildren(children) {
  return children.flat(Infinity).filter((child) => !isFalsy(child));
}

function isFalsy(value) {
  return value === false || value === null || value === undefined;
}

저는 그냥 flat으로 infinity뎁스를 줘서 만들었지만 flat을 펑션으로 만들어서 하시는 분들도 있으시더라고요, 저는 오히려 짧지만 그역할을 하면 이상이 없다고 생각이 드는데, flat을 쓰는게 맞을까 궁금합니다.

function updateAttributes($el, props) {
  if (props.props) {
    for (const key in props.props) {
      const value = props.props[key];

      if (key === "className") {
        $el.setAttribute("class", props.props["className"]);
      } else if (key.startsWith("on") && typeof value === "function") {
        const eventType = key.slice(2).toLowerCase();
        addEvent($el, eventType, value);
      } else if (typeof value === "boolean" || value === null) {
        // Boolean 또는 null 값 처리
        if (key === "checked" || key === "selected") {
          // Property만 설정하고 DOM 속성은 제거
          if (value === null) {
            $el[key] = false; // null이면 기본값으로
          } else {
            $el[key] = value;
          }
          $el.removeAttribute(key);
        } else {
          // 일반 boolean 속성들 (disabled, readOnly 등)
          if (value === null) {
            $el[key] = false; // null이면 기본값으로
            $el.removeAttribute(key);
          } else {
            $el[key] = value;
            if (value) {
              $el.setAttribute(key, "");
            } else {
              $el.removeAttribute(key);
            }
          }
        }
      } else if (!key.startsWith("on")) {
        if (value === null || value === undefined) {
          $el.removeAttribute(key);
        } else {
          $el.setAttribute(key, value);
        }
      }
    }
  }

  return {
    ...props,
    $el,
  };
}

createElement에서 각각의 속성이나 class처리하는데 if문을 많이 썻는데, switch로 쓰기엔 그 case들이 너무 다양해서 if문으로 작성했는데 이렇게 너무 많이 쓰는게 좀 더럽기도 하고 보는 사람이 되게 어려울거 같아서.. 고민을 많이 했는데 보통 이럴때는 어떻게 처리해주면 좀 더 깔끔하게 코드를 만들수 있을까요..?

과제 피드백

성진님 한 주 고생하셨습니다. 회고에 남겨주셨던것처럼 리액트 내부 구조에 대해서도 고민할 수 있는 기회였고, 사용하는 여러 자료구조나 구현 방식에 대해서도 많은 것을 고민할 수 있는 시간이였던 것 같네요! 적어주신 것 처럼 시간이 촉박하다보니 전반적인 흐름이나 사용하는데 중점을 뒀다면 여유 시간에 좀 더 명확하게 하는 과정을 겪으면 더 좋을 것 같네요!

이어서 질문 남겨주신 부분 답변 남겨보면요!

flat(Inifinity) 관련

넵 저도 아마 구현을 했다면 성진님처럼 필요한 기능에 대해서만 우선 구현을 했을 것 같아요! 그리고 가능한 브라우저 네이티브 구현을 사용하려고 해야 속도 측면이나 가독성, 안정성 측면에서도 훨씬 얻는 점이 단기적, 장기적으로 많다고 생각합니다.

if문 관련

걱정하시는 것처럼 지금은 절차적으로 따라가는게 쉬울 지는 몰라도 조금 더 복잡해진다면 해당 역할들을 코드를 읽으면서 명확하게 이해하는게 어려워질 것 같아 보이기는 하네요 ㅎㅎ 나름 코드를 작성하는데 있어서 3뎁스 또는 2뎁스 이상의 분기를 만들지 않겠다 라는 규칙을 만들고 더 깊은 뎁스가 발생하는 경우 함수를 분리해야 하는 시점, 더러운 코드를 작성하는 시점이라고 판단해보는 것도 좋을 것 같습니다.

if를 사용하든 switch를 사용하든은 사실 큰 문제가 아닌것 같아요. 분기를 타는 조건을 혹시 컴포넌트 별로 분리할 수 있는지, 명확한 타입화를 통해 추상화를 할 수 있는지 구분해보고 해당 동작들을 별도 함수로 분리해서 가독성을 높여보는게 좋지 않을까 싶네요!

고생하셨고 다음 주 과제도 화이팅입니다!!