Skip to content

Conversation

@totter15
Copy link

@totter15 totter15 commented Nov 15, 2025

과제 체크포인트

배포 링크

https://totter15.github.io/front_7th_chapter2-2/

기본과제

Phase 1: VNode와 기초 유틸리티

  • core/elements.ts: createElement, normalizeNode, createChildPath
  • utils/validators.ts: isEmptyValue
  • utils/equals.ts: shallowEquals, deepEquals

Phase 2: 컨텍스트와 루트 초기화

  • core/types.ts: VNode/Instance/Context 타입 선언
  • core/context.ts: 루트/훅 컨텍스트와 경로 스택 관리
  • core/setup.ts: 컨테이너 초기화, 컨텍스트 리셋, 루트 렌더 트리거

Phase 3: DOM 인터페이스 구축

  • core/dom.ts: 속성/스타일/이벤트 적용 규칙, DOM 노드 탐색/삽입/제거

Phase 4: 렌더 스케줄링

  • utils/enqueue.ts: enqueue, withEnqueue로 마이크로태스크 큐 구성
  • core/render.ts: render, enqueueRender로 루트 렌더 사이클 구현

Phase 5: Reconciliation

  • core/reconciler.ts: 마운트/업데이트/언마운트, 자식 비교, key/anchor 처리
  • core/dom.ts: Reconciliation에서 사용할 DOM 재배치 보조 함수 확인

Phase 6: 기본 Hook 시스템

  • core/hooks.ts: 훅 상태 저장, useState, useEffect, cleanup/queue 관리
  • core/context.ts: 훅 커서 증가, 방문 경로 기록, 미사용 훅 정리

기본 과제 완료 기준: basic.equals.test.tsx, basic.mini-react.test.tsx 전부 통과

심화과제

Phase 7: 확장 Hook & HOC

  • hooks/useRef.ts: ref 객체 유지
  • hooks/useMemo.ts, hooks/useCallback.ts: shallow 비교 기반 메모이제이션
  • hooks/useDeepMemo.ts, hooks/useAutoCallback.ts: deep 비교/자동 콜백 헬퍼
  • hocs/memo.ts, hocs/deepMemo.ts: props 비교 기반 컴포넌트 메모이제이션

과제 셀프회고

작업일지

11/17

과제시작!
매번 일단 코드를 먼저 쳤던것 같은데 이번엔 발제자료를 먼저 읽어보고 진행해보려 합니다.

normalizeNode에서 배열은 어떻게 처리하지...?
일단 faltMap으로 평탄화를 해줬긴 합니다.

/**
 * 주어진 노드를 VNode 형식으로 정규화합니다.
 * null, undefined, boolean, 배열, 원시 타입 등을 처리하여 일관된 VNode 구조를 보장합니다.
 */
export const normalizeNode = (node: VNode): VNode | null => {
  if (typeof node === "string" || typeof node === "number") return createTextElement(node);
  if (typeof node === "object" && node !== null) return node;
  return null;
};



/**
 * JSX로부터 전달된 인자를 VNode 객체로 변환합니다.
 * 이 함수는 JSX 변환기에 의해 호출됩니다. (예: Babel, TypeScript)
 */
export const createElement = (
  type: string | symbol | React.ComponentType<any>,
  originProps?: Record<string, any> | null,
  ...rawChildren: any[]
) => {
  const { key, ...resetProps } = originProps || {};
  const children = rawChildren.flat(Infinity).map(normalizeNode).filter(isEmptyValue);
  const isChildren = children.length > 0;

  return {
    type,
    key: key || null,
    props: {
      ...resetProps,
      ...(isChildren ? { children } : {}),
    },
  };
};

11/18

과제 요구사항에서 왜 이런 함수가 필요한지에 대한 이해가 없으니 코드를 작성하기가 어려웠습니다. 테스트 코드를 기준으로 잡고 어느정도 작성을 하고 있지만 react에 대한 근본적인 이해가 필요한것 같아서 GPT 선생님한테 물어봤습니다...

