Skip to content

[2팀 고다솜] Chapter 2-2. 나만의 React 만들기 #39

Open
ds92ko wants to merge 25 commits intohanghae-plus:mainfrom
ds92ko:main
Open

[2팀 고다솜] Chapter 2-2. 나만의 React 만들기 #39
ds92ko wants to merge 25 commits intohanghae-plus:mainfrom
ds92ko:main

Conversation

@ds92ko
Copy link

@ds92ko ds92ko commented Nov 16, 2025

과제 체크포인트

배포 링크

https://ds92ko.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 비교 기반 컴포넌트 메모이제이션

과제 셀프회고

기본 과제 작업 방식

이번 발제를 듣고 처음에는 정말 어디서부터 손을 대야 할지 감도 잡히지 않아서, 무작정 AI를 활용해 utils와 core 함수들을 의존성이 낮은 순서대로 빠르게 작성해 나갔습니다.
하지만 전체적인 플로우나 구조를 파악하지 못한 채 AI에게 명령만 내리다 보니, 이 친구가 짜준 코드가 제대로 된 코드인지 스스로 판별할 수 없어 한계에 부딪혔습니다.
그래서 잠시 작업을 멈추고, 아래 문서들을 하나씩 분석하며 기초를 다시 다졌습니다.

이 과정은 정말 토가 나올 것 같았지만(?) 이해가 안 되는 부분은 GPT에게 물어가며 문서를 읽고 또 읽기를 반복했습니다.
사실 이 문서를 이해하는 데 너무 많은 시간이 걸려서 자존감이 급격히 하락했지만, 나리님과 진성님의 응원, 그리고 2팀의 마스코트 (완)두콩이에 힘입어 끝까지 완독할 수 있었습니다.

image image

1차적인 이해를 마치고, AI가 작성한 코드를 검토하며 문제가 예상되는 부분을 수정했습니다.

기본 과제 수정 포인트

  • normalizeNode에서 Fragment 처리 누락된 부분을 보완했습니다.
  • basic.mini-react.test.tsx 내 path 규칙 주석을 확인하고, 해당 규칙에 맞도록 createChildPath를 수정했습니다.
    image
  • useEffect에서 cleanup()의 실행 순서를 올바르게 조정했습니다.
  • updateDomProps에서 이벤트 핸들러가 null 또는 undefined로 변경되는 경우의 누락된 처리를 추가했습니다.
  • insertInstance에서 이미 올바른 위치에 있는 노드는 건너뛰도록 하고, anchor가 null일 때 appendChild하도록 보완했습니다.

수정 후 basic 테스트가 정상적으로 통과하는 것을 확인하고 심화 과제로 넘어갔습니다.

심화 과제를 작업할 때 AI를 활용했던 방식

  1. 남은 스켈레톤 코드를 컨텍스트로 제공하고, 의존성 관계를 고려해 구현 순서를 정했습니다.

    AI가 제안한 구현 순서
    
    1. useRef (기본)
    ↓
    3. useMemo (useRef 사용)
    ↓
    4. useCallback (useMemo 사용)
    ↓
    5. useDeepMemo (useMemo 사용, 간단)
    ↓
    6. memo (useRef 사용)
    ↓
    7. deepMemo (memo 사용, 간단)
    ↓
    8. useAutoCallback (useCallback + useRef 사용, 복잡)
    
  2. 구현 단계에서 잡도리(?)를 시전했습니다.

    image

    작업 규칙 중 4번, 6번, 7번을 추가한 이유는, AI가 작성한 코드의 구조나 스타일을 임의로 바꾸지 않는 편이 전체 흐름을 이해하는 데 훨씬 유리하다고 판단했기 때문입니다.
    새로 함수를 만들어 복잡도를 높이거나, 주석이나 구조를 건드려 해석을 어렵게 만드는 상황을 피하고자 했습니다.

"여기를 구현하세요."의 모든 구현을 마친 후 테스트를 돌려봤는데 e2e에서 막혔습니다.
원인을 확인하니 브라우저 설치 문제였고, 설치 후 재실행했지만 여전히 통과하지 못했습니다. (이때 정말 절망적이였습니다..)
당황해서 로컬 서버를 실행해 봤는데 아예 렌더링이 안되고 있었고, 콘솔 에러를 확인해보니 props가 존재하지 않을 때 예외 처리가 누락되어 있음을 발견했고, 이를 수정하자 e2e까지 모두 통과할 수 있었습니다.

