[항해 플러스 프론트엔드 4기] 2주차 과제 회고

원정·2024년 12월 30일
4
post-thumbnail

2주차 과제도 <프레임워크 없이 SPA 만들기>로, 주제는 이벤트 관리와 렌더링 성능 최적화다.

목표는

  1. 가상돔을 이해하고 사용하기.
  2. 가상돔을 이용하여 이벤트 관리를 최적화하기.
  3. diff 알고리즘을 이용하여 불필요한 렌더링 줄이기.

이다.

💰 가상돔


가상돔은 DOM을 객체로 변환한 것이다.
완전히 똑같이 객체로 만든 건 아니고 DOM 조작에 필요한 속성만 골라서 만든 가벼운 복사본이다.
가상돔을 쓰는 이유를 검색해보면 대부분 '효율적인 렌더링을 위해서'라고 나온다.
가상돔이 어떻게 렌더링을 효율적으로 해주는 걸까?

  1. 사용자가 브라우저에 URL 입력 후 전송
  2. 서버에서 바이트 코드로 된 HTML 파일 반환
  3. 바이트 코드 -> 문자열 -> 문자열 토큰 -> 노드 -> DOM 생성
  4. DOM 생성 중 CSS 파일을 만났다면 -> 바이트 코드 -> 문자열 -> 문자열 토큰 -> 노드 -> CSSOM 생성
  5. DOM + CSSOM = 렌더 트리 생성
  6. 레이아웃 계산 후 페인팅
  7. DOM 생성 중 JavaScript 파일을 만났다면 자바스크립트 엔진에서 AST 생성 및 실행
  8. JavaScript에서 노드 추가/삭제, 레이아웃 스타일 변경 또는 브라우저 리사이징 등을 하면 리렌더링(리플로우 + 리페인팅) 발생
  9. 배경색이나 폰트 색상 등 레이아웃에 영향을 주지 않는 변화는 리페인트만 실행

브라우저의 렌더링 과정을 간략하게 살펴보면 위와 같다.
이 과정에서 리렌더링이 가장 많은 비용이 발생한다고 한다.

SPA는 하나의 HTML 파일에서 JavaScript로 DOM API를 사용해서 화면을 조작한다.
애플리케이션의 규모가 커질 수록 조작할 일도 많아질 것이고, 그럴 수록 브라우저에게 더 많은 부하가 걸릴 것이다.

브라우저의 부하를 줄이고자 diff 알고리즘을 사용하여 기존 가상돔과 새로운 가상돔을 비교하여 변경된 부분만 업데이트한다.

여기까지 찾아보니 의문이 들었다.

실제 렌더링을 효율적으로 하는 건 diff 알고리즘을 이용하는 거면 가상돔은 왜 쓰는 걸까?
diff 알고리즘을 써서 변경할 부분을 찾고, 그 부분만 DOM API로 업데이트 해주면 렌더링 효율도 좋아지고 가상돔을 쓸 필요가 없는 거 아닐까?

실제로 성능면으로만 봤을 때는 바닐라 자바스크립트가 리액트보다 뛰어나다고 한다.

리액트로 프로젝트 경험이 없는 나의 의견이지만, 리액트를 써서 얻는 가장 큰 이점은 선언형 프로그래밍을 할 수 있다는 것이다.

탭을 클릭하면 선택한 탭은 활성화된 스타일을 넣고, 나머지 탭은 비활성화된 스타일을 넣어줘야 한다고 해보자.

<div id="parent">
 <button class="tab">Tab 1</button>
 <button class="tab">Tab 2</button>  
 <button class="tab">Tab 3</button>
</div>

<script>
const tabClickHandler = (e) => {
 if(e.target.classList.contains("tab")){
   document.querySelectorAll(".tab").forEach(tab => {
     tab.classList.remove("active");
   });
   e.target.classList.add("active");
 }
};

document.querySelector("#parent").addEventListener("click", tabClickHandler);
</script>

바닐라 자바스크립트는 명령형 프로그래밍으로 모든 과정을 코드로 작성해줘야 한다.
DOM을 선택하여 이벤트 리스너를 할당하고, tab 클래스를 모두 순회하며 active 클래스를 삭제해주고, 클릭한 대상에 active 클래스를 넣어준다.
이보다 더 많은 동작을 한다면 그 동작의 과정까지 모두 작성해야 한다.