createElement는 언제 사용하는가?
JSX를 Virtual DOM 객체로 변환할 때 자동으로 호출되는 함수.
사용되는 타이밍은 첫렌더링시상태가 변경되었을때.

[Component render] 
       ↓
   Virtual DOM 생성
       ↓
[이전 VDOM] — diff —> [새 VDOM]
       ↓
필요한 DOM 변경만 실제 DOM에 반영 (patch)
       ↓
Memoization ← 렌더 최적화

babel은 어떻게 createElement를 사용하는가?
babel은 아래의 플러그인을 파싱시 사용함.

@babel/plugin-transform-react-jsx
@babel/preset-react (React 17이후)

{
  "presets": [
    ["@babel/preset-react", {
      "pragma": "createElement", --> 이 설정을 통해서 babel은 파싱시 createElement라는 함수를 호출한다.
      "pragmaFrag": "Fragment"
    }]
  ]
}

위의 설정을 통해서 JSX가 Virtual Dom 객체로 변경된다

// JSX
<div>Hello</div>
// vDom
createElement("div", null, "Hello");

setup함수를 만들면서...

setup은 createRoot사용시 사용됨
createRoot는 main 함수 즉, 애플리케이션 초기화때 사용됨.

  • container를 받으면 아래 children을 제거후 rootNode를 children으로 넣는 함수
  • vNode형식으로 받은 rootNode를 HTMLElement로 만들어서 appendChild 시켜야함.
type VNode = {
  type: string | Function | Symbol;
  props: object;
  children: VNode[];
}

vNode의 type이
'div','span' 같은 문자열이면 -> HTML 태그
Function이면 -> 함수 컴포넌트
Class이면-> 클래스 컴포넌트

함수컴포넌트일경우 이걸 호출하면 vnode를 반환함.

함수 컴포넌트

function Button(props) {
  return {
    type: 'button',
    props: { class: 'btn', ...props }
  };
}

JSX

<Button text="OK" />

vnode

vnode = {
  type: Button,      // 함수
  props: { text: "OK" },
  children: []
}

이를 render시

function renderVNode(vnode) {
  if (typeof vnode.type === "function") {
    // 1. 함수 컴포넌트 호출
    const nextVNode = vnode.type(vnode.props);

    // 2. 반환된 vnode를 다시 렌더링
    return renderVNode(nextVNode);
  }

  if (typeof vnode.type === "string") {
    // HTML element 생성
    const el = document.createElement(vnode.type);
    // props 적용...
    // children 렌더링...
    return el;
  }
}

11/19

reconciliation이란 뭔가?
react는 setState, useState, props 변화, context 변화 등을 감지하면 컴포넌트를 다시 렌더링

  1. 이전 Virtual Dom 트리
  2. 새로 계산된 Virtual Dom트리
    두개를 비교해서 어떤 부분이 달라졌는지 찾는다.
    그 차이점만 실제 DOM에 반영하는 과정이 Reconciliation

reconciliation 규칙

  1. 타입이 같으면 -> 속성을 비교
  2. 타입이 다르면 -> 전체 노드 갈아치움
  3. key를 기준으로 리스트 안정화

Fiber
Reconciliation은 Fiber 구조로 동작.

  • 업데이트 우선순위 정함
  • 작업을 중단/재개 가능
  • concurrent rendering(동시성 렌더링)가능함
    -> FiberReconciliation을 실행하는 엔진

reconcile 함수의 sudo코드

  1. 새 노드가 null이면 기존 인스턴스를 제거한다.(removeInstance)
  2. 기존 인스턴스가 없으면 새 노드를 마운트한다.(insertInstance)
    2-1. 새노드가 함수인경우,
    • 함수를 실행후 나온 VNode값을 reconcile에 넣은후 나온 결과값을 dom과 children에 넣음
      2-2. 텍스트 노드일 경우

