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

[11팀 박근백] [Chapter 1-2] 프레임워크 없이 SPA 만들기 #7

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

Conversation

Geunbaek
Copy link

@Geunbaek Geunbaek commented Dec 23, 2024

과제 체크포인트

기본과제

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

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

이벤트 위임

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

심화 과제

1) Diff 알고리즘 구현

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

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

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

과제 셀프회고

기술적 성장

  1. 이벤트 위임
  • 공통 조상에 이벤트 핸들러를 하나만 할당하여 자식 요소의 이벤트를 한꺼번에 다룰수 있다.
  • event.target을 이용하여 실제 이벤트가 발행한 자식을 알수 있습니다.
  1. 이벤트 캡처링
  • 이벤트가 일어난 타겟 엘리먼트까지 이벤트가 전파되는 과정
// 3번째 인자에 true 값을 줌으로써 캡처링 단계에서 이벤트를 캐치할 수 있다. ( default: false )

addEventListener(eventType, handler, true);
  1. 이벤트 버블링
  • 이벤트가 일어난 타겟 엘리먼트부터 root 까지 이벤트가 전파되는 과정
addEventListener(eventType, handler, false);
<body>
    <div id="root">
        root
        <div id="parents">
            parents
            <div id="child">
                child
            </div>
        </div>
    </div>
    <script>
        const root = document.getElementById("root")
        const parents = document.getElementById("parents")
        const child = document.getElementById("child")
       
        // case 1
        parents.addEventListener("click", () => console.log("parents"), true)
        child.addEventListener("click", () => console.log("child"))
        child.click()
        // parents
        // child

        // case 2
        parents.addEventListener("click", () => console.log("parents"))
        child.addEventListener("click", () => console.log("child"))
        child.click()
        // child
        // parents
    </script>
</body>
  • 각 element 에 event handler 를 부착하면 버블링이 되면서 모든 동일한 이벤트를 실행하겠지만 위임을 통해 처리되어 있어서 버블링되는 과정을 직접 구현해야한다.
function createSyntheticEvent(event) {
  let propagationStopped = false;
  return {
    type: event.type,
    target: event.target,
    currentTarget: event.target,
    preventDefault() {
      event.preventDefault();
    },
    stopPropagation() {
      propagationStopped = true;
      event.stopPropagation();
    },
    isPropagationStopped() {
      return propagationStopped;
    },
    nativeEvent: event,
  };
}

function handleGlobalEvent(event) {
  event = createSyntheticEvent(event);
  const eventType = event.type;
  const handlers = eventManager.get(eventType);
  
  // dom 트리 상위로 이벤트 전파
  let currentElement = event.target;
  while (currentElement && !event.isPropagationStopped()) {
    if (handlers.has(currentElement)) {
      const handler = handlers.get(currentElement);
      handler(event);
    }
    currentElement = currentElement.parentElement;
  }
}
  1. react 는 왜 VDOM을 사용했는가?
  • 선언적 UI

DOM 조작을 직접 하지 않고, "어떻게 보여야 하는지"만 선언

// 명령형 UI (jQuery 스타일)
$('#counter').text(count);
$('#counter').on('click', () => {
  count++;
  $('#counter').text(count);
  if (count > 10) {
    $('#message').show();
  }
});

// 선언적 UI (React 스타일)
function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <div onClick={() => setCount(count + 1)}>
        {count}
      </div>
      {count > 10 && <div>카운트가 10을 넘었습니다!</div>}
    </div>
  );
}
  • 서버 컴포넌트 통합
// ServerComponent.server.js
import { db } from './database';

async function ServerComponent() {
  const data = await db.query('SELECT * FROM users');
  
  return (
    <div>
      {data.map(user => (
        <ClientComponent 
          key={user.id} 
          user={user} 
        />
      ))}
    </div>
  );
}

// ClientComponent.client.js
"use client"
function ClientComponent({ user }) {
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <div onClick={() => setIsExpanded(!isExpanded)}>
      {user.name}
      {isExpanded && <UserDetails user={user} />}
    </div>
  );
}
  • virtual dom 은 실제 dom 보다 경량화된 자바스크립트 객체로 비교 알고리즘을 수행할때 비교적 빠르게 수행가능하다.

코드 품질

