Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7팀 김영우] [Chapter 1-2] 프레임워크 없이 SPA 만들기 #22

Open
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

ywkim95
Copy link

@ywkim95 ywkim95 commented Dec 25, 2024

과제 체크포인트

기본과제

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

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

이벤트 위임

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

심화 과제

1) Diff 알고리즘 구현

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

2) 포스트 추가/좋아요 기능 구현

  • 비사용자는 포스트 작성 폼이 보이지 않는다
  • 비사용자는 포스트에 좋아요를 클릭할 경우, 경고 메세지가 발생한다.
  • 사용자는 포스트 작성 폼이 보인다.
  • 사용자는 포스트를 추가할 수 있다.
  • 사용자는 포스트에 좋아요를 클릭할 경우, 좋아요가 토글된다.

과제 셀프회고

이번주차는 개인적인 몸상태가 좋지 않은 상태에서 시작한 상태였기 때문에 기본과제만 통과하자는 마인드로 임하였습니다.
가장 힘들었던 부분은 이벤트 매니저 부분으로 해당 이벤트들을 어떻게 관리해야될지 감이 오지않았던 것이 가장 컸습니다.
대부분의 파일들을 제공해주신 상태였기 때문에 필요한 내용만 작성하면 되었지만 위임 자체에 대한 개념이 거의 없는 상태이기도 하였고, 시간을 많이 투자할 수 없었기 때문에 많이 막힌 것으로 생각합니다.

또한 제가 약한 부분에 대해서 잘 알게 되었는데, 여러 개의 파일을 타고 들어가면 어디서부터 시작되었는지 까먹는다는 것을 알게 되었고 익숙해지기 위하여, 몇 가지 방법을 강구해봐야할 것 같습니다.

기술적 성장

새로 학습한 개념

  1. 이벤트 위임
  • 지난 1주차에서 이벤트 위임이라는 개념에 대해서 간략히 배웠지만 정확하게 사용한 것이 아니라고 생각을 하였습니다. 그렇기 때문에 이번 주차를 통하여 이벤트 위임을 확실하게 배우고 싶었습니다.
  • 상위 컴포넌트에서 하위 컴포넌트의 이벤트를 등록하여 하위 컴포넌트가 많아지더라도 상위컴포넌트에 등록이 되어있어 하위 컴포넌트 자체에 이벤트를 등록하지 않아도 되어 메모리효율성을 높일 수 있게 되는 개념이기 때문에 프론트엔드 최적화시 많이 사용할 것이라고 생각이 듭니다.
export function setupEventListeners(root) {
  if ($root && $root !== root) {
    events.forEach((value) => {
      $root.removeEventListener(value.eventType, value.handler);
    });
  }

  if ($root !== root) {
    $root = root;
    events.forEach((value, key) => {
      const elementMap = events.get(key);
      elementMap.forEach((handlers, eventType) => {
        $root.addEventListener(eventType, handleEvent);
      });
    });
  }
}
  1. 상태 관리 라이브러리 없이 상태 관리 구현
  • 제공해주신 코드를 토대로 개념을 조금이나마 익혔습니다. createStore를 통하여 state, setState, getState, action 등으로 직접 구현하신 내용을 읽어보았고, 기존의 상태관리라이브러리들이 어떻게 동작하는지 조금이나마 이해할 수 있었습니다.
  1. 노드(object)에서 HTML로의 변환하는 법
  • 처음에 리액트가 JSX와 babel 등을 통하여 html문서로 변환한다는 것은 알고 있었으나, 정확히 어떠하게 동작하는지 전혀 모르고 있는 상황이었습니다. JSX 문법 자체가 가지고있는 특징이나 그걸 어떻게 해야 변환을 할 수 있는지, props를 어떻게 하면 그대로 가지고올 수 있는지 등에 대해서 배웠고, 실전(리액트)에서 어떻게 구동되는지 흐름을 파악할 수 있을 듯합니다.
export function createElement(vNode) {
  if (
    vNode === null ||
    typeof vNode === "undefined" ||
    typeof vNode === "boolean"
  ) {
    return document.createTextNode("");
  }
  if (typeof vNode === "string" || typeof vNode === "number") {
    return document.createTextNode(`${vNode}`);
  }

  if (typeof vNode.type === "function" && typeof vNode === "object") {
    throw new Error();
  }

  if (Array.isArray(vNode)) {
    const $fragment = document.createDocumentFragment();
    vNode.forEach((child) => {
      $fragment.appendChild(createElement(child));
    });
    return $fragment;
  }

  const $el = document.createElement(vNode.type);
  updateAttributes($el, vNode.props);
  if (Array.isArray(vNode.children)) {
    vNode.children.forEach((child) => {
      $el.appendChild(createElement(child));
    });
  }
  return $el;
}