2-3. Fragment일 경우
-
2-4. HTML 요소일 경우
-
3. 타입이나 키가 다르면 기존 인스턴스를 제거하고 새로 마운트
4. 타입과 키가 같으면 인스턴스를 업데이트

11/20

  • 과제하느라 정신이 없어서 뭘 했는지 남기질 못했네요,,,

아하! 모먼트 (A-ha! Moment)

가끔 hook의 순서를 지키라는 오류가 떴는데 이런이유 였구나...!

기술적 성장

이번 과제내용을 이해하기 위해서는 상태변경시 reconcile 과정을 어떻게 진행하는지, hook이 어떻게 동작하는지, 이걸위해 어떤구조로 데이터를 저장하는지, 그리고 이게 어떻게 연결되어 react의 기능을 하는지에 대해 공부해야 했습니다.

context의 구조

image

RootContext (렌더링 루트 관리)
렌더링의 최상위 정보를 관리

  • container: DOM 컨테이너
  • node: 루트 VNode
  • instance: 루트 Instance

HooksContext (훅 상태 관리)
각 컴포넌트의 훅 상태를 경로 기반으로 격리하여 관리

  • state: 컴포넌트별 훅 상태 배열 (경로를 키로 사용)
  • cursor: 컴포넌트별 훅 커서 (다음 실행될 훅 인덱스)
  • visited: 이번 렌더링에서 방문한 컴포넌트 경로
  • componentStack: 현재 실행 중인 컴포넌트 경로 스택

EffectsContext (이펙트 큐 관리)
렌더링 완료 후 비동기로 실행

  • queue: 예약된 useEffect 이펙트들의 큐

hook과 render 관계

image

reconcile 동작 방식

Instance 구조
Instance는 VNode와 실제 DOM을 연결하는 데이터 구조

kind: 노드 타입 (HOST, TEXT, COMPONENT, FRAGMENT)
dom: 실제 DOM 노드 참조
node: 가상 DOM 노드 (VNode)
children: 자식 인스턴스 배열
key: 리스트 렌더링용 키
path: 컴포넌트의 고유 경로 (hooks 관리용)

useState 동작 방식

useState가 호출될때 cursor(컴포넌트에서 hook의 순서)와 path(컴포넌트의 path)값을 클로저로 저장한다.

  • 최초실행시엔 init 값을 hooks배열에 저장한다 hooksState[cursor] = init
  • setState호출시
    • 이전의 state값과 새로운 state값이 같으면 render안함
    • 이전의 state값과 새로운 stater값이 다르면 hook의 배열을 업데이트render()
    • cursor값 증가
image

useEffect 동작 방식

  • 이전 훅의 의존성 배열과 현재 의존성 배열을 비교(shallowEquals)
  • 의존성이 변경되었거나 첫 렌더링일 경우, 이펙트 실행을 예약(context.effects.queue.push)
  • cursor값 증가
    (reconcile 호출후)
  • 이펙트 실행 전, 이전 클린업 함수가 있다면 먼저 실행
  • 이펙트 함수 실행후 클린업 함수가 있다면 hooksState[cursor]에 cleanup함수 추가
  • hooksState[cursor] 업데이트
image

마주친 문제들

eventHandler 중복 저장

it("상태가 변경되면 다시 렌더링한다", async () => {
      const container = document.createElement("div");
      let setCount: ((updater: (value: number) => number) => void) | undefined;

      function Counter() {
        const [count, update] = useState(0);
        setCount = update;
        return <button onClick={() => update((value) => value + 1)}>{count}</button>;
      }

      setup(<Counter />, container);

      let button = container.querySelector("button") as HTMLButtonElement;
      expect(button).not.toBeNull();
      expect(button?.textContent).toBe("0");

      setCount!((value) => value + 1);
      await flushMicrotasks();
      button = container.querySelector("button") as HTMLButtonElement;
      expect(button?.textContent).toBe("1");

      button?.click();
      await flushMicrotasks();
      button = container.querySelector("button") as HTMLButtonElement;
      expect(button?.textContent).toBe("2");
    });

