ckdwns9121 님의 상세페이지[8팀 박창준] Chapter 1-2. 프레임워크 없이 SPA 만들기

과제 체크포인트

배포 링크

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

기본과제

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

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

이벤트 위임

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

심화 과제

Diff 알고리즘 구현

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

과제 셀프회고

기술적 성장

React의 핵심 개념인 Virtual DOM이 어떻게 동작하는지 간단히는 알고 있었지만 막상 직접 구현해보려 하니 막막했기도 했고 한편으로는 너무 재밌을것 같았습니다. 재밌을것 같았던 이유는 1주차 과제에서 했던 고민이였어요.😂

“innerHTML로 DOM을 전부 다시 그리는 방식이 정말 SPA라고 할 수 있을까?”

라는 의문이 생겼었는데 제가 너무 성급하게 고민을 했엇네요 ㅎㅎ.. 2주차에 Virtual DOM을 직접 구현하는 과제가 나오면서 코치님들이 과제를 어떻게 단계적으로 설계했고 수강생들에게 어떤 학습 포인트를 던지려 했는지 이해할 수 있었습니다.

고민했던 부분

과제를 진행하면서

  • 어떻게 바뀐 부분만 찾아서 렌더링할까?
  • 어떤 기준으로 이전 상태와 현재 상태를 비교해 업데이트해야 할까?

이 질문들에 답하기 위해 가상돔의 동작 원리와 diffing 알고리즘의 기본 개념부터 고민할 필요가 있었습니다.

특히 아래 부분은 초반에 잘 이해되지 않았습니다.

  • flat을 이용해 children을 평탄화하는 이유 → 이걸 평탄화해버리면 계층 구조가 깨지는 거 아닐까?
  • 이벤트 핸들러 관리에서 WeakMap이 왜 필요한지 → 그냥 Map이나 Set으로 처리해도 되는 거 아니야?
  • normalizeVNode가 왜 존재하는지, 무엇을 해결하는지 → 이미 VNode를 만들었는데 왜 또 정규화가 필요한 거지? 그냥 렌더링하면 되는 거 아니야?

처음에는 “평탄화를 하면 중첩된 계층 구조가 깨져버리는 게 아닐까?” 같은 오해도 있었고, WeakMap과 Map의 차이가 단순한 자료구조 차이인지 아니면 더 본질적인 이유가 있는지도 이해하지 못했습니다.

이해했던 과정

1. 평탄화가 해결해하려는 문제

평탄화를 하면 계층구조가 다 깨져버리는거 아니야? 라는 질문은 여러 케이스를 디버깅 해보면서 제가 잘못 이해했다는 것을 알았습나다.

제가 이해한 평탄화를 하는 이유는 다음과 같습니다.

  • children 배열을 1차원으로 만들어 배열, 문자열, VNode 등 혼합합적인 상태를 정리하고
  • 렌더링할 때 각 항목을 재귀로 순회하며 element를 생성할 수 있게 도와주는 과정임을 이해했습니다.

예를들어 아래와 같은 구조가 있으면

createVNode("div", null,
  "Hello",
  [
    "World",
    ["!", createVNode("span", null, "항해")],
    false,
    null
  ],
  createVNode("section", null,
    createVNode("h1", null, "플러스"),
    ["6기", createVNode("p", null, "화이팅")]
  )
)

평탄화 전

[
  "Hello",
  [
    "World",
    [
      "!",
      { type: "span", props: null, children: ["항해"] }
    ],
    false,
    null
  ],
  {
    type: "section",
    props: null,
    children: [
      { type: "h1", props: null, children: ["플러스"] },
      [
        "6기",
        { type: "p", props: null, children: ["화이팅"] }
      ]
    ]
  }
]

평탄화 후

[
  "Hello",
  "World",
  "!",
  { type: "span", props: null, children: ["항해"] },
  {
    type: "section",
    props: null,
    children: [
      { type: "h1", props: null, children: ["플러스"] },
      "6기",
      { type: "p", props: null, children: ["화이팅"] }
    ]
  }
]

즉 이걸 평탄화를 하지 않으면

  • children 안에서 중첩 배열이 섞여있게 되고 렌더링 로직에서 매번 배열,문자열,VNode등 이걸 구분해서 분기로 처리를 해야 하는데 상당히 로직히 복잡해질것 같다고 생각했습니다.