Map {
   eventType: WeakMap {
       element: handler
   }
}
  1. 이벤트 매니저 의 데이터 타입을 위와 같이 구현하여 diff 알고리즘 중 이벤트가 걸려있는 element의 부모가 제거되는 경우 그 하위에 적용되어있는 이벤트 핸들러도 함께 제거될수 있도록 하였습니다. ( WeakMap 의 특징을 활용 )
const eventManager = new Map();

export function addEvent(element, eventType, handler) {
  if (!eventManager.has(eventType)) {
    eventManager.set(eventType, new WeakMap());
  }

  const handlerCache = eventManager.get(eventType);
  handlerCache.set(element, handler);
}

export function removeEvent(element, eventType) {
  if (!eventManager.has(eventType)) {
    return;
  }

  const handlerCache = eventManager.get(eventType);
  handlerCache.delete(element);
}
  1. diff 알고리즘을 통해 실제 변경된 dom 이나 속성만을 변경하여 리렌더링 할 수 있도록 구현하였습니다.
function isChangedAttributes(originNewProps, originOldProps) {
  if (
    (!originNewProps && originOldProps) ||
    (originNewProps && !originOldProps)
  ) {
    return true;
  }

  const mergedProps = { ...originOldProps, ...originNewProps };
  return Object.keys(mergedProps ?? {}).some(
    (key) => mergedProps[key] !== originOldProps[key],
  );
}


function updateAttributes(target, originNewProps, originOldProps) {
  Object.keys(originOldProps ?? {}).forEach((key) => {
    if (isEvent(key)) {
      const eventType = key.slice(2).toLowerCase();
      removeEvent(target, eventType);
      return;
    }

    if (isClassName(key)) {
      target.removeAttribute("class");
      return;
    }

    target.removeAttribute(key);
  });

  Object.keys(originNewProps ?? {}).forEach((key) => {
    if (isEvent(key)) {
      const eventType = key.slice(2).toLowerCase();
      addEvent(target, eventType, originNewProps[key]);
      return;
    }

    if (isClassName(key)) {
      target.setAttribute("class", originNewProps[key]);
      return;
    }

    target.setAttribute(key, originNewProps[key]);
  });
}

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

  // 이전 노드가 삭제된 경우
  if (!newNode && oldNode) {
    parentElement.removeChild(parentElement.childNodes[index]);
    return;
  }

  // 노드의 타입이 다른경우
  if (newNode.type !== oldNode.type) {
    parentElement.replaceChild(
      createElement(newNode),
      parentElement.childNodes[index],
    );
    return;
  }

  // 텍스트 노드일 경우
  if (
    typeof newNode === "string" &&
    typeof oldNode === "string" &&
    newNode !== oldNode
  ) {
    parentElement.replaceChild(
      createElement(newNode),
      parentElement.childNodes[index],
    );
    return;
  }

  if (isChangedAttributes(newNode.props, oldNode.props)) {
    updateAttributes(
      parentElement.childNodes[index],
      newNode.props,
      oldNode.props,
    );
  }

  const newNodeChildren = newNode.children ?? [];
  const oldNodeChildren = oldNode.children ?? [];
  const maxChildren = Math.max(newNodeChildren.length, oldNodeChildren.length);

  for (let i = 0; i < maxChildren; i++) {
    updateElement(
      parentElement.childNodes[index],
      newNodeChildren[i],
      oldNodeChildren[i],
      i,
    );
  }
}

학습 효과 분석

  1. 추가 학습이 필요한 영역
  • react fiber 아키텍쳐를 상세히 공부해서 해당 프로젝트에 적용해보려고 했지만 생각보다 방대한 양에 일단 최대한 더 공부해보고 최소한의 기능 ( useState, useEffect ) 를 구현해볼 계획입니다.

과제 피드백

리뷰 받고 싶은 내용

작성중

@Geunbaek Geunbaek marked this pull request as draft December 23, 2024 06:53
@Geunbaek Geunbaek marked this pull request as ready for review December 23, 2024 13:01
@Geunbaek Geunbaek marked this pull request as draft December 23, 2024 13:03
const child = container.querySelector("#child");
child.click();
expect(clickHandler1).toHaveBeenCalledTimes(0);
});

Choose a reason for hiding this comment

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