GitHub Pages 배포 과정

모든 테스트를 통과한 후 GitHub Pages로 배포를 진행했습니다.
그래도 지난주에 한번 해봤다고 배포 자체는 익숙했지만, push 과정에서 GitHub Internal Server Error(500)가 발생했습니다.

image

그래서 Github Status를 확인하니 Git Operations 항목이 Incident 상태였습니다.
검색해보니 한 10분 정도면 정상화 된다고 했는데, 제 경우엔 거의 한시간 정도 걸린 것 같습니다...

image

정말 너무 졸렸는데 배포 끝내고 출근하고 싶어서 블로그 글 정독하면서 정상화되길 기다렸습니다...
겨우 배포까지 끝내고, 남은 시간은 AI가 작업한 코드를 바탕으로 React를 이해하고 학습하는 데 사용했습니다.

과제 종료 후...

React의 전체 렌더링 사이클과 재조정 과정을 이해하기 위해 피그잼을 활용해 다이어그램을 한땀 한땀 작성하며 구조를 정리했습니다.

React 렌더링 & 재조정 다이어그램 피그잼

image

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

1. 얕은 비교와 깊은 비교의 사용 기준

부끄러운 이야기지만, 발제날 팀별 모임에서 처음으로 React가 기본적으로 얕은 비교를 사용한다는 사실을 알았습니다.

image

당시에는 정말 단순히 "깊은 비교는 무겁다" 정도로 생각했었지만, 구현을 직접 분석하면서 "React는 어떤 기준으로 얕은 비교를 하고, 어떤 순간에 깊은 비교를 사용할까?"가 명확하게 정리되었습니다.

  • 얕은 비교: 성능을 우선으로 하고, 불변성을 전제로 빠르게 변경 여부를 판단할 때 사용
  • 깊은 비교: 중첩된 객체 구조의 props를 다루거나, 의존성 배열에 객체가 포함된 상황처럼 내부 값까지 정확히 비교해야 할 때 사용

React가 기본은 빠르게, 필요한 순간에만 정확성을 강화하는 비교 전략을 취하고 있다는 것을 확실히 이해할 수 있었습니다.

2. Virtual DOM의 성능에 대한 오해

그동안은 "Virtual DOM이 실제 DOM보다 빠르다"는 말을 막연하게 받아들이고 있었습니다.
그런데 직접 코드를 분석하며 VDOM은 단순히 가벼운 JavaScript 객체일 뿐이고, 그 자체로 성능 우위를 보장하는 기술은 아니라는 걸 알게 됐습니다.
실제로는 VDOM 생성 → diff 계산 → 실제 DOM 반영까지 이어지는 연산 과정 전체가 상황에 따라 더 큰 비용이 들 수 있습니다.

그럼에도 React가 VDOM을 사용하는 이유는, UI를 예측 가능한 방식으로 다루고 선언적으로 작성할 수 있게 해주는 데 있었습니다.
DOM 조작을 일일이 고민하지 않아도 React가 변경 사항을 모아 처리하고 일관된 패턴으로 업데이트해 주기 때문에, 개발자는 구조와 로직에 집중할 수 있다는 점이 특히 크게 와닿았습니다.

기술적 성장

1. Virtual DOM과 Reconciliation의 동작 원리

이번 과제를 통해 Virtual DOM이 실제 DOM 조작을 최소화하기 위한 재조정 알고리즘의 핵심이라는 점을 더 깊이 이해할 수 있었습니다.
reconcileChildren 흐름을 구성하면서 key 기반 매칭과 타입 기반 매칭이 기존 인스턴스를 어떻게 재활용하는지 확인할 수 있었고, 이를 통해 React에서 key prop을 강조하는 이유와 key가 없을 때 인덱스 기반 매칭이 갖는 한계를 분명하게 파악할 수 있었습니다.

2. Hook 시스템의 내부 동작

useStateuseEffect의 동작 과정을 따라가면서, Hook이 컴포넌트의 렌더링 사이클과 어떤 방식으로 연결되는지 이해하게 되었습니다.
특히 currentPathcurrentCursor가 각 컴포넌트의 훅 상태를 path 기반으로 분리하는 구조를 확인하며, 함수 컴포넌트에서도 상태 보존이 가능한 이유를 알 수 있었습니다.

