Skip to content

Conversation

@jumoooo
Copy link

@jumoooo jumoooo commented Nov 19, 2025

과제 체크포인트

배포 링크

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

기본과제

image image

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

과제 셀프회고

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

JSX와 createElement의 관계: 테스트 코드에 createElement가 없는데 어떻게 통과되는지 의문이었는데, JSX가 내부적으로 createElement를 자동 호출한다는 사실을 깨닫고 이마를 탁 쳤습니다. 이는 Babel이 JSX를 createElement 호출로 변환하는 트랜스파일 과정을 이해하는 중요한 순간이었습니다.

VNode(청사진) vs Instance(실체): 처음엔 두 개념이 혼동되었으나, VNode는 설계도이고 Instance는 실제 DOM, children, key 등을 들고 있는 현장의 실체라는 개념이 잡히면서 Reconcile 로직을 정리할 수 있었습니다. 특히 reconcile 함수에서 기존 Instance와 새 VNode를 비교해 '버릴지, 고칠지, 새로 만들지' 판단하는 최적화 로직을 직접 구현해 본 것이 가장 큰 깨달음이었습니다.

Hook의 전역 관리: 지난 과제에서 상태 관리를 전역에서 해야 한다고 생각해서 뺐었는데, 실제로 React도 Context나 Hook(Map, Set 등)을 전역 컨텍스트에서 관리한다는 것을 확인하고 "내 예상이 틀리지 않았구나!" 하는 확신을 얻었습니다. Path 기반 상태 격리 시스템을 통해 각 컴포넌트의 훅 상태를 Map<string, Hook[]> 형태로 관리하는 구조를 이해하게 되었습니다.

Fragment와 DOM 평탄화: <>(Fragment)나 컴포넌트는 실제 DOM이 null이기 때문에, 왜 부모가 자식 노드를 찾을 때 'DOM 평탄화' 과정이 필요한지, 그리고 왜 insertBefore 같은 로직이 중요한지 화면 렌더링 순서 버그(Footer가 위로 올라가는 현상)를 고치며 깊이 이해했습니다. Fragment는 자체 DOM이 없으므로 자식들을 재조정할 때 부모 DOM을 사용해야 한다는 점을 배웠습니다.


기술적 성장

함수형 컴포넌트의 처리: 일반 태그와 달리 함수형 컴포넌트는 children을 직접 가지는 게 아니라, 함수 실행 결과(props 전달)를 렌더링해야 한다는 점을 테스트 실패를 통해 배웠습니다. 더 중요한 것은 함수형 컴포넌트의 자식이 부모와 독립된 고유한 경로를 가져야 한다는 점이었습니다. reconcile 함수에서 createChildPath를 사용하여 자식의 고유 경로를 생성하도록 수정하여 Path 충돌 문제를 해결했습니다.

DOM 렌더링 순서 제어: E2E 테스트 중 Footer, Header, Main의 순서가 뒤죽박죽 섞이는 문제가 발생했습니다. 원인은 key 기반 인스턴스 매칭은 올바르게 작동하지만, DOM 순서 재배치 로직이 누락되어 있었기 때문입니다. 역순으로 순회하여 다음 인스턴스의 첫 DOM 노드를 anchor로 사용하는 insertBefore 전략을 구현하여 해결했습니다. 이 과정에서 DOM 노드를 물리적으로 이동시켜야 한다는 점을 이해하게 되었습니다.

useRef와 리렌더링: useRef가 단순한 변수가 아니라, 리렌더링 되어도 버려지지 않는 '보관함'이며, 내부 값을 바꿔도 보관함 자체는 그대로라 리렌더링을 유발하지 않는다는 원리를 명확히 했습니다. useRef는 lazy initializer 패턴을 사용하여 최초 한 번만 초기값을 평가하고, 이후에는 같은 참조를 반환하여 값 보존과 리렌더링 방지를 동시에 달성합니다.

이벤트 위임 패턴: 이벤트 시스템을 구현하며 이벤트 위임(Event Delegation) 패턴을 깊이 이해하게 되었습니다. 모든 이벤트 리스너를 루트 컨테이너에 한 번만 부착하고, 실제 이벤트 발생 시 타겟 요소를 찾아 핸들러를 실행하는 방식입니다. React DOM 스타일로 createRoot 시점에 이벤트 루트를 설정하고, 전역 이벤트 레지스트리를 통해 이벤트 타입을 관리하는 구조를 구현했습니다.

useEffect cleanup 실행 조건: 무한 스크롤 기능이 작동하지 않는 문제를 디버깅하며, useEffect의 cleanup이 의존성 변경 없이도 실행되는 문제를 발견했습니다. cleanup은 shouldRunEffecttrue일 때만 실행되어야 하며, 의존성이 변경되지 않았으면 기존 훅을 유지하여 cleanup 함수를 보존해야 한다는 점을 배웠습니다.

Path 기반 상태 격리: 중첩된 컴포넌트에서 useState가 각각 독립적으로 동작하도록 Path 충돌을 방지하는 메커니즘을 구현했습니다. 타입이 다른 컴포넌트가 같은 path를 사용하지 않도록 타입 식별자를 추가하여 고유한 path를 생성하는 로직을 추가했습니다.


코드 품질

만족스러운 구현:

  • Path 충돌 방지 로직: 타입이 다른 컴포넌트가 같은 path를 사용하지 않도록, 타입 식별자를 추가하여 고유한 path를 생성하는 로직을 구현했습니다. 이를 통해 중첩된 컴포넌트에서 useState가 각각 독립적으로 동작하도록 보장했습니다.

  • 함수형 컴포넌트 Path 수정: createChildPath를 사용하여 함수형 컴포넌트의 자식이 부모와 독립된 고유한 경로를 갖도록 수정하여 memo HOC가 정상적으로 동작하도록 했습니다.