createVnode 함수는 다음과 같이 구현했습니다.

1. createVnode

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

  • Infinty를 활용하여 모든 depth 레벨의 배열을 평탄화하였습니다.

2. WeakMap의 필요성

WeakMap은 DOM 엘리먼트가 제거될 때 GC에 의해 자동으로 참조가 해제되어 메모리 누수를 막기 위함이라는 점을 이해하게 되었습니다. 특히 디버깅 과정에서 DOM이 사라져도 WeakMap에서 별도 cleanup 없이 참조가 사라진다는 점 (키에 대한 강력한 참조를 생성하지 않기 때문)에 대해서 공부하였습니다.

또한 준일 코치님 멘토링 시간에 WeakMap은 Map, Set과 달리 .keys(), .values() 와 같은 함수를 제공해주지 않았고 이터러블 하지 않다라는것을 알게 되었습니다.

왜 WeakMap은 이터러블하지 않을까?에 대한 궁금증이 생겼고 아래와 같은 답을 얻게 되었습니다.

  • WeakMap은 GC대상인 키가 언제 회수될지 예측 불가능합니다. 만약 WeakMap을 순회하는 도중 가비지 컬렉터가 키를 회수해버리면 예상치 못한 오류나 동작이 발생할 수 있습니다.
  • GC도중 내부 구조가 변하게 된다면 순회하고 있는 이터레이터의 내부상태가 꼬일 수 있게 됩니다.
  • GC 도중에는 순회를 멈추면 되는거 아닌가? , 이터레이터가 GC의 상태를 참조할 순 없나? 와 같은 단순한 생각도 하였지만 사실 이는 자바스크립트 엔진에 있어서 상당한 성능 비용을 발생한다고 합니다. (목적에 비해 과도한 비용 발생 → 즉 오버엔지니어링)

따라서 위와 같은 이유로 WeakMap은 이터러블하지 않게(non-iterable) 설계되었다고 합니다.

Weakmap은 아래와 같은 상황에 활용하였습니다.

1. Virtual DOM 이전 상태 저장 renderElement

// 컨테이너별 이전 Virtual DOM을 저장하는 WeakMap
const previousVNodes = new WeakMap(); 

export function renderElement(vNode, container) {
  // 이전 Virtual DOM 가져오기
  const previousVNode = previousVNodes.get(container);
  
  // 현재 Virtual DOM 저장
  previousVNodes.set(container, normalizedVNode);
}
  • DOM 컨테이너 엘리먼트를 키로 사용하여 이전 Virtual DOM 상태를 저장하는 용도로 사용했습니다.
  • 상태가 바뀌면서 컨테이너가 DOM에서 제거되면 WeakMap에서도 자동으로 참조가 해제되는 역할을 WeakMap을 통해 구현했습니다.
  • diffing 알고리즘에서 이전 상태와 현재 상태를 비교할 때 사용하였습니다.

2. 이벤트 핸들러 관리 eventManager

// 엘리먼트별 이벤트 핸들러들을 저장할 WeakMap
const eventHandlers = new WeakMap(); 

export function addEvent(element, eventType, handler) {
  if (!eventHandlers.has(element)) {
    eventHandlers.set(element, {});
  }
  // ... 핸들러 등록 로직
}
  • 엘리먼트 자체를 키로 사용하여 각 엘리먼트에 등록된 이벤트 핸들러들을 관리하였습니다.
  • 엘리먼트가 DOM에서 제거되면 WeakMap에서도 자동으로 참조가 해제되어 메모리 누수를 방지하게 만들었습니다.
  • 이벤트를 위임하는 과정에서 특정 엘리먼트의 핸들러를 찾을 때 사용하였습니다.

3.JSX 트랜스파일링과 커스텀 JSX Factory

이번 과제에 흥미로운 부분 중 하나는 JSX가 실제로 트랜스파일 된다는 점이었습니다.

<MyComponent prop="value">Hello</MyComponent>

보통 이렇게 컴포넌트를 작성하는데 Babel과 같은 트랜스파일러가 이걸 변환하면

React.createElement(MyComponent, { prop: "value" }, "Hello")