3. 메모이제이션 전략의 차이

useMemo, useCallback, memo의 동작 방식을 살펴보면서 얕은 비교와 깊은 비교가 어떤 기준으로 선택되어야 하는지 명확해졌습니다.
useAutoCallback을 구성할 때는 useRef를 활용해 최신 함수 참조를 안전하게 유지하는 패턴을 적용해 보았고, 이를 통해 클로저와 참조 동등성의 차이를 더 정확히 이해할 수 있었습니다.

4. 렌더링 스케줄링과 비동기 처리

withEnqueue를 사용해 렌더링이 중복으로 발생하지 않도록 조정하는 방식을 확인했습니다.
마이크로태스크 큐를 활용해 여러 상태 변화를 배치로 처리하는 구조가 React의 성능 최적화에 어떻게 기여하는지 체감할 수 있었습니다.

5. 타입 시스템과 경로 기반 상태 관리

createChildPath의 경로 생성 규칙을 분석하면서 컴포넌트 트리에서 각 노드를 안정적으로 식별하는 경로 시스템의 중요성을 이해할 수 있었습니다.
key 존재 여부에 따라 경로가 달라지는 이유와 Fragment, 함수 컴포넌트, DOM 요소마다 서로 다른 경로 형식을 사용하는 의도도 파악할 수 있었습니다.

코드 품질

1. 코드 구조와 가독성 문제

과제 초반, AI가 만든 코드를 그대로 이해하기 위해 “헬퍼 함수 추가 금지, 리팩토링 금지”와 같은 제약을 두면서 코드가 전반적으로 복잡해졌습니다.
함수의 길이가 길어지고, 비슷한 로직이 여러 곳에서 반복되는 등 리팩토링이 필요한 부분이 다수 발견됐습니다.

  • mountNodeupdateInstance는 노드 타입별 로직이 한 함수 안에서 모두 처리되어 있어, 더 작은 단위로 나누어졌다면 흐름을 파악하기가 훨씬 수월했을 것 같습니다.
  • reconcileChildren 역시 key 매칭, 타입 매칭, 자식 정렬 등 여러 역할이 한 함수에 담겨 있어 따로 분리하는 것이 가독성 측면에서 더 좋다고 판단했습니다.
  • createChildPath에서는 Fragment, 함수 컴포넌트, DOM 요소 등 타입별로 다른 경로 규칙을 처리해야 해서 로직이 자연스럽게 복잡해졌습니다.
  • mountNodeupdateInstance에서 자식 노드를 처리하는 부분이 유사하게 반복되고 있어, 이 흐름을 한 번 더 추상화할 여지가 있어 보였습니다.

2. 엣지케이스와 방어적 프로그래밍

테스트를 진행하면서 AI가 작성한 코드에서 여러 엣지케이스와 누락된 최적화를 발견했습니다.

  • props가 없을 때 예외 처리 누락
  • normalizeNode에서 Fragment 처리 누락
  • updateDomProps에서 이벤트 핸들러가 null 또는 undefined로 변경될 때 처리 누락
  • useEffect의 cleanup 실행 순서 문제
  • insertInstance에서 이미 올바른 위치에 있는 노드 처리 누락 → 불필요 DOM 조작 발생
  • anchor가 null일 때 appendChild 처리 누락

또 함수마다 props를 다루는 방식이 달라 일관성이 떨어지는 부분도 있었습니다.
예를 들어 props가 존재하지 않을 때 setDomProps는 early return으로 바로 종료하는 반면, updateDomProps는 기본값을 {}로 사용하는 식이었습니다.
이런 차이는 이후 유지보수 과정에서 혼란을 줄 수 있어, 한 번은 정리해볼 필요가 있다고 느꼈습니다.

학습 효과 분석

과제를 마친 뒤, React의 렌더링 사이클과 재조정 과정을 피그잼으로 직접 다이어그램화하면서 전체 구조를 정리한 경험이 가장 도움이 됐습니다.
코드만으로는 파악하기 어려웠던 흐름이 시각화 과정에서 명확해졌고, 각 함수와 모듈이 어떤 방식으로 연결되는지 한눈에 이해할 수 있었습니다.