function Tabs() {
 const [activeTab, setActiveTab] = useState(0);
 
 return (
   <div id="parent">
     {tabs.map((tab, index) => (
       <button 
         key={index}
         className={`tab ${activeTab === index ? 'active' : ''}`}
         onClick={() => setActiveTab(index)}
       >
         {tab.label}
       </button>
     ))}
   </div>
 );
}

반면 리액트는 선언형 프로그래밍으로 탭 클릭 시 activeTab의 상태만 바꿔주면 activeTab === index 조건을 검사하여 알아서 탭의 스타일을 바꿔준다.

또한, 버튼을 Tab이라는 컴포넌트로 만든다면 재사용성 재사용성이 높아진다.
어느 화면에서든 탭이 필요하다면 컴포넌트를 불러와 사용하면 된다.

선언형 프로그래밍의 장점은 화면을 어떻게 업데이트할지보단 무엇을 보여줄지에 집중할 수 있다.
반면 단점은 과정을 추적할 수 없다는 것이다.

이런 단점이 있으니, 과제를 구현하면서 내부 과정을 알게 하는 게 과제의 주된 목적이 아닐까 생각했다.

얘기가 옆으로 샜다.
아무튼 가상돔은 선언형 프로그래밍을 할 수 있게 해주는 걸까?
음... 사실 잘 모르겠다.
검색해보면 추상화를 해서... 어쩌구... 저쩌구.. 하는데 피부로 와닿지 않는다.
어떻게 선언형 프로그래밍으로 바꿔주는 걸지 고민해보면서 진행해봐야겠다.

💰 jsx


jsx는 자바스크립트와 마크업 문법을 함께 사용할 수 있게 해주는 문법이다.

<div id="app">
  <ul>
    <li>
      <input type="checkbox" class="toggle" />
      todo list item 1
      <button class="remove">삭제</button>
    </li>
    <li class="completed">
      <input type="checkbox" class="toggle" checked />
      todo list item 2
      <button class="remove">삭제</button>
    </li>
  </ul>
  <form>
    <input type="text" />
    <button type="submit">추가</button>
  </form>
</div>

위와 같은 HTML을 가상돔 객체로 만들기 위해서는 변환이 필요하다.
바벨 홈페이지에 접속하여 "Try it out" 메뉴에 가보면 실제로 어떻게 변환되는지 나와있다.

바벨의 jsx 변환

기존에는 _jsxs가 아니라 React.createElement로 나왔던 걸로 아는데 버전이 바뀌면서 이름이 바꼈다.

const app = (
  <div id="app">
    <ul>
      <li>
        <input type="checkbox" class="toggle" />
        todo list item 1<button class="remove">삭제</button>
      </li>
      <li class="completed">
        <input type="checkbox" class="toggle" checked />
        todo list item 2<button class="remove">삭제</button>
      </li>
    </ul>
    <form>
      <input type="text" />
      <button type="submit">추가</button>
    </form>
  </div>
);

실제로 아무런 설정없이 위와 같이 jsx 문법을 사용하면 오류가 발생한다.
@babel/core@babel/plugin-transform-react-jsx를 추가해줘야 하는 걸로 알고 있었다.
하지만 팀원분게서 vite 환경에서는 별도의 설치없이 된다고 알려주셨다.

파일 상단에 /* @jsx */를 추가해주면 기본값으로 /* @jsx React.createElement */가 들어간다.
React.createElement/* @jsx 함수이름 */처럼 직접 작성한 함수의 이름으로 변경하면 가상돔 객체를 함수에서 인자로 받을 수 있다.

과제에서는 createVNode로 작성되어 있고 파일들이 분리되어 있었다.
이번에도 테스트 코드만 보면서 과제는 통과시켜놨는데 사실 내부 로직이 왜이렇게 되는지는 알지 못해서 하나씩 뜯어보며 리팩토링 해보자.

💰 가상돔 만들기


