heojungseok 님의 상세페이지[5팀 허정석] Chapter 1-2. 프레임워크 없이 SPA 만들기 (2)

과제 체크포인트

배포

링크와 다섯 영걸

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

기본과제

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

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

이벤트 위임

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

심화 과제

Diff 알고리즘 구현

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

과제 셀프회고

6기 동료분들은 1 주차 보다는 난이도가 낮다고 하나, 나에게는 똑같이 어려웠다. 도움이 필요할 때는 팀원들의 도움으로 문제를 해결해나갔다. 그 중 한 분이 나에게 과제를 풀어나가는 데 있어서 필요한 것이 내가 어떤 도구를 활용 할 수 있는지 아는 것이 중요한데, 그것을 모르고 맨 손으로 과제를 헤쳐나가는 느낌이 강하다고 했다.

격하게 공감되는 조언이었다. 과제를 임하기 전에 나에게 필요한 게 무엇인지 한 번은 생각하고 찾는 행위가 필요해보인다.

팀 5에서는 이번 주차는 하루에 하나 또는 그 이상 과제 구현 방식 또는 개발에 도움이 되는 학습 보고서를 작성했다. 양식은 자유였으며 repo issue 또는 개인 블로그에 정리하고 팀내 공유했다.(추후에는 6기 모두에게 공유가 됐다.)

이런 행위로 스스로 어떤 점이 부족한지 알게 됐고, 내 기준 실력이 우수한 인원의 보고서를 보고 있으면 글 자체도 깔끔하게 쓴 것을 볼 수 있었다. 그리고 다들 열심히 하고 있구나를 시각적으로 볼 수 있어서 더 자극이 되었다.

이번 주차가 아니더라도 글 쓰는 것을 꾸준히 하기 위해 노력 해야겠다. 많이 읽고 많이 쓰고.... 그리고 매일 쓰는 거라면 힘을 빼고 쓰는 연습도 필요해 보인다. 그리고 이번 과제도 당연히 Gemini CLI 와 함께했다.! 아래는 이번주 과제를 하면서 하루씩 정리한 보고서 링크다☺️

TIL_1_Array 메소드 TIL_2_JSX TIL_3_재귀 TIL_4_이벤트_위임 git-issue_학습보고서

기술적 성장

이번 2 주차 과제에서는 1 주차 때 관심이 갔던 '이벤트 위임' 에 대해서 학습을 해볼 수 있던 좋은 과제였다. eventManager 를 구현하고 renderElement에 적용하면서 복잡한 이벤트 핸들링을 유지보수성 높게 설계하는 패턴을 배웠다. 또한 diff 알고리즘이라는 새로운 개념도 학습 했다.

코드 품질

updateElement 함수를 작성해야 advanced 난이도를 통과할 수 있었습니다. 지금 내 실력으론 어림도 없어서 전적으로 AI툴에 의존했다. 모르는 걸 물어봐가면서 하기엔 물리적 시간이 부족했기에 코드의 설계보다는 과제의 통과에 집중했다. 통과 후에 다시 보고 리뷰해도 늦지 않는단 생각에 진행했다. 아래는 Gemini 로 구현한 updateElement 코드입니다. 시간 괜찮으신 분들은 코드 리뷰 작성해주셔도 좋은 것 같습니다. (코드 구현을 위한 질문 목록을 뽑질 못했습니다. 질문 목록을 뽑았다면 더 유익했을텐데... 다음 회고에 참고하겠습니다.)


import { addEvent, removeEvent } from "./eventManager";
import { createElement } from "./createElement.js";

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

  // 1. 새로운 속성 추가 및 변경
  for (const attr in newProps) {
    const newValue = newProps[attr];
    const oldValue = oldProps[attr];

    if (newValue !== oldValue) {
      if (attr.startsWith("on") && typeof newValue === "function") {
        const eventType = attr.toLowerCase().slice(2);
        if (oldValue) {
          removeEvent(target, eventType, oldValue);
        }
        addEvent(target, eventType, newValue);
      } else if (["checked", "disabled", "selected", "readOnly"].includes(attr)) {
        target[attr] = Boolean(newValue);
      } else if (attr === "className") {
        newValue ? target.setAttribute("class", newValue) : target.removeAttribute("class");
      } else if (attr === "style" && typeof newValue === "object") {
        for (const styleName in newValue) {
          target.style[styleName] = newValue[styleName];
        }
        for (const styleName in oldValue) {
          if (!(styleName in newValue)) {
            target.style[styleName] = "";
          }
        }
      } else {
        target.setAttribute(attr, newValue);
      }
    }
  }

  // 2. 없어진 속성 제거
  for (const attr in oldProps) {
    if (!(attr in newProps)) {
      if (attr.startsWith("on") && typeof oldProps[attr] === "function") {
        const eventType = attr.toLowerCase().slice(2);
        removeEvent(target, eventType, oldProps[attr]);
      } else if (["checked", "disabled", "selected", "readOnly"].includes(attr)) {
        target[attr] = false;
      } else if (attr === "className") {
        target.removeAttribute("class");
      } else if (attr === "style") {
        for (const styleName in oldProps[attr]) {
          target.style[styleName] = "";
        }
      } else {
        target.removeAttribute(attr);
      }
    }
  }
}