기존 지식의 재발견/심화

  1. JSX를 통한 props 관리
  • 위의 내용과 어느정도 이어지는 이야기 입니다. 이벤트 위임 방식으로 이벤트리스너들이 등록되는 것인줄 몰랐고, 다른 props들도 자연스럽게 변환된다고만 알고있었는데 기존의 attribute를 삭젲하고 새로운 attribute를 등록하는 과정이 있을줄은 몰랐습니다. 저는 컴포넌트가 수정되면 같이 GC를 통하여 attribute가 삭제되는 줄알았으나 최적화를 통하여 필요 없는 attribute만 직접 제거하고 현재 없는 attribute를 등록하는 과정으로 동작하는 것을 알게되었습니다.

구현 과정에서의 기술적 도전과 해결

  1. Map을 통한 이벤트 관리
  • Map을 factory를 통한 변환을 제외한 곳에서는 써본적이 없어 이벤트 관리를 하는 부분에서 Map 사용을 한 번 시도해보고 싶었습니다. 기존에는 배열을 통하여 관리를 진행하려고 생각하고 있었고, 실제 Q&A 시간에도 배열을 통하여 이벤트를 관리하는 내용이 나와 해당 방법으로 먼저 구현을 해보았습니다. 성공적으로 배열을 시도한 뒤 처음에는 WeakMap으로 시도를 하려고하였으나 forEach와 같은 반복문을 사용할 수 없었기 때문에 Map으로 변경하였습니다. Map에서 어떤 값들을 key와 value로 설정해야할지 처음부터 여러가지를 시도하게 되었고, key는 element, value에는 또다른 Map을 넣어 내부 Map에서 이벤트를 저장하게 설계하였습니다.
const events = new Map();
let $root = null;

function handleEvent(event) {
  const element = event.target;
  const elementMap = events.get(element);
  if (!elementMap) return;

  const handlers = elementMap.get(event.type);
  if (!handlers) return;

  handlers.forEach((handler) => handler(event));
}

export function setupEventListeners(root) {
  // TODO: 이벤트 함수를 가져와서 한 번에 root에 이벤트를 등록한다.
  if ($root && $root !== root) {
    events.forEach((value) => {
      $root.removeEventListener(value.eventType, value.handler);
    });
  }

  if ($root !== root) {
    $root = root;
    events.forEach((value, key) => {
      const elementMap = events.get(key);
      elementMap.forEach((handlers, eventType) => {
        $root.addEventListener(eventType, handleEvent);
      });
    });
  }
}

export function addEvent(element, eventType, handler) {
  if (events.has(element)) return;

  events.set(element, new Map());

  if ($root) {
    $root.addEventListener(eventType, handleEvent);
  }

  const elementMap = events.get(element);
  if (!elementMap.has(eventType)) {
    elementMap.set(eventType, new Set());
  }
  elementMap.get(eventType).add(handler);
}

export function removeEvent(element, eventType, handler) {
  const elementMap = events.get(element);
  if (!elementMap) return;

  const handlers = elementMap.get(eventType);
  if (!handlers) return;

  handlers.delete(handler);

  if (handlers.size === 0) {
    elementMap.delete(eventType);
  }

  if (elementMap.size === 0) {
    events.delete(element);
    if ($root) {
      $root.removeEventListener(eventType, handleEvent);
    }
  }
}
  1. WeakMap 사용을 통한 oldNode 관리
  • renderElement 함수를 사용시 기존 컴포넌트가 있는경우 updateElement를 실행하도록되어있었는데 이부분에서 말썽이 일어났습니다. 기존에 있는 oldNode부분이 이미 변환이 되어 HTMLElement로 표기가 되었고 oldNode의 초기 virtualNode가 필요하였기 때문에 WeakMap을 통하여 oldVirtualNode를 가지고있도록 하였습니다. WeakMap은 Map과 달리 key에 null을 할당하면 value도 없어지기 때문에 가벼운 데이터를 유지해야되는 SPA와 잘어울려 보여서 선택을 하였습니다.
const containerMap = new WeakMap();

export function renderElement(vNode, container) {
  // 최초 렌더링시에는 createElement로 DOM을 생성하고
  // 이후에는 updateElement로 기존 DOM을 업데이트한다.
  // 렌더링이 완료되면 container에 이벤트를 등록한다.
  // vNode를 정규화한 다음에 createElement로 노드를 만들고, container에 삽입하고, 이벤트를 등록합니다.
  const normalizedVNode = normalizeVNode(vNode);

  if (containerMap.has(container)) {
    const oldNode = containerMap.get(container);
    updateElement(container, normalizedVNode, oldNode);
  } else {
    const element = createElement(normalizedVNode);
    container.appendChild(element);
  }

  setupEventListeners(container);
  containerMap.set(container, normalizedVNode);
}

