과제 체크포인트
배포 링크
https://j2h30728.github.io/front_6th_chapter1-2/
기본과제
가상돔을 기반으로 렌더링하기
- createVNode 함수를 이용하여 vNode를 만든다.
- normalizeVNode 함수를 이용하여 vNode를 정규화한다.
- createElement 함수를 이용하여 vNode를 실제 DOM으로 만든다.
- 결과적으로, JSX를 실제 DOM으로 변환할 수 있도록 만들었다.
이벤트 위임
- 노드를 생성할 때 이벤트를 직접 등록하는게 아니라 이벤트 위임 방식으로 등록해야 한다
- 동적으로 추가된 요소에도 이벤트가 정상적으로 작동해야 한다
- 이벤트 핸들러가 제거되면 더 이상 호출되지 않아야 한다
심화 과제
Diff 알고리즘 구현
- 초기 렌더링이 올바르게 수행되어야 한다
- diff 알고리즘을 통해 변경된 부분만 업데이트해야 한다
- 새로운 요소를 추가하고 불필요한 요소를 제거해야 한다
- 요소의 속성만 변경되었을 때 요소를 재사용해야 한다
- 요소의 타입이 변경되었을 때 새로운 요소를 생성해야 한다
과제 셀프회고
테스트코드를 따라서 가상돔과 Diff 알고리즘을 구현해보면서 항상 사용하는 리액트 동작에 대해서 좀 더 알게되었습니다.
과제 진행 중 가장 큰 실수는 vNode 객체에 실제 DOM 요소를 저장한 것이었습니다.
커밋 99089b1에서 확인할 수 있듯이 vNode.el = element처럼 실제 DOM을 저장했고, 아이러니하게도 이 잘못된 방식이 겉보기에는 정상 작동했습니다.
// ❌ 잘못된 방식
vNode.el = element; // 실제 DOM을 vNode에 저장
updateElement(oldNode.el, newChildren[i], oldChildren[i], i);
// ✅ 올바른 방식
const currentElement = parentElement.childNodes[index]; // 인덱스 기반 접근
실제 DOM요소가 oldNode.el에 저장되어 있어서, 이를 parentNode로 사용하고 diff 알고리즘을 실행했습니다. 결과적으로 화면은 정상적으로 렌더링되지만, 가상돔의 본질을 놓치고 있었습니다.
과제를 진행하면서 "왜 내가 리얼돔을 객체에 넣었을까?"라는 의문이 생기게 되었습니다. createVNode가 반환하는 { type, props, children } 구조의 순수한 객체, 실제 DOM과의 분리 등에 대해서 다시 한 번 더 되짚어 가며 리팩토링을 진행했습니다.
회고를 작성하면서 돌이켜보니, 당시에는 "작동하는 코드"에만 집중하여 설계 원칙을 깊이 고려하지 못했던 것 같습니다. 이를 통해 표면적인 구현을 넘어 근본적인 이해의 중요성을 깨달았습니다.
기술적 성장
children과 childNodes의 차이
DOM API의 children과 childNodes의 미묘한 차이점을 과제를 구현하면서 경험했습니다. 처음에는 children가져와 사용했지만 텍스트 노드가 존재하지 않기떄문에 diff 알고리즘이 실행되면서 인덱스가 일치하지 않아서 인한 버그를 유발시켰습니다. 실제 화면에서도 제대로 나오지않을 뿐만아니라, 테스트 코드가 실패하는 것을 시작으로 디버깅을 진행했습니다. API를 제대로 찾아보지 않고 사용했던 점과 그로인해 DOM API 차이점을 알게 되었고, 테스트 코드의 중요성도 덤으로 알게 되었습니다.
이벤트 위임(Event Delegation) 패턴의 실제 구현
학습자료에서 제공한 내용을 토대로, 루트 요소에서 이벤트를 캐치하고 event.target부터 상위로 탐색하며 적절한 핸들러를 찾는 패턴을 구현했습니다. 동적으로 생성되는 DOM 요소들에도 일관된 이벤트 처리가 가능한 구조의 핵심을 이해했습니다.
코드 품질
특히 만족스러운 구현은 WeakMap으로 메모리 누수 방지, Map으로 이벤트 타입 분류, Set으로 핸들러 중복 방지를 했던 부분입니다. 각 계층이 명확한 역할을 담당하여 동작하도록 구현했습니다.
// WeakMap<element, Map<eventType, Set<handler>>> 구조
const eventListeners = new WeakMap();
리팩토링이 필요한 부분
기능 구현에 집중했기 때문에 클린 코드에 대해서 깊게 고려하지 못했습니다. 하나의 함수에 여러 역할을 하기도 합니다. 이런 함수들에 대해서 관심사 분리를 진행하고 단일책임원칙을 지키고 싶습니다.
코드 설계 관련 고민과 결정
처음에는 단순히 이벤트 매니저내의 이벤트 타입을 미리 정의한 상수 배열로 관리했습니다. 하지만 이 방식은 새로운 이벤트를 추가할 때마다 코드를 수정해야 하고, 사용하지 않는 이벤트에도 리스너를 등록하는 비효율성이 있었습니다.
// ❌ 초기 방식 - 정적 관리
const SUPPORTED_EVENTS = ['click', 'mousedown', 'keyup', 'change'];
// ✅ 개선된 방식 - 동적 관리
const delegatedEvents = new Set(); // 실제 사용되는 이벤트만
이후 Set을 활용한 동적 관리 방식으로 변경했습니다.
실제로 addEvent가 호출될 때만 해당 이벤트 타입을 delegatedEvents에 추가하여, 필요한 이벤트에만 root 리스너를 등록하겠끔 했습니다.
학습 효과 분석
가장 큰 배움이 있었던 부분
removeEvent 함수를 처음 구현할 때는 handler 파라미터의 진짜 용도를 깊게 생각하지 못했습니다.
단순히 이벤트 타입만 제거하면 된다고 생각했고, 특정 핸들러만 정밀하게 제거해야 하는 상황을 고려하지 못했습니다.
// ❌ 초기의 잘못된 생각
removeEvent(element, eventType); // 모든 핸들러 제거
// ✅ 올바른 접근
removeEvent(element, eventType, handler); // 특정 핸들러만 제거
과제의 모든 테스트 코드를 통과시킨 후, 스터디원 영서님의 글을 읽으면서 handler 파라미터가 제공된 진짜 이유를 깨닫게 되었습니다. 같은 요소에 여러 컴포넌트가 동일한 이벤트 타입의 핸들러를 등록했을 때, 특정 컴포넌트가 언마운트되면 해당 핸들러만 정확히 제거해야 메모리 누수를 방지할 수 있다는 것이었습니다.
이를 바탕으로 removeEvent 함수를 리팩토링했습니다.
handler 참조를 정확히 끊어내고, 빈 Set/Map을 정리하여 메모리 누수를 방지하는 방식으로 개선했습니다.
이런 일련의 경험을 통해 지식공유의 힘을 더 깊이 깨달았습니다. 나 또한 누군가에게 도움이 되는 글을 써보고 싶다는 동기가 생겼습니다.
실무 연결
- React의 내부 동작 원리를 이해함으로써 성능 최적화나 디버깅 시 더 효과적인 접근이 가능해졌습니다.
- SPA에서 발생할 수 있는 메모리 누수 패턴을 인식하고 방지하는 능력을 기를 수 있었습니다.
과제 피드백
리뷰 받고 싶은 내용
- 이벤트를 저장하는 저장소 계층 이벤트위임에서 사용되는 이벤트들을 저장해두는 저장소의 계층에 대해서 궁금합니다 요소를 보여주고 보여주지않는다는 기준으로 제일 상위계층을 element를 담았습니다. 그래서 element > event > handler 순으로 저장했었는데요. 다른 분들의 코드를 확인하니 각각 다르게 만들수가 있더라구요. 제가 접근을 좀 다르게 했던 것일까요? 멘토님이 생각하시는 최적의 계층설계가 궁금합니다.
- 런타임 에러 처리 현재 구현에는 에러 처리가 전혀 존재하지않는데, vNode 생성 실패나 DOM 조작 에러 시 핸들링하는 함수는 어떤방향으로 구현이 될까요?
과제 피드백
안녕하세요 이지현님, 수고하셨습니다. 이번 과제는 React의 핵심 원리인 가상 DOM과 diff 알고리즘을 직접 구현해보면서, 프레임워크가 어떻게 효율적인 렌더링을 수행하는지 이해하는 것이 목표였습니다. 특히 "필요가 공부를 만든다"는 것을 체험하며, 단순히 이론을 아는 것과 실제 구현을 위해 필요한 것을 찾아가며 학습하는 것의 차이를 경험하셨기를 바랍니다.
회고에서 작성해준 "vNode 객체에 실제 DOM 요소를 저장했던 실수"는 정말 좋은 경험이네요. 구현파트에서는 일단 돌아가는 코드가 중요하지만 깊이가 필요한 코드에서는 성능이가 원칙, 설계등이 중요한데 앞으로의 소중한 인사이트가 되어 줄거라 기대합니다. 가상 DOM의 핵심이 실제 DOM과의 분리인데, 이를 직접 겪으며 이해하게 된 것 같아요. 또한 WeakMap을 활용한 메모리 관리와 이벤트 위임 구현을 잘 이해하고 작성해주셨네요. 잘했습니다 :)
이렇게 프레임워크를 직접 구현해보면 단순한 이론이 아닌 구조와 흐름을 이해한 채로 개념을 체득하게 됩니다. 앞으로도 이런 방식으로 사용하는 도구의 원리를 파헤쳐보는 경험을 계속해보시기 바랍니다.
Q) 이벤트를 저장하는 저장소 계층 => 잘했습니다. element > eventType > handler 계층 구조로 선택하신 것은 매우 적절합니다. element가 제거되면 모든 이벤트가 사라지므로 element가 최상회 요소가 맞죠. 그리고 eventType에서 handler의 유무에 따라서 위임이 결정되므로 2번째에 위치하는게 자연스럽습니다. React 등 대부분의 프레임워크도 이 방식을 채택하고 있답니다.
Q) 런타임 에러 처리 => 우리가 잘 알고 있는 React의 ErrorBoundary라는 기능이 바로 에러를 처리해주는 기능입니다. 렌더링 에러를 try ~ catch로 감지해서 상위에서 catch하여 fallback UI 표시하는 기능이죠.
=> 지금 그리고 VDOM을 생성하는 로직에 try-catch를 만들고 에러감지시 상위에서 지정된 UI를 표기하도록 만들면 됩니다.
차근 차근 React를 다 만들지는 않더라도 React가 역사적으로 추가한 개념들을 하나씩 만들어가면 왜 이런게 필요했는지 어떻게 구현되어 있을지 선명하게 이해되는 경험을 하시게 될거에요!.
수고많았습니다. 3주차도 화이팅입니다 :)