여기서 toBe가 3으로 나와서 테스트가 통과가 안됐는데 원인은 addEventlistner가 중복으로 들어가서 였음

export const updateDomProps = (
  dom: HTMLElement,
  prevProps: Record<string, any> = {},
  nextProps: Record<string, any> = {},
): void => {
  // 이벤트 핸들러 처리: 변경되거나 제거된 이벤트 핸들러 제거
  Object.keys(prevProps).forEach((key) => {
    if (key === "children") return;

    const prevValue = prevProps[key];
    const nextValue = nextProps[key];

    // 이벤트 핸들러인 경우
    if (key.startsWith("on") && typeof prevValue === "function") {
      const eventName = key.slice(2).toLowerCase();

      // 이벤트 핸들러가 제거되었거나 변경된 경우
      if (nextValue === undefined || nextValue !== prevValue) {
        dom.removeEventListener(eventName, prevValue as EventListener);
      }
    }
  });

  // nextProps에 없는 일반 속성 제거
  Object.keys(prevProps).forEach((key) => {
    if (key === "children") return;
    if (key.startsWith("on")) return; // 이벤트 핸들러는 위에서 처리됨

    if (nextProps[key] === undefined) {
      // className은 "class" 속성으로 제거
      if (key === "className") {
        dom.removeAttribute("class");
      } else if (key === "style" && typeof prevProps[key] === "object") {
        // 스타일 객체의 경우 모든 속성 제거
        Object.keys(prevProps[key] || {}).forEach((styleKey) => {
          (dom as HTMLElement).style.removeProperty(styleKey);
        });
      } else {
        dom.removeAttribute(key);
      }
    }
  });

  // 새 속성 설정 (변경된 속성 포함)
  setDomProps(dom, nextProps);
};

updateDomProps실행시 변경되거나 제거된 이벤트 핸들러 제거후 새롭게 추가된 props 추가하게 변경

컴포넌트 삭제로인해 경로가 변경된 경우

// 중첩된 컴포넌트에서 useState가 각각 독립적으로 동작한다[test]
<div>
    <h1>Dynamic List</h1>
    {Array.from({ length: itemCount }, (_, i) => (
      <Item id={i} />
    ))}
    <Footer />
  </div>

context.hooks.state

Map(5) {
  '0' => [ 2 ],
  '0.c0.c1' => [ 1 ],
  '0.c0.c2' => [ 2 ],
  '0.c0.c3' => [ 0 ],
  '0.c0.c4' => [ 101 ]
}

위의 코드에서 length값이 3->2로 바뀌면 저장되었던 Footer의 상태값이 바뀌면서 '0.c0.c4'에 저장된 Footer의 상태값을 쓰지못하고 '0.c0.c3'에 저장된 값인 0이 상태값으로 나오면서 테스트가 실패

  1. createChildPath 함수 수정: 타입 정보를 경로에 포함하도록 변경
  • key가 없을 때: ${parentPath}.c${index}.t${typeName} 형식으로 경로 생성
  • 같은 타입의 컴포넌트는 타입 이름이 같아 경로가 유지됨
  1. reconciler.ts의 자식 재조정 로직 수정: 타입 기반 매칭 구현
  • key가 있으면 key로 매칭
  • key가 없으면 타입 기반 매칭
  • 같은 타입의 컴포넌트는 기존 인스턴스와 경로를 재사용

변경후 context.hooks.state

Map(4) {
  '0' => [ 2 ],
  '0.c0.tdiv.c1.tItem' => [ 1 ],
  '0.c0.tdiv.c2.tItem' => [ 2 ],
  '0.c0.tdiv.c4.tFooter' => [ 101 ]
}