과제에서 구현된 로직은 다음과 같다.

  1. renderElement(<Page />, $root);로 페이지가 $root에 렌더링된다.
  2. <Page />Page()와 같다. 함수형 컴포넌트를 실행시킨 것이다. 페이지 컴포넌트는 jsx를 반환할테니 createVNode의 결과값이 renderElement의 첫 번째 인자로 넘어간다.
  3. renderElement에서 createVNode가 반환한 가상돔을 normalizeVNode에서 표준화한다.
  4. normalizeVNode가 반환한 최종적인 가상돔을 createElement을 통해 실제 DOM으로 변환한다.
  5. 이후 업데이트가 발생되면 updateElement에서 diff 알고리즘을 활용하여 필요한 부분만 변경한다.

가장 먼저 실행되는 createVNode를 뜯어보자.

export function createVNode(type, props, ...children) {
  children = children
    .flat(Infinity)
    .filter(
      (child) =>
        child !== null && typeof child !== "undefined" && child !== false,
    );
  return { type, props, children };
}

최종 구현된 createVNode다.
flat(Infinity)를 활용해서 평탄화해야 하고, null, undefined, false 값을 걸러줘야 할까?

먼저 인자로 들어오는 값들을 확인해보자.

export const HomePage = () => {
  const { currentUser, posts, loggedIn } = globalStore.getState();

  return (
    <div className="bg-gray-100 min-h-screen flex justify-center">
      <div className="max-w-md w-full">
        <Header />
        <Navigation />

        <main className="p-4">
          {loggedIn && <PostForm />}
          <div id="posts-container" className="space-y-4">
            {[...posts]
              .sort((a, b) => b.time - a.time)
              .map((props) => {
                return (
                  <Post
                    {...props}
                    activationLike={props.likeUsers.includes(
                      currentUser?.username,
                    )}
                  />
                );
              })}
          </div>
        </main>

        <Footer />
      </div>
    </div>
  );
};

렌더링될 페이지다.
createVNode로 이 페이지의 반환값이 인자로 들어올 것이다.
콘솔로 찍어보면

예상과는 다르다.
HomePage 자체가 넘어온다 왜일까?

이유는 renderElement(<Page />, $root);과 같이 호출하기 때문이다.

바벨 홈페이지에서 확인해보면 <Page/> 자체도 _jsx 함수의 인자로 들어간다.

createVNode의 최종 형태에서는 함수를 처리하는 부분이 없으므로 normalizeVNode도 살펴보자.

export function normalizeVNode(vNode) {
  if (
    vNode === null ||
    typeof vNode === "undefined" ||
    typeof vNode === "boolean"
  ) {
    return "";
  }

  if (typeof vNode === "string" || typeof vNode === "number") {
    return vNode.toString();
  }

  if (typeof vNode.type === "function") {
    const { type, props, children } = vNode;
    vNode = normalizeVNode(type({ ...props, children }));
  }

  vNode.children = [...vNode.children]
    .map(normalizeVNode)
    .filter((child) => !!child);

  return vNode;
}

코드를 보면 함수일 경우 실행하도록 되어있다.
두 함수를 따로 보기 힘드니 하나로 합치고 함수일 때 처리하는 부분만 남기고 렌더링할 페이지를 단순하게 바꿔보자.

export function createVNode(type, props, children) {
  if (typeof type === "function") {
    return type();
  } else {
    const element = document.createElement(type);
    element.append(children);
    return element;
  }
}
export const HomePage = () => {
  const { posts } = globalStore.getState();

  return <h1>Hello World!</h1>;
};

합치는 김에 아예 createElement의 기능도 몰았다.

간단한 HTML을 렌더링하는데 성공했다.
이제 속성도 넣고 자식 태그도 넣어서 점점 더 복잡하게 만들어보자.

export const HomePage = () => {
  const { posts } = globalStore.getState();

  return (
    <div>
      <h1 id="title" className="font-bold">
        Hello World!
      </h1>
      <ul>
        <li className="text-red-500">1</li>
        <li className="text-green-500">2</li>
        <li className="text-blue-500">3</li>
      </ul>
    </div>
  );
};
export function createVNode(type, props, ...children) {
  if (typeof type === "function") {
    return type();
  } else {
    const element = document.createElement(type);
    if (props) {
      Object.entries(props).map(([key, value]) => {
        if (key === "className") {
          element.classList = value;
        }
        if (key.startsWith("on")) {
          const eventType = key.slice(2).toLowerCase();
          addEvent(element, eventType, value);
          return;
        }
        element.setAttribute(key, value);
      });
    }

    if (children) {
      [...children].forEach((child) => {
        element.append(child);
      });
    }
    return element;
  }
}