export function updateElement(parentElement, newNode, oldNode, index = 0) {
  // 1. oldNode가 없는 경우 (새로운 노드 추가)
  if (!oldNode) {
    parentElement.appendChild(createElement(newNode));
    return;
  }

  const target = parentElement.childNodes[index];

  // 2. newNode가 없는 경우 (기존 노드 제거)
  if (!newNode) {
    parentElement.removeChild(target);
    return;
  }

  // 3. 노드 타입이 변경된 경우 (기존 노드 교체)
  if (newNode.type !== oldNode.type) {
    parentElement.replaceChild(createElement(newNode), target);
    return;
  }

  // 4. 텍스트 노드인 경우
  if (typeof newNode === "string" && newNode !== oldNode) {
    target.nodeValue = newNode;
    return;
  }

  // 5. 엘리먼트 노드인 경우
  if (newNode.type) {
    // 속성 업데이트
    updateAttributes(target, newNode.props, oldNode.props);

    // 자식 노드 업데이트
    const newChildren = newNode.children || [];
    const oldChildren = oldNode.children || [];

    // 새로운 자식들을 순회하며 업데이트 또는 추가
    for (let i = 0; i < newChildren.length; i++) {
      updateElement(target, newChildren[i], oldChildren[i], i);
    }

    // oldChildren이 newChildren보다 많을 경우, 초과하는 자식들을 제거
    for (let i = oldChildren.length - 1; i >= newChildren.length; i--) {
      target.removeChild(target.childNodes[i]);
    }
  }
}

학습 효과 분석

리액트를 한 번도 쓰지 않은 나에게 가상 DOM에 대한 개념을 알 수 있던 시간이었다. 그 가상 DOM이 실제로 변화하는 과정을 로그로 본 경험이 큰 배움이라고 생각한다. 또한, createElementnormalizeVNode 가 서로 상호작용하여 가상에서 실제 DOM 으로 구현되는 과정을 알 수 있었다.

과제 피드백

라이브러리를 직접 구현해볼 수 있는 경험이 좋았습니다.

리뷰 받고 싶은 내용

과제 피드백이라기보단 AI 활용법에 대한 의견을 듣고싶습니다.

이번 주차 과제도 AI 툴을 많이 이용했습니다. 기본적으로는 Gemini CLI 통해 과제를 구현했습니다. 그렇다보니 글로 내 의견을 피력해야하는게 너무나 중요하게 느껴졌습니다. 개발 및 과제 학습하면서 AI 툴을 튜터 및 실용적으로 활용하기 위한 고민 두 항목 적어봤습니다. 코치님의 경험이나 의견이 있으시면 답변 부탁드리겠습니다.

  1. AI의 답변을 단순히 수용하지 않고, 나만의 것으로 만들려면 어떤 질문을 던져야 할까?
  2. 프론트엔드 구현 중 막혔을 때 AI한테 도움을 요청하는 가장 좋은 방식은?

감사합니다.

과제 피드백

정석님 기대하고 있었습니다. 역시 기대한 만큼 필요한 부분들에 대해서 명확하게 코드로 잘 옮겨주신 것 같네요. 멋지십니다. 작성해주신 회고도 매우 인상깊었는데요. 볼드체로 작성해주신것처럼 과제를 하는데 있어서 뿐만 아니라 개발 업무 전반에 있어서 명확한 목적과 해결 과정을 러프하게나마 미리 정리하고 진행하는게 매우 생산성, 정확성에 있어서 중요하다고 생각해요. 항해에서 공부를 계속 해나가심에 따라 여러 도구를 획득하게 되실텐데요. 결국 가장 중요한건 이 도구를 잘 습득했다가 전부가 아니라 정석님이 회사에 가셔서 이 도구들을 잘 활용하는거지 않을까 싶습니다. 요런것들도 사실 연습이 필요한 부분이니까 매 과제과제마다 잘 챙겨주시면 좋을것 같아요.

질문 주신거 이어서 답변 드려보면요.

AI의 답변을 단순히 수용하지 않고, 나만의 것으로 만들려면 어떤 질문을 던져야 할까?

가장 먼저 해야할건 이 문제에 대해서 내 머리속에서 어느정도 구현을 해야 하는지 직접 계획을 세우고 검토해보는것 같아요. AI로 문제를 풀때도 동일하게요. 이 과정이 어느정도 내 기준에 납득이 되고 난 뒤에 작업이 진행이 되어야 코드의 일관성이나 문제를 푸는 방식이 일치가 되고 추후에 고치는 작업도 줄게 되는 것 같아요. 작업의 퀄리티와도 연결되는 부분이겠지만, 각 해당되는 작업들의 명확한 계획을 이야기를 들어보고 그 작업을 선별한 다음에 진행을 하는 것도 방법일 것 같구요! 다른 분들이 하시는 것처럼 코드의 구현을 어느정도 개입해서 주석 형태로 작성을 한 다음 코드 작성을 맡겨보는 것도 방법일 것 같구요.

개인적으로는 맥락은 프로젝트 전체를 이 부분에 있어서 너무 작업범위를 제안 받기보다는 작은 범위범위에 대해 작업을 진행하는 것이 좋은 것 같아요. 명확한 검증 후에 실제 코드로 반영하는 사이클을 만드는게 좋다고 생각하는 편인 것 같습니다.

두번째 질문으로 FE 구현을 막혔을 때 라고 해주셨는데요. FE 구현은 명확한 화면이 있다보니 멀티 모달 즉, 이미지같은것들을 최대한 활용해보는게 저는 좋더라구요. 글로 문제 상황을 전달하다보면 내 언어적인 문제도 명확하게 있다보니.. 여러번 반복해서 설명을 하는 것보다는 이미지로 딱 전달을 하는게 퀄리티 있는 답변을 얻는데 도움이 많이 되었던 것 같아요 ㅎㅎ 사용을 해보시면 좋을것 같습니다.

고생하셨고 다음주도 화이팅입니다 정석님 지켜볼게요