tc 가져가서 저도 버블링 구현 트라이 해봐도 될까요? ㅎㅎ

Copy link
Author

Choose a reason for hiding this comment

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

네 !

Choose a reason for hiding this comment

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

우와... 테스트 코드도 짜셨군요!👍

Choose a reason for hiding this comment

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

최고네 최고야~!~! 🫶🏻🫶🏻

Copy link

Choose a reason for hiding this comment

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

테스트 코드까지.. 정말 대단하십니다👍👍👍

<form
className="mb-4 bg-white rounded-lg shadow p-4"
onSubmit={handleAddPost}
>

Choose a reason for hiding this comment

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

시맨틱 태그를 활용해서 가독성이나 seo를 챙기는 부분일까요?
spa관점으로 보면 form태그는 preventDefault를 해줘야하는 trade-off가 존재해서 의견이 궁금합니다.

Copy link
Author

Choose a reason for hiding this comment

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

아 이부분을 이렇게 구현한 이유는 FormData를 사용하기 위해서 였습니다.

preventDefault 를 해주는것에 대한 단점이 혹시 무엇인지 여쭤봐도 될까요??

Choose a reason for hiding this comment

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

장/단 이라기보다는, 시맨틱 태그의 기본적인 동작을 저지한다는 관점에서 html의 의도가 아닌 개발자의 의도를 입힌다고 생각됩니다.

여러 데이터를 폼으로 받지 않는 경우이니, 저는 form을 안썼을 것 같다고 생각이 들어서 의견을 여쭤봤어요 ㅎㅎ

코드에 문제가 있단 것은 절대 아닙니다!

Choose a reason for hiding this comment

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

확장 가능성이있는 포스트 등록기능일 수도 있으니 폼으로 관리하는 것도 좋은 방법일 수도 있겠네요.

Copy link
Author

Choose a reason for hiding this comment

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

저는 react-hook-form와 zod 를 주로 활용했었는데요.
사용자에게 입력을 받고 서버로 요청을 보내는 로직에서는 form 태그를 주로 활용해서 자연스레 저렇게 짠것 같기도 합니다 !

Comment on lines 14 to 30
if (virtualDOM === null) {
const element = createElement(normalizedVNode);
container.append(element);
virtualDOM = normalizedVNode;
setupEventListeners(container);
return;
}

if (container.firstChild) {
updateElement(container, normalizedVNode, virtualDOM);
virtualDOM = normalizedVNode;
} else {
const element = createElement(normalizedVNode);
container.append(element);
virtualDOM = normalizedVNode;
}
setupEventListeners(container);

Choose a reason for hiding this comment

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

  • setupEventListeners
  • oldVDOM 업데이트
    두개의 동작은 어떠한 상황에도 element 생성/업데이트 이후에 동작하는 것으로 보입니다.
    공통으로 사용되는 로직을 여러번 작성하는 부분을 개선할 수 있지 않을까 싶어요!
    (사실 제가 해당코드를 디버깅 중에 참고했었습니다. 제 생각에 개선해서 짰다고 생각하는데 한번 봐주시고 같이 얘기해도 좋을 것 같아요 ㅎㅎ)

Copy link
Author

Choose a reason for hiding this comment

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

이 코드를 이렇게 짠 이유는 준일 코치님께서 알려주셨던 https://jamie95.tistory.com/99 의 첫번째 항목을 고려해서 짠것이였는데요.

지금보니 불필요한 코드의 반복이 보여서 별로이긴 하네요..
준만님의 코드가 더 간결해보여서 좋긴한데 저 글의 첫번째 항목을 지키면서 불필요한 반복을 줄일 방법은 혹시 없을까요?