위 코드 처럼 JavaScript 함수 호출 코드로 바뀝니다. 이번 과제는 React를 사용하는게 아니라서 createVNode 함수를 JSX의 변환 대상(factory)로 지정이 되어있었습니다.

/** @jsx createVNode */

이런식으로 말이죠. 이 주석은 프라그마 주석이라고 부르는데 Babel, esbuild 같은 도구에 “JSX를 만나면 React.createElement 대신 createVNode로 변환해줘” 라고 알려주는 역할을 합니다.

esbuild: {
  jsx: "transform",
  jsxFactory: "createVNode",
},

vite 설정에서도 위처럼 jsxFactory를 지정해서 모든 JSX 문법을 createVNode로 변환하도록 설정이 되어있었는데 이 덕분에

<Toast isVisible={true} message="안녕하세요" />

는 최종적으로

createVNode(Toast, { isVisible: true, message: "안녕하세요" })

로 변환되고 normalized로 함수형 컴포넌트를 처리할 수 있게 됩니다. 추가적으로 영서님께서 잘 정리해주신 createVnode 문서를 보고 React 17부터 적용된 automatic runtime 개념에 대해서 이해할 수 있었습니다.

예전에는 그냥

“React 버전 업데이트돼서 이제 import React 안 해도 된대!”

라고 단순히만 짚고만 넘어갔다면 지금은

“아, React 17+에서는 Babel이 JSX를 React.createElement로 변환하는 게 아니라, react/jsx-runtime에서 jsx, jsxs 같은 헬퍼 함수를 import해서 동작하는구나. 그래서 React를 import 하지 않아도 되구나...”

라는 좀 더 구체적인 원리를 이해하려고하는 개발자로 조금이나마 성장할 수 있었습니다.

4. normalizedVNode

처음에는 normalizeVNode가 왜 필요한지 직관적으로 이해되지 않았습니다.

“이미 createVNode에서 VNode를 만들었는데 왜 또 정규화(normalize)를 해야 하지?” “그냥 렌더링 하면 되지 않나?”

라는 의문이 있었는데 아래와 같은 역할을 한다고 이해했습니다.

  1. 함수형 컴포넌트 실행 처리 JSX에서 같은 것은 내부적으로 createVNode(MyComponent, props, children)로 생성되는데, 이때 MyComponent는 실제 DOM 태그가 아니라 함수입니다. 따라서 이걸 그대로 DOM으로 만들 수 없고, 먼저 MyComponent(props)를 실행해 VNode 트리로 변환해야 합니다. 이 과정을 normalizeVNode에서 처리합니다.

  2. children 정리 (불필요한 값 제거) JSX에서는 조건부 렌더링에서 false, null, undefined 같은 값이 children 배열에 섞일 수 있습니다.

예를 들어

<div>{isTrue && <span>Hello</span>}</div>

여기서 isTrue가 false면 children에 false 값이 남는데, 이걸 그대로 렌더링하면 DOM에 이상한 동작이 발생할 수 있습니다. normalizeVNode는 이런 값들을 걸러내고 깨끗한 children 배열을 만드는 역할을 담당합니다.

정규화 전정규화 후
imageimage
빈문자열 존재 (false, null 값 포함)빈문자열 제거, children 정리 완료

코드 품질

WeakMap을 사용하여 이전 container를 키로 사용하게 구현했습니다. 이 구조로 인해 수동으로 previousVnode.delete(container)와 같은 코드를 구현하지 않아도 자동으로 이전 DOM에 대한 참조를 CG가 제거할 수 있도록 구현했습니다.

import { setupEventListeners } from "./eventManager";
import { createElement } from "./createElement";
import { updateElement } from "./updateElement";
import { normalizeVNode } from "./normalizeVNode";

/**
 * 컨테이너별 이전 Virtual DOM을 저장하는 WeakMap
 * 메모리 누수 방지를 위해 WeakMap 사용
 * @type {WeakMap<HTMLElement, any>}
 */
const previousVNodes = new WeakMap();

/**
 * Virtual DOM을 실제 DOM으로 렌더링하는 함수
 * 이전 Virtual DOM과 비교하여 효율적으로 DOM을 업데이트
 * @param {any} vNode - 렌더링할 Virtual DOM 노드
 * @param {HTMLElement} container - 렌더링할 컨테이너 엘리먼트
 */