리팩토링 필요:

  • 이벤트 디버깅 도구: 현재 window.__REACT_DEBUG_EVENTS__ 플래그를 활용해 로그를 찍어보고 있지만, 더 체계적인 디버깅 도구가 필요할 수 있습니다. 이벤트 시스템의 상태를 시각화하거나 이벤트 흐름을 추적하는 도구가 있다면 추가해 보고 싶습니니다.

설계 관련:

  • 학습과 구현의 분리: AI를 활용한 학습 내용을 .cursor/mockdowns/study 폴더로 분리하고, 실제 구현 과정과 문제 해결 내용은 .cursor/mockdowns/react-implementation에서 진행하여 학습과 구현의 경계를 명확히 하려 노력했습니다. 이를 통해 추후 교육 자료로 활용할 수 있도록 구성했습니다.

학습 효과 분석

가장 큰 배움:

Reconcile 과정에서 기존 Instance와 새 VNode를 비교해 '버릴지, 고칠지, 새로 만들지' 판단하는 최적화 로직을 직접 구현해 본 것이 가장 컸습니다. 특히 타입이 다른 컴포넌트가 같은 path를 사용하지 않도록 Path 충돌을 방지하는 메커니즘을 구현하며, 컴포넌트 트리 구조와 상태 격리의 관계를 깊이 이해하게 되었습니다.

함수형 컴포넌트의 Path 충돌 문제를 해결하며, 각 컴포넌트가 트리 구조에 따라 고유한 경로를 부여받아야 훅 상태가 완전히 격리된다는 점을 배웠습니다. createChildPath를 사용하여 자식의 고유 경로를 생성하는 것이 얼마나 중요한지 깨달았습니다.

추가 학습 필요:

이벤트 시스템을 구현하면서 무한 스크롤 기능 등에서 이벤트가 제대로 등록/해제(cleanup)되지 않는 문제를 겪었습니다. React의 합성 이벤트(Synthetic Event)나 이벤트 위임 방식에 대해 더 깊은 학습이 필요함을 느꼈습니다. 특히 JSDOM 호환성 문제를 해결하며 네이티브 이벤트를 직접 사용하는 방식으로 수정했지만, 실제 React의 Synthetic Event 구현 방식을 더 자세히 공부하고 싶습니다.


과제 피드백

과제에서 좋았던 부분:

테스트 코드가 처음엔 난해하게 느껴졌습니다. 특히 container 내부가 어떻게 변하는지(예: childNodes 길이 등)를 검증하는 로직을 이해하는 데 시간이 걸렸지만, 덕분에 DOM 조작의 결과를 확실히 알 수 있었습니다. 테스트 코드를 통해 구현이 올바른지 검증할 수 있어 안정감을 느꼈습니다.

React 내부 구현체의 복잡함을 단계별로 쪼개서 구현해 볼 수 있는 점이 좋았습니다. Phase별로 나누어져 있어 단계적으로 학습하고 체계적으로 구현할 수 있었습니다.

과제에서 모호하거나 애매했던 부분:

초기에는 JSX와 createElement의 관계가 명확하지 않아 혼란스러웠지만, Babel의 트랜스파일 과정을 이해하고 나니 자연스럽게 해결되었습니다. 이는 학습 과정에서 자연스럽게 해결되는 부분이었습니다.


리뷰 받고 싶은 내용

이벤트 리스너 관리 및 디버깅

무한 스크롤 구현 시 이벤트가 중간에 사라지거나 중복 등록되는 이슈가 있었습니다. 문제의 원인은 useEffect cleanup이 의존성 변경 없이도 실행되는 것이었고, 이를 해결하기 위해 cleanup은 shouldRunEffecttrue일 때만 실행되도록 수정했습니다.
또한 updateDomProps에서 이벤트 핸들러를 등록하기 전에 Object.is(prevValue, nextValue) 체크를 먼저 수행하여 함수 참조가 같으면 이벤트 핸들러 등록 로직을 건너뛰는 문제를 발견했습니다. 이벤트 핸들러 처리 로직을 Object.is 체크 이전으로 이동하여 항상 재등록하도록 수정했습니다.

현재 window.__REACT_DEBUG_EVENTS__ 플래그를 활용해 로그를 찍어보고 있는데, 실제 프레임워크 레벨에서 이벤트를 디버깅하거나 관리할 때 주로 사용하는 패턴이나 팁이 있을까요? 특히 이벤트 위임 환경에서 이벤트 핸들러가 제대로 등록/해제되는지 확인하는 방법에 대해 조언을 받고 싶습니다.

@jumoooo
Copy link
Author

jumoooo commented Nov 22, 2025

useRef 그냥 useState 사용해서구현 했는데 더 좋은 방법으로 하는 방법이 있나 궁금하네요

import { useState } from "../core";

/**
 * 리렌더링되어도 변경되지 않는 참조(reference) 객체를 반환합니다.
 * .current 속성을 통해 값에 접근하고 변경할 수 있습니다.
 *
 * @param initialValue - ref 객체의 초기 .current 값
 * @returns `{ current: T }` 형태의 ref 객체
 */
export const useRef = <T>(initialValue: T): { current: T } => {
  // useState를 사용하여 ref 객체를 한 번만 생성하도록 합니다.
  // lazy initialization을 사용하여 초기 렌더링 시에만 객체를 생성합니다.
  const [refObject] = useState(() => ({ current: initialValue }));

  // 리렌더링이 되어도 같은 객체 참조를 반환합니다.
  // ref.current 값을 변경해도 리렌더링을 트리거하지 않습니다.
  return refObject;
};

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.

2 participants