return {
type,
props,
children: children.flat(Infinity).filter(isRenderedVNode),

Choose a reason for hiding this comment

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

아하..저는 재귀 평탄화 함수를 직접짰는데,, 그냥 flat(무한뎁스)로 해결하면 되는거였군요 ㅋㅋ

Copy link
Author

Choose a reason for hiding this comment

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

아하 그래서 재귀에대한 말씀을 하셨었군요.


let currentElement = event.target;

while (currentElement && !event.isPropatation()) {

Choose a reason for hiding this comment

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

오타발견 입니다.ㅎㅎ
isPropatation -> isPropagation

}

const handlerCache = eventManager.get(eventType);
handlerCache.delete(element);

Choose a reason for hiding this comment

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

캐시의 용도가 무엇인지 궁금합니다~!

Copy link
Author

Choose a reason for hiding this comment

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

key: element
value: handler
의 구조를 가진 WeakMap 객체로 타겟 엘리먼트에 대한 핸들러를 가지고 있는 용도입니다.

Choose a reason for hiding this comment

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

덕분에 많이 알아갑니다,,,,bbb

const updateLikeUsers = (post) => {
const isLiked = post.likeUsers.some(
(user) => user.username === currentUser.username,
);

Choose a reason for hiding this comment

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

저랑 로직이 거의 비슷한데 저는 includes 메서드를 썼습니다.
문득 some을 사용해서 헬퍼함수를 넣어 처리하는것과 단순히 includes를 이용하는것 중 어떤게 좋은지 궁금하네요 ㅎㅎ
제 생각에는 이런 케이스에서는 includes가 가독성 + 코드간소화 를 모두 챙길 수 있는 것 같습니다.

결론으로, 복잡한 헬퍼함수 들어갈때는 array메소드 조합보다는 헬퍼함수를 받는 some, reduce같은 종류의 함수들이 좋을 것 같네요.

Copy link
Author

@Geunbaek Geunbaek Dec 26, 2024

Choose a reason for hiding this comment

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

likeUsers에 들어있는 데이터의 타입이 다른게 가장 큰 이유일거 같습니다.
저는 likeUsers에 이름뿐만이 아닌 user 객체를 넣었거든요.

단순히 string 만을 넣었다면 includes가 더 좋은것 같긴 합니다!

Choose a reason for hiding this comment

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

아~ 그렇군요!

Choose a reason for hiding this comment

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

오 저는 객체를 넣으면 some 메서드를 사용하면 되는 군요!
저는 user를 넣을까 하다가 객체를 넣으면 includes가 제대로 동작하지 않을 것 같아서? username 넣도록 우회했는데..👍

@Geunbaek Geunbaek changed the title WIP [11팀 박근백] [Chapter 1-2] 프레임워크 없이 SPA 만들기 [11팀 박근백] [Chapter 1-2] 프레임워크 없이 SPA 만들기 Dec 26, 2024
@Geunbaek Geunbaek marked this pull request as ready for review December 26, 2024 22:03
Copy link

@devJayve devJayve left a comment

Choose a reason for hiding this comment

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

근백님 모난 실력이지만 열심히 리뷰해봤습니다.. 여러가지 방법론들을 고민하고 적용하려고 노력했던 흔적들이 잘 느껴져서 좋았습니다 ㅎㅎ 테스트 케이스를 직접 작성해보고 테스트해보신 것도 정말 대단하십니다 저는 로그만 주구장창 찍는데.. 다음부터 저도 시도해보겠습니다 ~!!

src/lib/util.js Outdated
Comment on lines 1 to 9
export const isRenderedVNode = (vNode) =>
vNode !== null && vNode !== undefined && typeof vNode !== "boolean";

export const isTextVNode = (vNode) =>
typeof vNode === "string" || typeof vNode === "number";

export const isEvent = (key) => key.startsWith("on");

export const isClassName = (key) => key === "className";

Choose a reason for hiding this comment

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

자주 사용하는 함수에 대해 유틸 함수를 만들어준 부분 좋은 것 같습니다 ㅎㅎ 다만 util 네이밍 자체가 좀 범용적인 것 같아서 조금 더 구체화하면 좋을 것 같습니다
(당장 마땅히 생각나는 것 없지만.. NodeUtil? 한번 고민해보면 좋을 것 같습니다 .. )

}

function updateAttributes(target, originNewProps, originOldProps) {
Object.keys(originOldProps ?? {}).forEach((key) => {

Choose a reason for hiding this comment

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

여기서 null값의 예외를 처리해주기보단 함수 시그니쳐나 호출 부분에서 처리해주는 건 어떨까요? 지금과 같이 수행하면 추후에 originOldProps를 하단에서 다시 사용하게 될 때 null값에 대한 예외를 다시 처리해주어야 할 것 같아요

function updateAttributes(target, originNewProps = {}, originOldProps = {}) {}

updateAttributes(
      parentElement.childNodes[index],
      newNode.props || {}, 
      oldNode.props || {},
    );

Copy link
Author

Choose a reason for hiding this comment

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

혹시 이렇게 작성하는걸 선호하시는 이유를 말씀해주실수 있을까요??

Choose a reason for hiding this comment

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

�지금 당장에는 문제가 없지만 originOldProps를 하단에서 다시 사용하는 경우에 또 null check를 해야하는 경우가 생길 수 있다고 고려했습니다! 예를 들어 위와 같이 사용한 뒤에,

Object.keys(originNewProps ?? {}).forEach((key) => {
  oldProp = OriginOldProps[index]; // OriginOldProps에 대한 null check 다시 해야 함

이런 경우가 생길 수 있어서 디버깅이나 오류 발생 가능성을 줄이기 위해 초기에 선언해주면 어떨까? 하는 측면에서 제안드렸습니다

Copy link
Author

Choose a reason for hiding this comment

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

아 이해했습니다.
상세한 예시까지 알려주셔서 감사합니다 !

@@ -3,8 +3,21 @@ import { createElement } from "./createElement";
import { normalizeVNode } from "./normalizeVNode";
import { updateElement } from "./updateElement";

let virtualDOM = null;

Choose a reason for hiding this comment

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

virtualDOM을 전역 객체로 사용했을 때 특별한 문제는 없었을까요? 저는 root container의 내부 변수로 할당해주었는데 container가 GC에 의해 없어질 때 같이 제거될 수 있기 때문이었어요. 그리고 slack에서 다른 분들 얘기 나눈 것들 보면 virtualDOM을 WeakMap 자료구조를 사용해서 위와 마찬가지로 GC에 의해 자동적으로 제거될 수 있게 하는 방법도 있더라구요 ㅎㅎ 다른 의도가 있는게 아니라면 이런 방법들도 고려해봐도 좋을 것 같아요!

Comment on lines 40 to 41
root?.removeEventListener(eventType, handleGlobalEvent);
root?.addEventListener(eventType, handleGlobalEvent);

Choose a reason for hiding this comment

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

여기서 root에 옵셔널 체이닝을 사용한 이유가 있을까요? root가 없어도 이벤트 리스너 등록/삭제 실패가 프로그램 흐름에 문제는 없기 때문인가요?

Copy link
Author

Choose a reason for hiding this comment

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

습관적으로 적었다가 지우지 않은것 같네요..
이런 곳을 볼때마다 정말 TS의 중요성을 다시금 느끼게 되는것 같아요.

Comment on lines 4 to 6
if (!isRenderedVNode(vNode)) {
return "";
}

Choose a reason for hiding this comment

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

만약 의도가 "렌더링이 가능한 가상 돔 노드가 아닌 경우"에 대한 거라면 isRenderableVNode라는 네이밍은 어떠신가요 ?!

Copy link
Author

Choose a reason for hiding this comment

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

좋은것 같습니다 ! 영어의 부족함이 여기서 탄로나는 ....

Comment on lines 3 to 21
function createSyntheticEvent(event) {
let propagationStopped = false;
return {
type: event.type,
target: event.target,
currentTarget: event.target,
preventDefault() {
event.preventDefault();
},
stopPropagation() {
propagationStopped = true;
event.stopPropagation();
},
isPropagationStopped() {
return propagationStopped;
},
nativeEvent: event,
};
}

Choose a reason for hiding this comment

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

합성 이벤트를 활용하셨네요 !! 저는 target -> 실제로 이벤트가 발생한 가장 안쪽 요소, currentTarget -> 현재 이벤트 핸들러가 동작하고 있는 요소로 이해했습니다.
그런데 현재 코드에선 target과 currentTarget이 동일한 값을 참조하고 있는데 이벤트 버블링 과정에서 currentTarget을 업데이트해주면 좋을 것 같아요!!

if (handlers.has(currentElement)) {
        syntheticEvent.currentTarget = currentElement;

Copy link
Author

Choose a reason for hiding this comment

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

오 감사합니다 !
currentTarget을 사용을 안하고 있다보니 신경을 안썼네요.

Comment on lines 30 to 34
if (handlers.has(currentElement)) {
const handler = handlers.get(currentElement);
handler(event);
}
currentElement = currentElement.parentElement;

Choose a reason for hiding this comment

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

핸들러 함수가 존재하지 않거나 핸들러 함수 실행에 실패했을 때? 에 대한 예외처리도 있으면 좋을 것 같아요~!

Suggested change
if (handlers.has(currentElement)) {
const handler = handlers.get(currentElement);
handler(event);
}
currentElement = currentElement.parentElement;
if (handlers.has(currentElement)) {
const handler = handlers.get(currentElement);
try {
handler(event);
} catch (error) {
// 커스텀한 에러 객체 생성도 좋은 방법인 것 같습니다
}
}
currentElement = currentElement.parentElement;

Copy link
Author

Choose a reason for hiding this comment

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

handler의 유무는 위에서 체크하고 있어 괜찮을것 같아보이긴 한데 혹시 함수실행에 실패했을때에는 어떠한 경우가 있을까요?

Choose a reason for hiding this comment

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

handlers에 has로 currentElement 유무를 확인한 뒤 get을 해주어 handler를 가져오다보니 내부적으로 유효하지 않은 값이 할당되어있을 가능성을 고려해봤는데 잘못 생각한 것 같네요..ㅎ 다만 handlers.has → GC 수집 → handlers.get 이런 흐름도 예측해볼 수 있긴한데 너무 이론적인 엣지 케이스인 것 같아 그냥 무시하셔도 될 듯 합니다 ㅋㅋ..

함수 실행 실패는 handler 수행 동작 과정에서 어떤 오류가 발생해 실패한 경우 프로그램에 지장을 주지 않게 하기 위해 try catch로 적절하게 예외처리하면 좋지 않을까 하는 생각이었어요! 저도 구체적으로 떠오르진 않아 GPT한테 물어봤는데,

  • 함수 파라미터 문제
  • 함수 내부에서 참조하는 외부 값이 변경 또는 삭제
  • this 컨텍스트 바인딩 문제
  • 이밴트 객체 속성 접근 문제
    뭐 이런 것들이 있을 수 있다고는 합니다..!

Choose a reason for hiding this comment

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

역시 도운님 ,,, 예시 최고 bb

const child = container.querySelector("#child");
child.click();
expect(clickHandler1).toHaveBeenCalledTimes(0);
});

Choose a reason for hiding this comment

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

우와... 테스트 코드도 짜셨군요!👍

Comment on lines 3 to 21
function createSyntheticEvent(event) {
let propagationStopped = false;
return {
type: event.type,
target: event.target,
currentTarget: event.target,
preventDefault() {
event.preventDefault();
},
stopPropagation() {
propagationStopped = true;
event.stopPropagation();
},
isPropagationStopped() {
return propagationStopped;
},
nativeEvent: event,
};
}

Choose a reason for hiding this comment

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

우와.. stopPropagation을 사용 유무를 체크하시려고 이벤트 객체를 커스텀해서 내보내는 건가요??
진짜 대단하�시네용....👍

Copy link
Author

Choose a reason for hiding this comment

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

네 맞습니다!

Comment on lines 23 to 36
function handleGlobalEvent(event) {
event = createSyntheticEvent(event);
const eventType = event.type;
const handlers = eventManager.get(eventType);

let currentElement = event.target;
while (currentElement && !event.isPropagationStopped()) {
if (handlers.has(currentElement)) {
const handler = handlers.get(currentElement);
handler(event);
}
currentElement = currentElement.parentElement;
}
}

Choose a reason for hiding this comment

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

이벤트 버블링 구현까지...👍

const updateLikeUsers = (post) => {
const isLiked = post.likeUsers.some(
(user) => user.username === currentUser.username,
);

Choose a reason for hiding this comment

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

오 저는 객체를 넣으면 some 메서드를 사용하면 되는 군요!
저는 user를 넣을까 하다가 객체를 넣으면 includes가 제대로 동작하지 않을 것 같아서? username 넣도록 우회했는데..👍

Copy link

@osohyun0224 osohyun0224 left a comment

Choose a reason for hiding this comment

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

안녕하세요 근백님 ~! 담당 학습메이트 오소현입니다 :)

요번주 휴일없이,,,⭐️ 크리스마스에도 달려주셨던 근백님 정말 고생많으셨습니다 :0 최고최고!! 다른 분들 코드 리뷰도 너무 적극적으로 해주시고, 이슈가 공유되면 바로 달려와 도움 주시는 근백님 덕분에 든든합니다!! 이번주도 과제 엄청 잘해주셨네요,, 특히 도운-준만-원정님께서 정말 꼼꼼하게 리뷰를 남겨주셔서 저도 리뷰 남겨주신 내용들 보면서 근백님의 코드가 인기쟁이인 이유를 알았습니다.. 굿굿 역시 최고최고 이번주 저랑 거의 zep에 같이 계셨는데 진짜 고생많으셨습니다 !! ❤️

또한 PR에도 이벤트 위임과 버블링을 가지고 이벤트 관리 방법엄청 잘 설명해주시고, 이를 통해 리스너의 수를 최소화하는 것도 잘 정리해주셔서 이해하기 쉬웠습니다! 잘 정리해주셔서 너무 감사해요 :) 이번주 내내 fiber로 구현하시는 걸 도전하셨는데 근백님은 반드시 !! 해내실 것이라고 ㅎㅎ 생각합니다 ^0^

부족한 실력이지만 요번주도 한번 리뷰를 남겨보았어요! 다른 분들 리뷰 내용에 공감가는 것도 많고 bb 최고였습니다 근백님께서 요번주 과제를 한번더 되돌아보는 계기가 되었으면 좋겠어요🍀🍀

이번주 과제도 너무 수고많으셨고, 앞으로의 과제도 화이팅입니다! 근백님의항상 앞으로가 기대됩니다 ㅎㅎㅎ 이번주 고생하셨어요 :)

Comment on lines 56 to 61
addPost(state, content) {
const { currentUser } = state;

const newPost = {
id: state.posts.length + 1,
author: currentUser.username,

Choose a reason for hiding this comment

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

state.posts.length + 1 이렇게 구현하면 나중에 삭제 추가되면 포스트 추가 시 ID가 중복될 것 같네용

Suggested change
addPost(state, content) {
const { currentUser } = state;
const newPost = {
id: state.posts.length + 1,
author: currentUser.username,
addPost(state, content) {
const newPostId = state.posts.reduce((maxId, post) => Math.max(post.id, maxId), 0) + 1;
const newPost = {
id: newPostId,
author: state.currentUser.username,

요렇게 바꿔보면 어떨까 싶습니다!

src/lib/util.js Outdated
Comment on lines 4 to 9
export const isTextVNode = (vNode) =>
typeof vNode === "string" || typeof vNode === "number";

export const isEvent = (key) => key.startsWith("on");

export const isClassName = (key) => key === "className";

Choose a reason for hiding this comment

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

제가 도운님 리뷰에 이어서 한번 추천해보겠습니다 ㅎㅎ

isTextNodeVNode , 주어진 키가 "className"인지 알아보니까 isClassNameProp ? 어떠신가요 !

return {
type: event.type,
target: event.target,
currentTarget: event.target,

Choose a reason for hiding this comment

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

약간 여기서 currentTarget이 항상 event.target과 같게 설정되는것 같은데 이벤트 핸들러가 호출될 때 currentTarget이 설정되어야 하니까 요 부분을 handleGlobalEvent로 옮기는 건 어떤가요??

Comment on lines 30 to 34
if (handlers.has(currentElement)) {
const handler = handlers.get(currentElement);
handler(event);
}
currentElement = currentElement.parentElement;

Choose a reason for hiding this comment

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

역시 도운님 ,,, 예시 최고 bb

}

const handlerCache = eventManager.get(eventType);
handlerCache.delete(element);

Choose a reason for hiding this comment

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

덕분에 많이 알아갑니다,,,,bbb

Comment on lines 51 to 106
export function updateElement(parentElement, newNode, oldNode, index = 0) {
// 새로운 노드가 추가될 경우
if (newNode && !oldNode) {
parentElement.appendChild(createElement(newNode));
return;
}

// 이전 노드가 삭제된 경우
if (!newNode && oldNode) {
parentElement.removeChild(parentElement.childNodes[index]);
return;
}

// 노드의 타입이 다른경우
if (newNode.type !== oldNode.type) {
parentElement.replaceChild(
createElement(newNode),
parentElement.childNodes[index],
);
return;
}

// 텍스트 노드일 경우
if (
typeof newNode === "string" &&
typeof oldNode === "string" &&
newNode !== oldNode
) {
parentElement.replaceChild(
createElement(newNode),
parentElement.childNodes[index],
);
return;
}

if (isChangedAttributes(newNode.props, oldNode.props)) {
updateAttributes(
parentElement.childNodes[index],
newNode.props,
oldNode.props,
);
}

const newNodeChildren = newNode.children ?? [];
const oldNodeChildren = oldNode.children ?? [];
const maxChildren = Math.max(newNodeChildren.length, oldNodeChildren.length);

for (let i = 0; i < maxChildren; i++) {
updateElement(
parentElement.childNodes[index],
newNodeChildren[i],
oldNodeChildren[i],
i,
);
}
}

Choose a reason for hiding this comment

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

이 함수 다 좋네요 bb 그런데 중복되는 로직이 있어보이고, 텍스트 노드 업데이트 로직을 textContent 변경으로 구현해보면 더 좋을것 같아서 코드를 제안해봅니다~!

Suggested change
export function updateElement(parentElement, newNode, oldNode, index = 0) {
// 새로운 노드가 추가될 경우
if (newNode && !oldNode) {
parentElement.appendChild(createElement(newNode));
return;
}
// 이전 노드가 삭제된 경우
if (!newNode && oldNode) {
parentElement.removeChild(parentElement.childNodes[index]);
return;
}
// 노드의 타입이 다른경우
if (newNode.type !== oldNode.type) {
parentElement.replaceChild(
createElement(newNode),
parentElement.childNodes[index],
);
return;
}
// 텍스트 노드일 경우
if (
typeof newNode === "string" &&
typeof oldNode === "string" &&
newNode !== oldNode
) {
parentElement.replaceChild(
createElement(newNode),
parentElement.childNodes[index],
);
return;
}
if (isChangedAttributes(newNode.props, oldNode.props)) {
updateAttributes(
parentElement.childNodes[index],
newNode.props,
oldNode.props,
);
}
const newNodeChildren = newNode.children ?? [];
const oldNodeChildren = oldNode.children ?? [];
const maxChildren = Math.max(newNodeChildren.length, oldNodeChildren.length);
for (let i = 0; i < maxChildren; i++) {
updateElement(
parentElement.childNodes[index],
newNodeChildren[i],
oldNodeChildren[i],
i,
);
}
}
export function updateElement(parentElement, newNode, oldNode, index = 0) {
if (!oldNode && newNode) {
parentElement.appendChild(createElement(newNode));
return;
}
if (!newNode) {
parentElement.removeChild(parentElement.childNodes[index]);
return;
}
const childElement = parentElement.childNodes[index];
if (newNode.type !== oldNode.type) {
parentElement.replaceChild(createElement(newNode), childElement);
return;
}
if (typeof newNode === "string" && newNode !== oldNode) {
childElement.textContent = newNode;
}
if (isChangedAttributes(newNode.props, oldNode.props)) {
updateAttributes(childElement, newNode.props, oldNode.props);
}
const maxLength = Math.max(newNode.children?.length || 0, oldNode.children?.length || 0);
for (let i = 0; i < maxLength; i++) {
updateElement(childElement, newNode.children[i], oldNode.children[i], i);
}
}

const child = container.querySelector("#child");
child.click();
expect(clickHandler1).toHaveBeenCalledTimes(0);
});
Copy link

Choose a reason for hiding this comment

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

테스트 코드까지.. 정말 대단하십니다👍👍👍

Comment on lines +17 to +24
const handleToggleLike = () => {
if (!loggedIn) {
alert("로그인 후 이용해주세요");
return;
}
toggleLike(id);
};

Copy link

Choose a reason for hiding this comment

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

toggleLike 과 관련없는 loggedIn 상태를 외부에서 처리하는게 훨씬 가독성이 좋고 재사용성이 좋아지네요👍👍

Comment on lines +42 to +45
if (isClassNameProps(key)) {
$el.setAttribute("class", props[key]);
return;
}
Copy link

Choose a reason for hiding this comment

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

유효성체크용 util 함수를 정말 잘 활용하셨네요!!! 저도 놓친부분이 많아서 반성하고 갑니다,,

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.

6 participants