다이어그램을 그리며 setup()render()reconcile()mountNode()/updateInstance()reconcileChildren()으로 이어지는 전체 흐름과, 각 단계에서 훅 컨텍스트가 어떻게 관리되는지, 그리고 DOM이 어떤 순서로 갱신되는지를 자연스럽게 정리할 수 있었습니다.
특히 key 기반 재조정 로직과 DOM 재배치 과정이 왜 그런 구조를 갖는지 더 깊이 이해하는 계기가 되었습니다.

또한 setState 호출이 enqueueRender()를 거쳐 다시 render()reconcile() 사이클로 이어진다는 점, memo()deepMemo()가 어느 시점에서 props 비교를 수행하는지, useEffect의 실행과 cleanup 타이밍이 어떻게 결정되는지를 시각적으로 확인할 수 있었습니다.

전체 다이어그램을 완성하고 나니, 각 단계가 따로 존재하는 기능이 아니라 하나의 흐름 속에서 긴밀하게 연결된 사이클이라는 점을 분명하게 이해할 수 있었습니다.

과제 피드백

"항해 절망편 - 나만의 React 만들기"

저에게 이번 과제는 항해 7기 이래 단!언!코! 가장 극악의 난이도였습니다.
지난주의 "프레임워크 없이 SPA 만들기"도 너무 어려웠지만, 이번 과제는 특히 방대한 사전 지식이 필요했습니다.
그동안 React의 내부 구조나 동작 원리에 대해 관심을 가지지 않은 채 단순히 "사용법 익히기"에만 급급했던 제 자신을 반성하게 되었습니다.

제 수준으로는 과제를 직접 수행하기 어려워, "AI가 작성한 코드를 바탕으로 React를 이해해보자"라는 방향을 잡았습니다.
실제로 이번주는 과제를 진행하는 시간보다, AI가 작성한 코드를 이해하고 개념을 하나씩 공부하는 데 훨씬 더 많은 시간이 들었습니다.

이렇게 완수한 과제가 과연 의미가 있을까 싶지만, 그럼에도 나름의 의미를 부여해볼 수 있었습니다.

  • 만약 이 과제를 접하지 않았더라면, 당연하게만 사용해왔던 React의 내부를 깊이 공부해볼 생각을 했을까?
  • 복잡한 React의 구조와 웹 기술의 발전과 더불어 그들이 해결하고자 했던 문제들을 조금이나마 이해할 수 있었던 시간이었다.
  • 앞으로 React뿐 아니라 다른 어떤 프레임워크나 라이브러리를 학습할 때도, 적어도 "사용법 익히기"를 벗어나 단순히 지나쳤던 부분을 직접 "탐구하려는 자세"를 가지게 되었다.

이번 과제를 통해 부족한 부분을 확인하고, 더 깊이 있는 학습을 이어갈 동기를 얻을 수 있었습니다.
앞으로는 단순한 사용법을 넘어 기술의 본질을 직접 파헤치며 성장하겠습니다.
2주간 값진 배움을 얻을 수 있었던 발제에 진심으로 감사드립니다.

리뷰 받고 싶은 내용

AI가 작성한 코드를 분석하면서 대부분의 동작은 이해할 수 있었지만, 아래 두 부분은 제가 제대로 이해한 게 맞는지 확신이 안 들어서 실제 React 동작과 비교해 확인하고 싶습니다.

1. reconcileChildren에서 DOM을 역순으로 재배치하는 이유

현재 구현에서는 자식 노드를 역순으로 삽입하는 방식으로 DOM 위치를 맞추고 있습니다.
제가 이해한 바는 다음과 같습니다:

  • 기존 DOM 노드를 재사용하면서 앞에서부터 삽입하면 anchor 값이 계속 바뀌어 엉뚱한 위치로 들어갈 수 있다.
  • 뒤에서부터 처리하면 anchor가 안정적으로 유지돼 예기치 않은 DOM 이동이나 중간 렌더 비용을 줄일 수 있다.

이 해석이 맞는지 확인하고 싶습니다.
또한 React는 실제로 어떤 순서로 child를 배치하며, 그 방식이 성능이나 안정성 측면에서 가지는 장점과 단점이 무엇인지도 궁금합니다.

2. useEffect의 cleanup과 effect 실행 시점