위 코드를 실행하면 정상동작한다.
처음에는 'element를 쌓는 부분이 없는데 어떻게 정상 동작하는 걸까?'라고 생각했다.

이유는 createVNode는 가상돔 전체를 쌓아서 반환하지 않는다.
실행 결과가 다음 createVNode의 인자로 들어가는 걸 반복해서 최종적으로 실행된 createVNode가 반환한 element가 반환되는 것이다.

이해를 위해서 바벨에서 실행해보자.

HomePage가 반환하는 내용이다.
순서를 살펴보면

  1. div 태그가 생성되기 위해 children을 인자로 넘겨줘야 하는데, children에서도 createVNode의 실행 결과를 넘겨줘야 한다.
  2. h1 태그가 먼저 생성되고 children[0]로 들어간다.
  3. ul 태그가 생성되기 위해 li태그가 먼저 만들어진다.
  4. li 태그가 각각 실행된 결과를 배열로 ul 태그에 넘겨준다.
  5. ul 태그에서 children을 순회하며 자식으로 넣어준다.
  6. 최종적으로 div태그에서 children을 자식으로 받은 결과를 반환한다.

형제 관계라면 순서대로, 부모-자식 관계라면 자식 -> 부모 순서대로 실행된다.

다시 HomePage 내용을 되돌려서 처리해보자.

export const HomePage = () => {
  const { currentUser, posts, loggedIn } = globalStore.getState();

  return (
    <div className="bg-gray-100 min-h-screen flex justify-center">
      <div className="max-w-md w-full">
        <Header />
        <Navigation />

        <main className="p-4">
          {loggedIn && <PostForm />}
          <div id="posts-container" className="space-y-4">
            {[...posts]
              .sort((a, b) => b.time - a.time)
              .map((props) => {
                return (
                  <Post
                    {...props}
                    activationLike={props.likeUsers.includes(
                      currentUser?.username,
                    )}
                  />
                );
              })}
          </div>
        </main>

        <Footer />
      </div>
    </div>
  );
};