코드 품질

개인적으로 리팩토링이 필요한 부분이 꽤나 많다고 생각이 듭니다. 이번에는 시간이 모자라 구현에 급급하였기 때문에 updateElement.jseventManager.js, createElement.js 등에서 조금 더 리팩토링이 필요하다고 생각이 듭니다.

학습 효과 분석

확실히 모르는 영역에 대해서 배운것이 가장 컸던것 같습니다. 이전에도 적어둔 이벤트 위임에 대해서 가장 크게 배웠습니다. 위임된 이벤트에 대한 addEventListener, removeEventListener를 관리하고 설정하는 방법에 대해서 더 자세히 알고 싶어졌습니다.

학습을 계속 진행하다보니 모든 부분에서 다 부족해보이고 모자라다는 느낌을 많이 받았습니다. 자바스크립트에 대한 전반적인 이해 부족이 문제가 아닐까 생각이 듭니다. 이번 주차의 추가 학습이 필요하다고 여겨지는 영역은 diff알고리즘 영역에 대해서 더 공부해봐야겠다는 생각이 듭니다.

이벤트 위임이나 상태관리에 대한 영역은 실무에서 직접 적용할 수 있을지는 조금 더 생각을 해봐야할 것 같습니다.

과제 피드백

과제에서 모호하거나 애매했던 부분

처음에 잘못 이해하고 있었던 영역이 createElement와 normalizeVNode 영역이었습니다. 두 개의 함수를 구분하여 적용한다는 생각을 잘 못하고 계속 하나로 합쳐서 구현한다는 생각을 지속적으로 하다보니 두 함수에서 겹치는 영역이 많아져서 동일한 로직이 두 번 적용되는 버그가 있었습니다. 이러한 부분을 나중에 깨달아 분리하는 작업을 진행하게 되었고 왜 그랬을지 생각을 해보았는데 노션의 설명에 있는 조건 부분이 비슷하게 작성되어있어 헷갈린것 같았습니다.
지금은 각각의 함수가 각자의 로직만을 실행하도록 정상적으로 적용해두었습니다.

과제에서 좋았던 부분

여러 가지 함수에 대해서 나누어 구현하는 것이 좋았습니다. 저는 항상 처음 구현할 때 한 번에 구현하여 나중에 리팩토링 시 함수를 나누는 것이 어려운 작업 중에 하나였는데 미리 나누어진 부분을 통하여 내용을 채운 뒤 확인하니 조금 더 분리를 명확하게 할 수 있어서 좋았습니다.

리뷰 받고 싶은 내용

  1. src/lib/createElement.js에서 updateAttributes라는 함수가 있는데 src/lib/updateElement.js의 addNewEvents와 어느정도의 유사성이 보인다고 생각이 들어 공통된 영역에 가깝다고 생각하는데 만약 따로 함수로 뺀다면 lib에 파일을 생성하는 것이외에 보편적으로 사용하는 파일의 위치가 따로 있을까요?
  2. 현재 대부분의 로직에서 if를 통하여 early return을 진행하고 있는데 이 방법이외에 또 다르게 구현하는 방법이 있을까요? 지금 당장 생각나는 방식은 switch 문을 통하여 분기처리를 하는 방법이라고 생각이 드는데 만약 switch의 조건과 다르게 분기처리 하려면 결국 아래에 if로 분기처리를 추가적으로 해야된다는 생각이 들어서 기존의 방법보다 오히려 더 가독성이 떨어져보일 거라는 생각도 듭니다.

(추가)

  • 과연 Map > Map > Set이 과연 괜찮은 구조인지 잘 모르겠습니다. Map > Set으로 구현하거나 Map > Map으로 구현할 수도 있지 않을까? 라는 생각도 들고있는데… 아니면 다른 구조의 방식도 더 좋은게 있을까 궁금합니다.

Choose a reason for hiding this comment

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

어떤 액션이 일어나는지 직관적으로 알 수 있어 좋았지만 action에 관련된 내용이 globalStore.js에 들어가면 일관성있어 설계상 더 좋을 것 같습니당!

Copy link
Author

Choose a reason for hiding this comment

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

감사합니다 지금보니 확실히 globalState로 넣어서 처리하는게 더 깔끔해보이네요 피드백 감사드려요!

Choose a reason for hiding this comment

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

post.jsx 와 마찬가지로 globalStore.js에 액션을 정리하면 일관성있을 것 같아요!!

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.

2 participants