export function renderElement(vNode, container) {
  // 함수형 컴포넌트 정규화
  const normalizedVNode = normalizeVNode(vNode);

  // 이전 Virtual DOM 가져오기
  const previousVNode = previousVNodes.get(container);

  // 이미 컨테이너 안에 DOM이 있다면 업데이트
  if (container.firstChild && previousVNode) {
    updateElement(container, normalizedVNode, previousVNode, 0);
  }
  // 없으면 DOM 생성하고 container에 붙임
  else {
    const $el = createElement(normalizedVNode);
    container.appendChild($el);
  }

  // 현재 Virtual DOM 저장
  previousVNodes.set(container, normalizedVNode);

  // container에 이벤트 리스너 등록
  setupEventListeners(container);
}

학습 효과 분석

1. React가 Virtual DOM을 아키텍쳐를 도입한 이유

이번 과제를 통해 React 팀이 왜 Virtual DOM이라는 아키텍처를 선택했는지에 대해 더 깊게 이해할 수 있었습니다. 브라우저의 DOM은 기본적으로 매우 느린 구조입니다. 직접 DOM 조작을 하게되면

  • 불필요한 repaint, reflow가 발생하고,
  • 특정 상태 변경이 어디까지 영향을 주는지 계산하기 어렵고,
  • 다수의 DOM 연산이 꼬여버릴 경우 성능 저하와 버그가 쉽게 생깁니다.

React 팀은 이 문제를 해결하기 위해 Virtual DOM 이라는 브라우저 DOM의 메모리 상 추상화 계층을 만들었다고 생각합니다.

2. 내가 구현한 Virtual DOM 방식

Virture DOM의 핵심 아이디어를 간접적으로 이번과제에서 체험해볼 수 있었는데요. 저는 아래와 같은 구현을 하였습니다.

  • Virtual DOM 생성: createVNode()로 JSX를 가벼운 객체로 변환
  • 정규화: normalizeVNode()로 함수형 컴포넌트 실행 및 일관된 형태로 변환
  • 렌더링: renderElement()에서 이전 Virtual DOM과 비교
  • Diffing: updateElement()에서 실제 변경사항만 DOM에 반영

이 구현을 토대로 Virture DOM의 내부 구현 원리를 간접적으로 체험할 수 있는 과제였습니다.

이 방식 덕분에 컴포넌트 단위로 선언적으로 UI를 작성할 수 있고, 복잡한 UI 변경을 일일이 계산하지 않아도 되고, React가 알아서 최적화된 최소 DOM 패치를 만들어낼 수 있다고 이해했습니다.

3. Synthetic Event(합성 이벤트)에 대한 이해 React는 이벤트 처리를 위해 Synthetic Event(합성 이벤트)라는 추상화 객체를 사용합니다. 이 개념은 준일 코치님의 발제에서 처음 접했는데요 React가 각 DOM 요소에 이벤트 리스너를 개별로 등록하지 않고, root에 단 하나의 이벤트 리스너만 등록해 모든 이벤트를 중앙에서 관리한다는 점이 매우 인상적이었습니다. 특히 이 설계 방식을 이해하면서, 1주차 과제에서 가졌던

"SPA에서는 전역 이벤트 관리를 어떻게 할까요?"

라는 궁금증에 대한 해답을 찾을 수 있었습니다. Synthetic Event 시스템 전체를 직접 구현한 것은 아니지만 root 이벤트 위임 방식과 메모리 관리 로직을 직접 다뤄보면서 React가 왜 이런 설계를 택했는지를 코드 수준에서 역시 간접적으로 체험할 수 있었습니다.

image

1주차 과제 리뷰받고 싶었던 내용.

4. Synthetic Event가 해결하려고자 했던 문제

이벤트 리스너는 root(container)에 한 번만 등록되고 -> 실제로 발생한 이벤트는 root로 버블링되어 올라온 뒤 -> React 내부에서 Virtual DOM 트리를 순회하며 “어떤 컴포넌트의 핸들러가 호출되어야 하는지”를 계산해 실행됩니다.

이 방식은 다음과 같은 이유로 강력한 장점을 가집니다.

