-
Notifications
You must be signed in to change notification settings - Fork 49
[5팀 박수범] Chapter2-2. 나만의 React 만들기 #22
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
Conversation
[Phase 1] TDD의 첫 단계로, 리액트 최적화의 기반이 되는 두 가지 핵심 비교 함수를 구현합니다.
이 커밋은 `basic.equals.test.tsx`의 모든 테스트 케이스를 통과합니다.
### 주요 변경 사항
1. **`shallowEquals` (얕은 비교)**
* `Object.is`를 사용하여 기본 타입 및 참조를 정확하게 비교합니다.
* 배열과 객체는 1단계(1-level) 깊이까지만 순회하며 비교합니다.
* `useEffect`, `useMemo` 등의 의존성 배열을 비교하는 표준 로직의 기반이 됩니다.
2. **`deepEquals` (깊은 비교)**
* 재귀(Recursion)를 사용하여 중첩된 객체와 배열의 모든 속성 값을 비교합니다.
* 향후 `deepMemo` HOC(고차 컴포넌트) 구현 시 props를 비교하는 데 사용됩니다.
`basic.mini-react.test.tsx`의 `createElement` 관련 테스트를 통과시키기 위해 두 가지 로직을 수정합니다.
1. **텍스트 노드 `nodeValue` 문자열 변환:**
JSX의 `{index}`(숫자)가 `createTextElement`를 거칠 때,
`nodeValue`가 `number` 타입으로 할당되는 문제를 수정.
`String()`으로 명시적 변환을 추가하여 테스트의 `string` 타입 기대값과 일치시킴.
2. **컴포넌트 `children` 처리:**
`createElement`가 함수형 컴포넌트(자식이 없음)에도
`props.children = []`을 할당하던 문제를 수정.
타입이 DOM/Fragment이거나 `children.length > 0`일 때만
`props.children`을 할당하도록 분기하여,
테스트의 `expected` VNode 구조와 일치시킴.
[Phase 3] VNode의 props를 실제 DOM 노드에 적용하는 'DOM 인터페이스'를 구현합니다.
이 커밋은 `basic.mini-react.test.tsx`의 "3단계" 테스트 중 함수형 컴포넌트를 사용하지 않는 'style 객체' 테스트를 통과시킵니다.
### 주요 변경 사항
1. **`setDomProps` / `updateDomProps` 구현:**
* `style` 객체를 `dom.style` 프로퍼티로 변환합니다.
* `on...` 이벤트 핸들러를 `addEventListener/removeEventListener`로 처리합니다.
* `className`, `checked`, `disabled` 등은 DOM 프로퍼티(property)로 설정합니다.
* `data-` 속성 등은 `setAttribute/removeAttribute`로 처리합니다.
2. **`setup.ts` 업데이트:**
* `setup`의 임시 `mount` 함수가 하드코딩된 `className` 대신, `core/dom`의 `setDomProps`를 호출하도록 수정하여 `style` 처리가 가능하도록 했습니다.
3. **DOM 탐색 유틸리티 구현:**
* `getDomNodes`, `getFirstDom`, `insertInstance`, `removeInstance`를 구현했습니다. 이 함수들은 `Fragment`나 컴포넌트처럼 DOM 노드를 직접 소유하지 않는 인스턴스를 재귀적으로 탐색하여 올바르게 삽입/제거하는 데 사용됩니다.
[Phase 3] TDD 사이클을 완료하여 1, 2, 3단계의 모든 기본 렌더링 테스트를 통과시킵니다.
1. core/dom.ts (v4):
- `boolean 속성의 토글` 테스트 통과를 위해 `isProperty` 헬퍼에 `readOnly` (camelCase)를 추가합니다.
- boolean 속성 제거 시 `""` 대신 `false`를 할당하도록 `updateDomProps`를 수정합니다.
2. core/reconciler.ts (v9):
- `props.children과 배열 자식을 정규화한다` 테스트 통과를 위해 `reconcileChildren`의 `anchor` (삽입 기준점) 계산 로직을 수정합니다.
- `reconcileChildren`이 `startAnchor` 인자를 받도록 시그니처를 변경하고, `mountHost`와 `mountFragment`가 올바른 `anchor`를 전달하도록 수정합니다.
[Phase 4, 5, 6 Complete] MiniReact의 핵심 동적 기능을 구현하고, 정적/동적 테스트의 모든 의존성을 해결합니다. 1. **Reconciler:** mountComponent/updateComponent 로직 구현 및 Fragment 처리 개선. 2. **Hooks:** useState 구현 (lazy init, bailout, enqueueRender 호출). 3. **Scheduling:** enqueue/withEnqueue 및 render/enqueueRender 시스템 연결. 4. **Fixes:** updateDomProps에서 TextNode 업데이트 로직을 추가하여 DOM 업데이트 동기화 버그를 해결.
[Phase 4, 5, 6 통합] MiniReact의 핵심 동적 기능을 구현하고, 모든 의존성과 버그를 해결합니다. 1. Hooks (P6): useState, useEffect의 전체 라이프사이클 및 상태 격리 로직 구현. 2. Reconciliation (P5): Keyed Reconciliation Algorithm을 완성하여 동적 리스트의 상태를 보존하고, DOM 순서(anchor) 버그를 해결. 3. Scheduling (P4): enqueueEffects 구현으로 useEffect의 비동기 실행 및 클린업을 보장. 4. Stability: TextNode 업데이트 및 boolean prop 처리 버그를 해결하여 DOM 조작 안정성을 확보.
[Phase 4 & 6] 동적 렌더링을 위한 핵심 파이프라인을 구축했습니다. 1. Reconciler (v19): - Key 기반 리스트 비교 알고리즘 구현. - 'State Collision Guard' 추가: 리스트 아이템 삭제 시, 이동해 온 다른 컴포넌트의 상태를 삭제하지 않도록 보호 로직 구현. - Strict Type Matching: Key가 없어도 컴포넌트 타입이 다르면 재사용하지 않도록 수정. 2. Hooks (v14): - useState: Lazy initializer, 상태 격리, 리렌더링 예약(enqueueRender) 구현. - useEffect: 기본 실행 및 스케줄링 구조 구현 (클린업 타이밍 튜닝 필요). 3. DOM (v6): - readOnly 프로퍼티 처리 및 TextNode 업데이트 로직 수정.
[Phase 7 Complete] 성능 최적화 및 참조 유지를 위한 심화 기능들을 구현하여 Advanced 테스트를 모두 통과합니다. 1. Hooks: - useRef: 리렌더링을 유발하지 않는 영속적 값 보존 로직 구현. - useMemo/useCallback: 의존성 배열 비교(shallow/deep)를 통한 값/함수 메모이제이션. - useAutoCallback: 최신 상태를 캡처하면서도 참조 안정성을 보장하는 래퍼 구현. 2. HOCs & Reconciler: - memo/deepMemo: 컴포넌트에 `__memoConfig` 메타데이터를 부여. - Reconciler: 업데이트 시 HOC의 메타데이터를 확인하고, Props 비교(Equals)를 통과하면 렌더링을 건너뛰는(Bailout) 최적화 로직 추가.
useMemo 훅 내부에서 useState를 호출할 때 초기값을 제공하지 않아, 첫 렌더링 시 cache가 undefined로 설정되는 문제가 있었습니다. 이로 인해 cache.deps에 접근 시 TypeError가 발생하며 렌더링이 실패했습니다. useState에 factory 함수를 실행하여 얻은 값과 deps를 포함하는 객체를 생성하는 초기화 함수를 전달하도록 수정하여 이 문제를 해결했습니다.
[Phase 1-7 Finalized] MiniReact의 모든 기본 및 심화 기능을 구현하고 브라우저 런타임 안정성을 확보합니다. 1. Fix: '로딩 화면 멈춤' 버그 해결: useEffect 내부의 불필요한 이펙트 스케줄러(enqueueEffects) 호출을 제거하여 렌더 루프와의 타이밍 충돌(Race Condition)을 방지함. 2. Final Check: TextNode 업데이트, Keyed reconciliation, 상태 이동, Hook 시스템 등 모든 로직이 최종적으로 안정화되었습니다.
packages/react/src/core/reconciler.ts – 컴포넌트 경로 분리 컴포넌트가 다른 컴포넌트를 반환할 때 자식 전용 경로(.c0)를 생성하도록 변경 부모/자식 간 훅 상태 충돌 방지로 effect·상태 저장소가 올바르게 유지 packages/app/src/App.jsx – 초기 렌더 보강 useEffect에서 스토어 구독 직후 forceUpdate() 한 번 호출 데이터가 effect 실행 이전에 도착해도 화면이 즉시 최신 상태를 반영하도록 보장
- core/dom.ts에서 DOM 프로퍼티 처리 대상에 selected를 추가해,
<option selected={...}>가 attribute가 아닌 프로퍼티로 반영되도록 수정
- 브라우저가 초기 렌더 시 남아 있는 selected attribute 때문에
limit=100, sort=name_asc처럼 잘못된 기본값이 유지되던 현상 해결
- HomePage/SearchBar는 기존 구현을 그대로 두고, 렌더링 레이어에서만
선택 상태를 올바르게 동기화하도록 조정
1. Hooks Refactoring: - useRef: useState 의존성을 제거하고 Hooks 배열에 직접 접근하는 방식으로 리팩토링 - useMemo: useState 대신 useRef를 캐시 저장소로 사용하여 불필요한 리렌더링 스케줄링 방지.
[Refactoring] hooks.ts와 render.ts 간의 순환 참조를 제거하고 렌더링 성능을 최적화합니다. 1. 구조적 개선 (Dependency Resolution): - `render.ts`에서 `enqueueEffects()` 호출을 제거하여 `hooks` 모듈에 대한 불필요한 의존성을 끊었습니다. - `useEffect` 훅 내부에서 실행 조건(`shouldRun`)이 충족될 때 스스로 `enqueueEffects()`를 호출하도록 변경했습니다. - 이로써 모듈 초기화 시점의 `undefined` 참조 문제를 근본적으로 해결했습니다. 2. 성능 최적화: - 매 렌더링 사이클(`render`)마다 무조건 이펙트 큐를 확인하던 로직을 제거했습니다. - 실제 `useEffect`가 사용된 경우에만 스케줄링이 발생하도록 하여, 불필요한 함수 호출 비용을 절감하고 책임 소재를 명확히 했습니다.
[DX Improvement] 조건문/반복문 내 훅 사용으로 인한 순서 꼬임(Hook Mismatch) 발생 시, 개발자에게 경고를 출력하고 크래시를 방지합니다. 1. 문제 상황: - 조건문 내에서 훅을 호출하여 렌더링 간 훅 순서가 달라질 경우, `useRef`가 이전의 `useState` 데이터를 참조하여 런타임 에러 발생 가능성. 2. 해결 방안: - `useRef` 내부에서 현재 커서의 훅 타입이 `ref`가 아닌 경우(예: `state`)를 감지. - 타입 불일치 시 `console.error`로 상세한 경고 메시지 출력 (컴포넌트 경로 및 커서 위치 포함). - 해당 위치의 훅 데이터를 강제로 초기화하여 애플리케이션 멈춤(Crash) 방지.
[Documentation] Reconciler의 핵심 로직에 대한 이해를 돕기 위해 상세한 주석을 추가합니다. 1. 변경 사항: - `reconcile`, `mount`, `update`, `unmount` 등 생명주기 함수에 JSDoc 추가. - `reconcileChildren` 내부의 복잡한 로직(Collision Guard, State Transfer, Anchor Calculation)에 대한 설명 보강. - `getNextUsableAnchor` 등 헬퍼 함수의 역할 명시. 2. 참고: - 로직 변경 없음
| }; | ||
| instance.children = reconcileChildren(parentDom, instance, node.props.children || [], path, anchor); | ||
| return instance; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재는 일관성을 위해 Fragment도 Instance 객체(children, path, key 포함)를 생성하여 메모리에 유지하고 있습니다. 하지만 Fragment는 상태도 없고 DOM도 없는데, 굳이 인스턴스 객체를 만들지 않고 children 배열만 렌더링하도록 최적화하는 것이 성능상 유의미한 이점이 있을까요? 아니면 Reconciler의 일관성을 유지하는 게 더 중요할까요?
| key === "disabled" || | ||
| key === "readOnly" || | ||
| key === "value" || | ||
| key === "selected"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
나 같은 폼 요소의 상태 동기화 문제를 해결하기 위해 isProperty 리스트를 만들어 Attribute 대신 DOM Property를 직접 제어하도록 구현했습니다. 현재는 필요한 속성만 하드코딩 해두었는데 필요시마다 하드코딩으로 속성을 추가해야 할까요? 다른분들은 어떻게 처리하셨는지 궁금합니다. 저는 selected속성을 추가안했어서 DOM Property를 Attribute로 받아버리는 에러를 겪었어서 isProperty 리스트가 중요하다고 생각하는데 하드코딩으로 관리하려니 뭔가 찝찝해서 질문남깁니다.
과제 체크포인트
배포 링크
배포링크
기본과제
Phase 1: VNode와 기초 유틸리티
core/elements.ts:createElement,normalizeNode,createChildPathutils/validators.ts:isEmptyValueutils/equals.ts:shallowEquals,deepEqualsPhase 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 비교 기반 컴포넌트 메모이제이션과제 셀프회고
트러블슈팅 및 해결
Phase 1: VNode와 기초 유틸리티
배열 렌더링 테스트 실패
{index}처럼 숫자를 넣으면 그대로number타입이nodeValue에 전달됐습니다. 그런데 테스트는 문자열을 기대하고 있는 상태였습니다.createTextElement에서String(nodeValue)로 강제 변환하도록 수정했습니다. 덕분에 숫자도 문제없이 화면에 표시됩니다.함수형 컴포넌트 테스트 실패
<TestComponent />)임에도props.children이 빈 배열로 설정되면서 테스트가 꼬였습니다.createElement에 분기 처리를 넣어, DOM/Fragment 타입이거나 실제 자식이 있을 때만props.children을 세팅하도록 변경했습니다.Phase 3: DOM 인터페이스 구축
style 객체 반영 실패
className만 적용하고style은 무시했습니다.div.style.backgroundColor가 항상''가 되면서 테스트가 실패했습니다.core/dom.ts에setDomProps와updateDomProps를 구현했습니다.style, 이벤트, boolean 속성,data-속성 등을 올바른 DOM API로 적용하도록 했습니다.setup.ts에서 mount 연결
setDomProps(dom, node.props)를 호출하도록 수정했습니다.Phase 3: <select> 기본값이 URL/스토어와 다르게 표시된 이슈
원인:
브라우저는
<option>에selectedattribute가 있으면 값이"false"여도 선택된 것으로 처리했습니다.MiniReact는 이걸 attribute만 갱신했기 때문에 UI와 실제 상태가 달랐죠:
limit=20,sort=price_asclimit=100,sort=name_asc가 선택됨<select>보다<option>을 먼저 삽입하면서 브라우저가 초기 선택을 고정하기도 했습니다.해결:
selected를 attribute가 아닌 DOM 프로퍼티로 업데이트하도록 변경core/dom.ts의isProperty목록에"selected"추가(dom as any).selected = value형태로 반영배운 점:
Phase 4: 렌더 스케줄링
렌더 큐 처리 실패
utils/enqueue.ts의enqueue/withEnqueue로 비동기 render 순서를 보장했습니다.루트 렌더 사이클 문제
Phase 5: Reconciliation
reconcileChildren이 anchor를 잘못 계산하여 Fragment 자식이 기존 div 자식 앞에 삽입되었습니다.startAnchor인자를 추가하고 mountHost/mountFragment 로직을 개선하여 DOM 순서를 보장했습니다.Phase 6-1: Footer 상태 초기화
원인:
[Item, Item, Item, Footer]에서 중간 Item이 삭제되면서 Footer가 이동했고, 훅 상태가 초기화되었습니다.해결:
orphanedHooks에 보관아하! 모먼트 (A-ha! Moment)
Phase 1 & 2 — VNode 정규화와 Fragment의 역할
Phase 3 — DOM 인터페이스: VNode Props는 '명령'이다
onClick={handler}→ 이벤트 등록style={{ color: 'red' }}→ dom.style 적용disabled={true}→ DOM 프로퍼티 설정Phase 7-1: useState vs useRef 이해
Phase 7-2: useCallback과 memo 협력
Phase 7-3 — 최종 런타임 안정화와 Hook 시스템의 한계
테스트 통과와 런타임 안정성은 별개의 문제
단위 테스트는 모두 통과했지만, 실제 브라우저 환경(번들러, 모듈 로딩 순서 등)에서는 순환 참조로 인해 앱이 정상적으로 구동되지 않는 문제가 있었습니다.
이를 통해 테스트 케이스만으로는 실제 런타임의 모든 변수를 검증할 수 없다는 점을 확인했고, 통합 환경에서의 실 검증의 중요성을 다시 확인했습니다.
아키텍처 설계와 의존성 관리의 중요성
hooks와render모듈 간의 강한 결합(Coupling)이 순환 참조를 유발했습니다.이를 의존성 주입(Dependency Injection) 방식으로 구조를 재배치해 해결했고, 그 과정에서 역할 분리와 결합도 최소화가 얼마나 중요한지 체감했습니다.
기술적 성장
이번 과제는 리액트의 API를 단순히 '사용'하는 것을 넘어, 그 '동작 원리'를 바닥부터 구현하며 리액트가 나에게 왜 유니크한 키를 강요했는지, 조건문 또는 반복문 내에서 훅을 사용하지 말라고했는지, Fragment 태그는 왜 필요했는지 등 가벼운 호기심이 풀렸습니다.
1. HTML Attribute와 DOM Property의 결정적 차이
기존에는 두 용어를 혼용해서 사용했으나,
<select>요소의 초기값 버그를 디버깅하며 그 차이를 확실히 체감했습니다.setAttribute('selected', 'true')(Attribute)를 호출해도 화면의 선택값은 변하지 않았습니다. Attribute는 초기 상태일 뿐, 현재 상태를 대변하지 않기 때문입니다.element.selected = true)를 제어해야 함을 깨달았습니다. 이를 통해core/dom.ts의updateDomProps로직을 수정하여 UI와 데이터의 동기화를 완벽하게 구현했습니다.2. useState와 useRef의 본질적 차이와 설계 의도
두 훅 모두 "리렌더링 간에 값을 유지한다"는 공통점이 있지만, 구현체 관점에서 명확한 역할의 차이를 이해했습니다.
QnA 시간에 질문에 대한 답을 들으면서 더욱 명확해졌습니다.
enqueueRender를 호출하여 시스템에 "화면을 갱신하라"는 신호를 보냅니다.3. 리액트 최적화의 메커니즘
최적화가 단일 훅만으로 완성되는 것이 아니라, Hook과 Reconciler의 상호작용임을 배웠습니다.
이 구조를 직접 구현하며 "왜 최적화를 위해 두 가지를 함께 써야 하는지"에 대한 명확한 기술적 근거를 갖게 되었습니다.
4. 복잡한 상태 동기화와 아키텍처
구현 과정에서 마주친 난관들을 해결하며 시스템 설계 능력이 향상되었습니다.
실제 useEffect가 사용된 경우에만 스케줄링이 발생하도록 하여, 불필요한 함수 호출 비용을 절감하고 책임 소재를 명확히 했습니다.
코드 품질
1. 특히 만족스러운 구현
hooks.ts의useRef최적화: 처음엔 단순히useState를 래핑하는 쉬운 방법을 택했지만 리팩토링을 통해 리렌더링을 유발하지 않는 별도의 훅 타입을 정의하여 성능과 목적에 부합하는 구현을 해낸 점이 만족스럽습니다.2. 리팩토링이 필요한 부분
core/dom.ts가 충분히 역할을 하고 있지만,setAttribute와 프로퍼티 설정 로직을 조금 더 명확한 인터페이스로 분리하면 유지보수에 유리할 것 같습니다.과제 피드백
과제에서 좋았던 부분
리뷰 받고 싶은 내용
셀렉트박스의 초기값이나 readOnly 속성을 처리하면서 HTML Attribute와 DOM Property의 동작 차이로 인한 버그를 겪었습니다. 현재 core/dom.ts의 updateDomProps와 isProperty 헬퍼 함수를 통해 이를 분기 처리하고 있습니다.
현재 폼 상태 동기화의 정확성을 위해 value, checked 등 핵심 속성만 화이트리스트(isProperty)로 관리하여 Property로 주입하고 있습니다.
혹시 이 방식이 나중에 커스텀 엘리먼트(Web Components)나 비디오/오디오 태그 등을 지원할 때 큰 구조적 변경을 요구할까요? 아니면 그때 리스트를 추가하는 것으로 충분할까요?