위 페이지가 정상적으로 나오게 하기 위해서 두 가지 작업을 해줘야 한다.

  1. children 평탄화하고 null, undefined, false 값을 걸러줘야 한다((드디어 이유를 찾았다!).
  2. type 실행 시 {...props, children}으로 넘겨주기

먼저 children을 평탄화하는 이유는

<div id="posts-container" className="space-y-4">
  {
    [...posts]
      .sort((a, b) => b.time - a.time)
      .map((props) => {
        return (
          <Post
            {...props}
            activationLike={props.likeUsers.includes(currentUser?.username)}
          />
        );
      });
  }
</div>

위와 같은 상황때문이다.
createVNode에서 ...children으로 children을 배열로 이미 받고 있는데 children[...posts]라는 배열이 넘어간다.
따라서 createVNode에서 중첩 배열로 받게 되는데 이걸 그대로 로직을 태우면

화면에 element 배열이 그대로 출력되게 된다.

undefined, null, false 값을 걸러줘야 하는 이유도 화면에 문자열로 출력되기 때문이다.

type 실행 시 {...props, children}으로 넘겨줘야 하는 이유는 과제를 제출하고 같은 팀원분께서 여쭤봤는데 제대로 대답하지 못했다.
테스트 코드를 통과하려다보니 이리저리 만지다가 통과가 됐기 때문인데, 코드를 뜯어보면서 이유를 찾게 됐다.

이유는 컴포넌트를 실행할 때 알맞게 인자로 넘겨주기 위해서인데, Link 태그를 예시로 살펴보자.

function Link({ onClick, children, ...props }) {
  const handleClick = (e) => {
    e.preventDefault();
    onClick?.();
    router.get().push(e.target.href.replace(window.location.origin, ""));
  };
  return (
    <a onClick={handleClick} {...props}>
      {children}
    </a>
  );
}

위 태그는 createVNode에서 인자로

{ type: "Link", props: { href: "/", className: "getNavItemClass 결과값"}, children: ["홈"]}

형태로 넘어온다.
Link는 함수니 실행시켜야 하고 인자를 받고 있으니 넘겨줘야 하는데 Link 함수의 시그니처를 살펴보면

function Link({ onClick, children, ...props })

위와 같이 구조 분해 할당으로 받고 있다.
그렇기 때문에 props를 펼쳐서 보내주거나,

props.children = children;
type(props);

로 해도 동일하게 동작할 것 같다.

이제는 vNode의 타입이 number일 경우 string으로 반환해주기만 하면 화면에 렌더링할 수 있다.

과제에서는 normalizeVNodecreateElement에서 undefined, null 등등 falsey한 값들을 걸러주는 코드가 있는데, '이미 createVNode에서 걸러줄 텐데 왜 필요한 걸까?'라는 고민이 있었다.
화면상에서는 체크가 되지 않아 과제 중간 Q&A때 코치님께 여쭤봤다.

코치님께서 함수가 단독으로 쓰일 수 있어 안정성을 위해 추가한 테스트 코드라고 답변해주셨다.
이로써 jsx의 반환값을 가상돔으로 변환하고 DOM으로 바꾸는 것까지 알아봤다.

명령형 프로그래밍에서 선언형 프로그래밍으로 동작할 수 있게 바뀌었다.
'꼭 가상돔이어야 선언형 프로그래밍을 구현할 수 있을까?'라는 의문에는 아닌 것 같다.

하지만 DOM은 많은 정보를 담고 있다.
DOM으로 비교하는 로직을 구현한다면 이 많은 속성들을 다 훑을 것이다.
'가상돔은 DOM에서 필요한 정보만 갖은 경량화된 돔 객체이기 때문에 비교 과정을 효율적으로 변경해준다.'가 과제를 하면서 내린 개인적인 결론.

💰 WeakMap


이번에 새로 알게된 타입 WeakMap!
WeakMap의 최대 장점은 key로 설정한 값이 GarbageCollector(이하 GC)에게 정리된다면 key에 연결된 value도 정리된다!
주의할 점은 key로 객체, 함수, 배열 등을 넣어줄 수 있다.

과제를 하면서 WeakMap을 활용할 수 있는 두 가지가 있었다.

  1. oldNode 관리
  2. 이벤트 전역 객체 관리
let oldNode = null;

export function renderElement(vNode, container) {
  vNode = normalizeVNode(vNode);

  if (!container.childNodes[0]) {
    oldNode = null;
  }
  // 최초 렌더링시에는 createElement로 DOM을 생성하고
  if (!oldNode) {
    container.append(createElement(vNode));
  } else {
    // 이후에는 updateElement로 기존 DOM을 업데이트한다.
    updateElement(container, vNode, oldNode);
  }
  // 렌더링이 완료되면 container에 이벤트를 등록한다.
  setupEventListeners(container);
  oldNode = vNode;
}

oldNode를 변수에 저장하고 renderElement 실행 시 container가 비어있는지 체크한다.
이유는 테스트가 하나씩 실행될 때마다 body.removeChild를 사용하여 내부를 삭제하고 다시 빈 div태그를 생성하여 body에 넣어주기 때문이다.
따라서 DOM 업데이트 시 실제 DOM은 비어있는데, 비교 과정에서 oldNode는 요소가 있기 때문에 오류가 발생한다.
이를 해결하고자 'container가 비어있으면 oldNodenull을 넣어주자'라고만 생각했다.

슬랙에 같은 문제를 겪은 분께서 해결한 상황을 공유해주셨다.
다양한 방법을 고려하는 모습에 본받아야겠다고 생각했다.

사실 이때까지도 'WeakMap이 뭐지? 나중에 한 번 알아봐야겠다!'라고 생각했다.

그러다 zep에서 팀원분들과 얘기를 하다가 이벤트 관리에대해서 얘기를 했는데, 한 분이 WeakMap을 써서 관리하면 key가 GC에 정리되면 value도 정리된다는 걸 알려주셨다.

추가로 과제 중간 Q&A시간에도 코치님께서 한 번 더 말씀해주셔서 잘 써먹으면 용이한 타입이겠다라고 생각했다.

const eventHandlers = {};

export function setupEventListeners(root) {
  Object.keys(eventHandlers).forEach((eventType) => {
    root.addEventListener(eventType, handleEvents);
  });
}

export function addEvent(element, eventType, handler) {
  if (!eventHandlers[eventType]) {
    eventHandlers[eventType] = new Map();
  }

  const elementHandlerMap = eventHandlers[eventType];
  elementHandlerMap.set(element, handler);
}

export function removeEvent(element, eventType) {
  if (eventHandlers[eventType] && eventHandlers[eventType].has(element)) {
    eventHandlers[eventType].delete(element);
  }
}

function handleEvents(e) {
  const handlers = eventHandlers[e.type];
  if (!handlers) return;

  const handler = handlers.get(e.target);
  if (!handler) return;

  handler(e);
}

이벤트 관리하는 코드는 기존에 Map을 사용해서 저장했다.
처음에는 CSS 선택자로 문자열 key를 설정하려 했지만, 고유하지 않을 것 같다는 판단이 생겨서 element를 통째로 저장했다.

아무튼 Map으로 저장하면 문제(WeakMap을 알기 전까지 생각지도 못했지만)가 updateElement 시에 새로운 가상돔은 있지만 기존 가상돔이 없을 경우 DOM을 삭제하게 되는데 이벤트 리스너를 별도로 삭제하지 않았다.

기존 가상돔과 새로운 가상돔의 type이 같아 속성을 변경할 경우에만 이벤트 리스너를 삭제해주고 있었는데, 페이지를 여러번 이동하면 Map에는 이벤트 핸들러가 계속 쌓일 것이다.

하지만 WeakMap을 사용하면 이러한 상황을 방지해준다.
key로 갖고 있는 element가 사라지면 WeakMap에서 알아서 사라지기 때문이다.

const nodeToContainerMap = new WeakMap();

export function renderElement(vNode, container) {
  vNode = normalizeVNode(vNode);

  // 최초 렌더링시에는 createElement로 DOM을 생성하고
  if (!nodeToContainerMap.has(container)) {
    container.append(createElement(vNode));
  } else {
    // 이후에는 updateElement로 기존 DOM을 업데이트한다.
    const oldNode = nodeToContainerMap.get(container);
    updateElement(container, vNode, oldNode);
  }
  // 렌더링이 완료되면 container에 이벤트를 등록한다.
  setupEventListeners(container);
  nodeToContainerMap.set(container, vNode);
}

renderElement 또한 key로 참조하고 있는 container가 사라지면 value가 같이 사라진다.

const eventHandlers = {};

export function setupEventListeners(root) {
  Object.keys(eventHandlers).forEach((eventType) => {
    root.addEventListener(eventType, handleEvents);
  });
}

export function addEvent(element, eventType, handler) {
  if (!eventHandlers[eventType]) {
    eventHandlers[eventType] = new Map();
  }

  const elementHandlerMap = eventHandlers[eventType];
  elementHandlerMap.set(element, handler);
}

export function removeEvent(element, eventType) {
  if (eventHandlers[eventType] && eventHandlers[eventType].has(element)) {
    eventHandlers[eventType].delete(element);
  }
}

function handleEvents(e) {
  const handlers = eventHandlers[e.type];
  if (!handlers) return;

  const handler = handlers.get(e.target);
  if (!handler) return;

  handler(e);
}

💰 updateElement


updateElement는 diff 알고리즘을 사용하여 기존 가상돔과 새로운 가상돔의 달라진 부분을 찾고 달라진 부분의 DOM을 변경하여 효율적으로 렌더링하는 함수다.

사실 내용은 크게 어렵지 않은데, 구현하기가 어렵다.
내용만 살펴보면

  1. 새로운 가상돔만 있는 경우: 추가한다.
  2. 기존 가상돔만 있는 경우: 삭제한다.
  3. 기존 가상돔과 새로운 가상돔의 타입이 스트링인 경우: 내용이 다르다면 바꿔준다.
  4. 기존 가상돔과 새로운 가상돔의 태그가 다른 경우: 바꿔준다.
  5. 기존 가상돔과 새로운 가상돔의 태그가 같은 경우: 속성을 비교하여 바꿔준다.
  6. 자식 요소들에 대해서 재귀적으로 호출한다.

이다.

구현하면서 가장 어려웠던 부분은 다른 점을 찾았는데, 어떻게 정확하게 해당하는 DOM을 선택할 수 있을까였다.

export function updateElement(parentElement, newNode, oldNode, index = 0) {
  if (!newNode && oldNode) {
    parentElement.removeChild(parentElement.childNodes[index]);
    return;
  }

  if (newNode && !oldNode) {
    const newElement = createElement(newNode);
    parentElement.append(newElement);
    return;
  }

  if (typeof newNode === "string" && typeof oldNode === "string") {
    if (newNode != oldNode) {
      parentElement.childNodes[index].textContent = newNode;
    }
    return;
  }

  if (newNode.type !== oldNode.type) {
    const newElement = createElement(newNode);
    const oldElement = parentElement.childNodes[index];
    if (oldElement) {
      parentElement.replaceChild(newElement, parentElement.childNodes[index]);
      return;
    }
    parentElement.appendChild(newElement);
    return;
  }

  const element = parentElement.childNodes[index];
  const newChildren = newNode.children;
  const oldChildren = oldNode.children;
  const maxLength = Math.max(newChildren.length, oldChildren.length);

  updateAttributes(element, newNode.props, oldNode.props);

  for (let i = 0; i < maxLength; i++) {
    updateElement(element, newChildren[i], oldChildren[i], i);
  }
}

updateElement에서 인자로 받은 index와 반복문에서 할당한 i가 코드에 섞여있어 애를 먹었다.
처음에는 '자식 요소를 파고 들면서 index가 증가하나?'라고 생각했지만 열심히 디버깅해보니 아니였다.

음... 그림으로 표현한 거긴한데, 같은 색으로 표시한 것들이 하나의 반복문에서 돈다?
형제 요소가 3개가 있다면, 첫째가 0, 둘째가 1, 셋째가 2고 부모 요소에서 자식 요소로 넘어갈 때는 0이 된다.

따라서 parentElementchildrenupdateElement로 받은 index 번지의 요소가 현재 검사하는 요소가 된다.

말로 설명하자니 좀 어렵긴 한데... 아무튼 그렇다.

💰 마치며


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

중간에 조금 흐트러지긴 했지만, 10시에 일어나서 할 거 하고 책상에 앉아서 새벽 3시까지 있를 하고 있다.
쓸데없이 보내는 시간이 많지만 그래도 새벽 3시까지는 컴퓨터를 끄지 않고 있다.
절대적인 시간은 벌어놨으니, 시간을 알차게 쓸 고민을 해야겠다.

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

책상 앞에 앉아있는 시간은 늘었지만, 그 시간동안 공부를 하고 있나? 생각해보면 아닌 시간이 더 많다.
해야 할 일들을 몇가지 정해두고 시간 분배를 해야할까?
아무튼 시간을 어떻게 '잘' 보낼지 고민하고 개선해야 한다.

프로그래밍 책을 보면 보통 초반부에 이 기술이 나온 역사가 나온다.
사실 이 부분은 와닿지 않아서 대충 보고 넘겼는데, 멘토링 시간에 리액트가 어떤 문제를 해결하려고 나왔는지를 알면 왜 가상돔이란 전략을 썼고 다른 기술들은 왜 다른 방법을 차용한 건지에 대해서 알 수 있다고 했다.

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

같은 팀원 분께서 고맙게도 10시에 코테 연습을 같이 해주기로 하셨다.
일단 어떻게 할지 몰라서 프로그래머스의 기초 트레이닝을 하고 있다.
어떤 방법으로 하면 더 좋을지 하면서 개선해 나가야겠다.

기술을 공부하기 전에 어떤 문제가 있어서 나왔고 어떤 방식으로 해결하려 했는지를 알고 가는 게 전체적인 맥락을 이해하고 넘어가야겠다.

0개의 댓글

관련 채용 정보