[5팀 오태준] Chapter 2-2. 나만의 React 만들기 #42
Open
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
과제 체크포인트
배포 링크
https://taejun0.github.io/front_7th_chapter2-2/
기본과제
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 비교 기반 컴포넌트 메모이제이션심화 과제 완료 기준:
advanced.hooks.test.tsx,advanced.hoc.test.tsx과제 셀프회고
아하! 모먼트 (A-ha! Moment)
Reconciliation의 anchor 메커니즘과 DOM 재배치 로직:
가장 어려웠던 부분은 key 기반 자식 노드 재배치였습니다.
[a, b, c]→[b, c, a]로 변경할 때,b가 첫 번째 위치로 이동해야 하는데 계속 실패했습니다.처음에는 단순히
nextSibling을 anchor로 사용했지만, 재배치 과정에서 DOM이 아직 이동하지 않은 상태이기 때문에 anchor 계산이 잘못되었습니다.해결책은 역순으로 anchor를 계산하는 것이었습니다:
nextChildren을 역순으로 순회하면서 각 자식의 anchor를 계산insertBefore호출 시,currentDom이 이미anchor.previousSibling이 아니거나, 첫 번째 위치가 아니면 무조건 이동특히
b가 첫 번째로 이동할 때,currentDom.nextSibling === anchor인 경우에도currentPrevSibling !== null이면 이동해야 한다는 점을 깨달았습니다. 이는 DOM이 이미 올바른 상대 위치에 있어도, 절대 위치가 다르면 이동이 필요하기 때문입니다.컴포넌트 경로 변경과 상태 마이그레이션:
Footer컴포넌트의 상태가Item개수 변경 시 유지되지 않는 문제가 있었습니다. 원인은 컴포넌트의 경로(path)가 변경되면hooks.state와hooks.cursor가 새 경로에 없어서 상태가 초기화되는 것이었습니다.처음에는 경로 계산 로직을 수정하려고 했지만, 실제 문제는 경로 변경 시 상태를 마이그레이션하지 않는 것이었습니다.
해결책은
updateComponent에서 경로가 변경될 때:oldPath에서hooks.state,hooks.cursor,hooks.visited를 읽어옴newPath로 복사oldPath의 데이터를 삭제context.hooks.cursor.set(path, 0)호출 전에 수행이렇게 하면 경로가 변경되어도 컴포넌트의 상태가 유지됩니다.
Fragment의 DOM 노드 수집과 제거:
{hasChildren && <>...</>}에서hasChildren이 false가 되면 Fragment의 자식들이 제거되어야 하는데, 계속 남아있었습니다.문제는
getDomNodes함수가FRAGMENT와COMPONENT를 제대로 처리하지 못했던 것입니다.instance.dom을 직접 반환하면 Fragment는 DOM이 없어서 빈 배열이 반환되었습니다.해결책은
getDomNodes에서:FRAGMENT와COMPONENT는instance.dom이 없으므로 재귀적으로 자식들의 DOM을 수집NodeTypes.FRAGMENT와NodeTypes.COMPONENT를 명시적으로 비교 (이전에는instance.kind === 'FRAGMENT'로 비교했는데 타입 오류 발생)updateComponent에서childNode가 null이거나 빈 children일 때,prevChild가 Fragment면 명시적으로reconcile(parentDom, prevChild, null, path)호출이렇게 하면 Fragment의 모든 자식 DOM이 재귀적으로 제거됩니다.
기술적 성장
Reconciliation 알고리즘의 복잡성 이해:
이번 과제를 통해 Reconciliation이 단순히 "이전과 현재를 비교"하는 것이 아니라, 여러 전략을 조합해야 한다는 것을 깊이 이해했습니다:
또한 anchor를 사용한 DOM 재배치 로직에서:
insertBefore의 동작 원리와previousSibling의 관계디버깅 전략의 중요성:
문제가 계속 해결되지 않을 때, "같은 방식으로 계속 시도"하는 것보다는:
console.log를 추가해 실행 흐름 추적특히
console.log를 전략적으로 배치해, 어떤 함수에서 문제가 발생하는지 정확히 파악한 것이 큰 도움이 되었습니다.상태 관리와 경로의 관계:
React의 Hook 시스템이 컴포넌트 경로에 의존한다는 것을 직접 구현하며 이해했습니다:
hooks.visited를 사용해 현재 렌더링에서 방문한 컴포넌트만 추적cleanupUnusedHooks에서 방문하지 않은 컴포넌트의 상태를 정리코드 품질
특히 만족스러운 구현:
리팩토링이 필요한 부분:
reconcile함수가 1000줄이 넘어 가독성이 떨어집니다.mountHost,updateHost,mountComponent,updateComponent등을 별도 파일로 분리하면 좋을 것 같습니다.updateHost와updateFragment의 로직이 거의 동일하므로, 공통 로직을 추출해 중복을 제거할 수 있습니다.console.log가 많이 남아있어 제거가 필요합니다.코드 설계 관련 고민과 결정:
reconcile함수에 anchor 파라미터를 추가했습니다. 이는 재배치 로직을 크게 개선했지만, 함수 시그니처가 복잡해졌습니다."0.c0.i1.c2")를 사용해 계층 구조를 표현했습니다. 디버깅에 유용하지만, 경로 변경 시 마이그레이션이 필요해 복잡도가 증가했습니다.NodeTypes.FRAGMENT와 같은 상수를 사용해 타입을 비교했습니다. 이는 타입 안정성을 높이지만, 런타임 비교가 필요합니다.학습 효과 분석
가장 큰 배움이 있었던 부분:
insertBefore와removeChild를 사용할 때, 노드의 현재 위치와 부모를 정확히 확인해야 합니다.추가 학습이 필요한 영역:
any타입을 사용하는 부분을 더 엄격한 타입으로 개선할 수 있을 것 같습니다.실무 적용 가능성:
과제 피드백
과제에서 모호하거나 애매했던 부분:
createChildPath함수의siblings파라미터: 실제로는 사용되지 않았는데, 시그니처에 포함되어 있어 혼란스러웠습니다.{hasChildren && <>...</>}에서hasChildren이 false일 때 Fragment가 null이 되어야 하는지, 빈 Fragment로 처리해야 하는지 명확하지 않았습니다. 테스트를 통해 null이 되어야 함을 확인했습니다.과제에서 좋았던 부분:
리뷰 받고 싶은 내용
Reconciliation 로직의 구조 개선 방안:
현재
reconcile함수가 1000줄이 넘어 가독성이 떨어집니다.mountHost,updateHost,mountComponent,updateComponent등을 별도 파일로 분리하는 것이 좋을까요? 아니면 현재처럼 하나의 파일에 유지하는 것이 더 나을까요?또한
updateHost와updateFragment의 로직이 거의 동일한데, 공통 로직을 추출해 중복을 제거할 수 있을까요? 어떤 방식으로 리팩토링하면 좋을지 조언해주실 수 있을까요?컴포넌트 경로 관리 전략:
현재 컴포넌트 경로를 문자열(
"0.c0.i1.c2")로 표현하고 있는데, 경로 변경 시 상태 마이그레이션이 필요합니다. 더 효율적인 경로 관리 방식이 있을까요?예를 들어:
어떤 방식이 더 나을지 조언해주실 수 있을까요?
anchor 계산 로직의 최적화:
현재 anchor 계산 로직이 복잡하고, 역순 순회를 사용하고 있습니다. 더 효율적이거나 명확한 방법이 있을까요?
특히 key 기반 재배치에서:
insertBefore호출 조건이 부분들을 더 간결하게 표현할 수 있는 방법이 있을까요?
타입 안정성 개선:
현재
any타입을 일부 사용하고 있는데, 더 엄격한 타입 정의로 개선할 수 있을까요?특히:
normalizeNode함수에서any를 사용하는 부분createElement의 children 타입string | symbol | React.ComponentType인데, 이를 더 구체적으로 타입화할 수 있는 방법이런 부분들을 어떻게 개선하면 좋을지 조언해주실 수 있을까요?
디버깅 전략:
문제가 계속 해결되지 않을 때,
console.log를 추가해 디버깅하는 방식 외에 더 효과적인 방법이 있을까요?예를 들어:
어떤 방식이 더 효율적일지 조언해주실 수 있을까요?