1 .메모리 사용량 단순한 예를 들어 Vanilla JS에서는 버튼 100개에 이벤트 리스너도 100개가 필요하지만 React는 root에 단 하나의 리스너만 있으면 되기 때문에 리스너 수와 메모리 사용량을 획기적으로 줄일 수 있습니다.

2. cross-browser 이벤트 표준화 React는 native 이벤트를 Synthetic Event로 감싸 브라우저마다 미묘하게 다른 이벤트 동작(예: event.target, event.srcElement, stopPropagation 등)을 통일된 API로 표준화합니다. 덕분에 개발자는 브라우저 호환성 문제를 신경 쓰지 않아도 되는 장점이 있다고 생각합니다.

3. 렌더링 최적화와의 결합 Vanilla JS에서는 DOM 요소를 replaceChild나 innerHTML로 교체하면 기존에 걸려 있던 이벤트 리스너가 다 사라지지만 React는 Virtual DOM에서 node를 교체해도 root에서 이벤트를 받고 Virtual DOM 기준으로 매핑하기 때문에 이벤트 연결이 깨지지 않습니다. 이 덕분에 렌더링 최적화와 이벤트 관리가 깔끔하게 결합될 수 있습니다.

이번 과제를 하면서 Vanilla JS로도 비슷한 root 이벤트 위임 방식을 직접 구현해보고 왜 React가 이런 아키텍쳐로 설계되었는지를 코드 레벨에서 이해할 수 있었던 점이 매우 큰 수확이라 생각합니다.

추가적으로 제가 이해한 리렌더링 동작 흐름을 다이어그램화 해보았습니다.

리렌더 흐름

graph TD
    A[상태 변경] --> B[컴포넌트 재실행]
    B --> C[새로운 vNode를 생성]
    C --> D[renderElement 호출]
    D --> E[이전 vNode 가져오기]
    E --> F[updateElement 호출]
    F --> G[노드 타입 비교]
    G --> H{타입이 같은가?}
    H -->|Yes| I[속성 업데이트]
    H -->|No| J[노드 교체]
    I --> K[자식 노드 재귀 업데이트]
    J --> L[새 DOM 생성 및 교체]
    K --> M[이벤트 리스너 재설정]
    L --> M

추가 학습한 개념

그럼 Context API는 왜 Virtual DOM의 diffing처럼 부분 업데이트 최적화를 하지 않을까?

ContextAPI는 Provider에서 value가 변경되면 그 Context를 구독하는 모든 컴포넌트가 다시 렌더링됩니다. 여기서 React의 Virtual DOM diffing이 속성 단위로 바뀐 부분만 찾아내는 것처럼, Context도 “value 객체의 특정 필드만 바뀐 경우”에는 부분 업데이트로 최적화할 수 있지 않을까? 하는 궁금증이 생겼습니다.

이걸 찾아보고 정리한 내용은 아래와 같습니다.

1. Context API가 selector 기반 최적화를 지원하지 않는 이유

  • ContextAPI의 사용 목적 자체가 전역적으로 자주 변하지 않는 값 (ex. 테마, 다국어 로케일, 로그인 정보)을 손쉽게 접근하기 위함입니다.
  • 따라서 복잡한 diffing, selector기반 부분적인 리렌더링 지원을 하지 않는다고 합니다. (ContextAPI의 범위 밖).
  • 최적화가 필요할 시 Context를 분리하거나, useMemo, useCallback, React.memo와 같은 메모이제이션을 도입, 혹은 Redux, Zustand 같은 외부 상태 관리 라이브러리 활용을 고려해야 합니다.
  • 즉 -> 자주 변하지 않는 값을 위해 설계했으니 너네가 최적화를 알아서해라. 그게 싫으면 다른 라이브러리를 써라..🥲

2. React의 ContextAPI 설계 철학

  • 복잡한 상태 관리 최적화까지 책임지지 않고 단순한 전역 데이터 전달용으로만 쓰게 만들자.
  • 이는 내부적으로 **Object.is(동일성 비교)**로만 provider value 변화 여부를 판단하기 때문에 shallow compare만 하고 값이 다르면 모든 구독 컴포넌트를 리렌더링하도록 설계함.