원래 AI 구현에서는 useEffect의 cleanup이 의존성이 바뀌는 순간 바로 동기적으로 실행되었습니다.
그런데 실제 React는 commit 단계 이후에 cleanup과 effect를 처리한다고 알고 있어서, 저는 cleanup을 useEffect 내부에서 즉시 실행하지 않고, 렌더가 끝난 뒤에 flush 단계에서 cleanup을 먼저 실행하고, 이후 새로운 effect를 실행하며, 마지막으로 cleanup을 다시 저장하는 순서로 수정했습니다.

이 흐름을 기준으로 확인하고 싶은 점은 다음과 같습니다:

  • 제가 수정한 방식이 특정 상황에서 문제를 일으킬 수 있는지 알고 싶습니다.
  • cleanup을 commit 후 단계에서 effect보다 먼저 실행하는 방식이 실제 React와 어떻게 다른지 알고 싶습니다.
  • React가 passive effect를 처리할 때, 이전 cleanup과 새로운 effect가 어떤 순서로 실행되는지 정확히 확인하고 싶습니다.

- 컨테이너 유효성 검사 (null 및 HTMLElement 타입 체크)
- null 루트 엘리먼트 검사
- 이전 렌더링 내용 정리 및 컨테이너 초기화
- cleanupUnusedHooks 호출로 이전 훅 cleanup 실행
- 루트/훅 컨텍스트 리셋
- 첫 렌더링 실행
- 루트 렌더 사이클 구현
- reconcile 함수 기본 구조 구현 (null 처리, 마운트, 업데이트 분기)
- 노드 타입별 마운트 로직 구현 (TEXT, HOST, COMPONENT, FRAGMENT)
- 노드 타입별 업데이트 로직 구현
- 자식 노드 reconciliation 구현 (key 기반 매칭 및 역순 순회)
- 컴포넌트 렌더링 시 visited 관리 추가
- key 비교를 Object.is로 개선하여 정확도 향상
- HOST 마운트 시 자식 노드 DOM 삽입 순서 보장
- flushEffects에서 이전 cleanup을 먼저 실행하도록 수정
- hook.kind 체크 추가
- cleanup 실행 위치를 flushEffects 내부로 통합
- updateDomProps에서 이벤트 핸들러가 null/undefined로 변경된 경우 제거 처리 추가
- insertInstance에서 이미 올바른 위치에 있는 노드는 건너뛰도록 최적화
- insertInstance에서 anchor가 null일 때 appendChild 처리 추가
- useState의 lazy initialization을 사용하여 ref 객체를 한 번만 생성
- 리렌더링 시에도 동일한 ref 객체 참조 유지
- ref.current 변경 시 리렌더링 트리거하지 않음
- useRef를 사용하여 이전 의존성 배열과 계산된 값을 저장
- equals 함수로 의존성을 비교하여 변경 시에만 factory 함수 실행
- 의존성이 동일하면 캐시된 값 반환하여 메모이제이션 구현
- useMemo를 사용하여 함수를 메모이제이션
- 의존성이 변경되지 않으면 같은 함수 참조 반환
- 의존성이 변경되면 새로운 함수 반환하여 메모이제이션 구현
- useMemo를 사용하되 deepEquals를 equals 함수로 전달하여 깊은 비교 수행
- 중첩된 객체와 배열도 재귀적으로 비교하여 메모이제이션 구현
- useRef를 사용하여 이전 props와 렌더링 결과를 저장
- equals 함수로 이전 props와 현재 props를 비교하여 렌더링 여부 결정
- props가 변경되면 컴포넌트를 재렌더링하고, 동일하면 이전 렌더링 결과를 재사용하여 리렌더링 방지
- 이전 props를 항상 현재 props로 업데이트하여 다음 비교를 준비
- memo HOC와 deepEquals 함수를 사용하여 깊은 비교 수행
- 중첩된 객체와 배열도 재귀적으로 비교하여 메모이제이션 구현
- useRef를 사용하여 최신 함수를 저장
- useCallback을 사용하여 안정적인 함수 참조 생성
- 함수 참조는 변경되지 않으면서 항상 최신 상태를 참조하는 콜백 구현
- setDomProps에서 props null 체크
- reconciler에서 모든 node.props 접근에 옵셔널 체이닝 적용
- 컴포넌트 호출 및 updateDomProps에서 props 기본값 처리
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