코드 품질

reoncile 함수를 작성하면서 동작 방식을 이해하기 위해서 instance를 이해해야하는데 이걸 이해하려면 다시 reconcile이 어떤건지 이해해야하는 순환구조가 반복되서 reconcile 함수부분은 AI에게 맡겼습니다. 그러다보니 해당부분만 계속 떼우는 방식으로 진행하니 코드가 지저분해진것 같습니다,,, 아직 reconcile을 완전히 이해한건 아니라 리팩토링을 하면서 이해하는 시간을 가져볼까 합니다,,,

학습 효과 분석

사실 프론트엔드 개발자로서 react의 동작구조를 알아야지라는 생각만 가지고 정작 깊게 파보는건 미뤄둔 상태로 있었는데 이번 기회에 로직부분을 다룰수 있어서 좋았습니다. 아마 혼자였다면 여기까지 이해하는데 더 시간이 걸렸을것 같습니다.

이번 과제를 하면서 정말 많은 지식을 접했는데 이걸 제걸로 만들기엔 일주일이란 시간은 확실히 짧은것 같습니다.

과제 피드백

리뷰 받고 싶은 내용

  1. 코치님은 보통 학습을 할때 어떤 과정을 거치시나요? (00을 공부해봐야지 -> 일단 개념을 공부해본다/일단 써본다 등)
    '00을 공부해봐야지' 라는 생각을 어떤때에 하시게 되나요?

  2. 이번 과제는 리액트를 만들어보는거였는데 이후에 학습내용으로 추천하는게 있으실까요?

@totter15 totter15 changed the title 과제 시작 [1팀 천진아] Chapter2-2. 나만의 React 만들기 Nov 15, 2025
천진아 and others added 8 commits November 18, 2025 00:48
- 이벤트 핸들러 제거 로직 추가 (removeEventListener)
- 타입 정보를 포함한 경로 생성으로 컴포넌트 경로 유지
- 타입 기반 자식 매칭으로 중첩 컴포넌트 상태 독립성 보장
- 일반 HTML 속성 설정 활성화
- cleanupUnusedHooks 구현
천진아 added 8 commits November 21, 2025 01:46
- key 기반 자식 재조정 및 DOM 재사용 구현
  - key가 있는 자식을 재배치할 때 기존 DOM 재사용
  - key 변경 시 이전 인스턴스 cleanup 및 새로 마운트
  - DOM 노드 재배치 로직 추가 (reorderDomNodes)

- useEffect 훅 구현
  - 의존성 배열 기반 조건부 실행
  - 이전 cleanup 함수 실행 후 새 effect 실행
  - 렌더링 후 비동기 실행

- 언마운트 및 cleanup 처리 개선
  - 언마운트된 컴포넌트의 훅 상태 정리
  - key 변경 시 cleanup 함수 실행
  - visited 경로 관리로 정확한 cleanup 수행

- Fragment 동적 렌더링 지원
  - Fragment의 모든 DOM 노드 재귀적 수집
  - Fragment 자식 변경 시 올바른 DOM 구조 유지
…utoCallback)

- useState를 사용하여 안정적인 ref 객체를 생성하는 useRef 구현
- 커스텀 equals 함수를 사용한 의존성 비교로 useMemo 구현
- useMemo를 사용하여 useCallback 구현
- deepEquals를 사용한 useDeepMemo 구현
- 최신 상태를 참조하면서도 안정적인 참조를 가진 콜백을 생성하는 useAutoCallback 구현
- useRef를 사용하여 이전 props와 렌더링 결과를 저장하는 memo HOC 구현
- equals 함수로 props 비교하여 불필요한 리렌더링 방지
- deepEquals를 사용하여 props를 깊게 비교하는 deepMemo HOC 구현
- 중복으로 작성된 값제거
- console.log 제거
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.

1 participant