사실 이부분은 React팀이 ContextAPI를 설계했을 당시의 의도한 방향이라고 결론을 내렸습니다.

과제 피드백

  • 끊임없이 궁금증을 가지고 새로운걸 공부할 수 있는 시간이였습니다!👍
  • WeakMap을 활용해서 이전 container를 저장하는 아이디어를 냈는데 꽤 괜찮은 거 같습니다.
  • 현재 index 기반 비교를 하고 있는데 실제 React처럼 key 기반의 비교를 하려면 어떻게 구현해야할지 고민이 들었습니다.
  • 준형님이 올려주신 비동기 컴포넌트에 대한 추가적인 고민.. 저도 이런걸 고민할 수 있는 개발자로 성장하고싶단 생각을 했습니다.
image

리뷰 받고 싶은 내용

Q1. 이번 과제를 수행하면서 좀더 React에 대한 Deep Dive를 해보고 싶어졌습니다. 추가적으로 학습하면 좋은 내용이 있으면 추천해주실 수 있을까요?

Q2. 현재 구현된 diffing은 index기반 비교 알고리즘입니다. 이를 key 기반으로 확장하려면 어떤식으로 리팩토링을 진행하면 좋을까요?

Q3. updateAttributes 함수에서 놓치고 잇는 edge case가 있을까요?

현재 updateAttributes 함수에서는

  • on으로 시작하는 이벤트 → 이벤트 핸들러 등록
  • className → class 속성으로 변환
  • boolean 속성 → property로 직접 할당
  • 그 외 나머지 → setAttribute로 처리

이 기준으로 분기하고 있는데 이 처리방식에서 누락되거나 예외 처리할 edge case가 있을지 리뷰를 받고싶습니다.

과제 피드백

안녕하세요 창준님! 2주차 과제 잘 진행해주셨네요 ㅎㅎ 고생하셨습니다!

“innerHTML로 DOM을 전부 다시 그리는 방식이 정말 SPA라고 할 수 있을까?” 라는 의문이 생겼었는데 제가 너무 성급하게 고민을 했엇네요 ㅎㅎ.. 2주차에 Virtual DOM을 직접 구현하는 과제가 나오면서 코치님들이 과제를 어떻게 단계적으로 설계했고 수강생들에게 어떤 학습 포인트를 던지려 했는지 이해할 수 있었습니다.

맞아요 ㅎㅎ 일부로 "불편함"을 경험해보고, 이걸 해결하는 방식으로 과제를 설계했답니다! 알아주셔서 감사드려요!!

Q1. 이번 과제를 수행하면서 좀더 React에 대한 Deep Dive를 해보고 싶어졌습니다. 추가적으로 학습하면 좋은 내용이 있으면 추천해주실 수 있을까요?

react 자체에 대해 파고들어 가는 방법도 있고, preact 같은 경량화된 버전의 react 구현체의 코드를 살펴보면서 내가 작성한 코드와 무엇이 다른지 비교해보는 방법도 있답니다 ㅎㅎ

개인적으로 훅을 처음부터 아예 구현하는 방식으로 시도해보면 좋다고 생각하는데.... 이게 생각보다 난이도가 높아서... 중간과정이 필요한데 그게 preact 살펴보기일 것 같아요!

Q2. 현재 구현된 diffing은 index기반 비교 알고리즘입니다. 이를 key 기반으로 확장하려면 어떤식으로 리팩토링을 진행하면 좋을까요?

props에 key를 받아온 다음에, index 이전에 key에 대해 먼저 검사해서 위치를 변경해주거나 그대로 두거나 하는 방법이 있을 것 같네요 ㅎㅎ

Q3. updateAttributes 함수에서 놓치고 잇는 edge case가 있을까요?

제가 솔루션 만들 때 미처 신경쓰지 못한 부분인데요, data attribute 에 boolean을 넣으면 사라지는 현상이 있었어요 ㅎㅎ

그래서 사실 제일 좋은 방법은 어플리케이션을 만들어가면서 엣지케이스를 채워가는거라고 생각합니다.. 허허

그리고 또 defaultValue 같은것도 리액트에서 지원하는데 현재에는 이런 개념이 없어요.

select / option 을 react로 변환해보시면 알 수 있답니다!