과제 체크포인트
배포 링크
https://nimusmix.github.io/front_6th_chapter1-2/
기본과제
가상돔을 기반으로 렌더링하기
- createVNode 함수를 이용하여 vNode를 만든다.
- normalizeVNode 함수를 이용하여 vNode를 정규화한다.
- createElement 함수를 이용하여 vNode를 실제 DOM으로 만든다.
- 결과적으로, JSX를 실제 DOM으로 변환할 수 있도록 만들었다.
이벤트 위임
- 노드를 생성할 때 이벤트를 직접 등록하는게 아니라 이벤트 위임 방식으로 등록해야 한다
- 동적으로 추가된 요소에도 이벤트가 정상적으로 작동해야 한다
- 이벤트 핸들러가 제거되면 더 이상 호출되지 않아야 한다
심화 과제
Diff 알고리즘 구현
- 초기 렌더링이 올바르게 수행되어야 한다
- diff 알고리즘을 통해 변경된 부분만 업데이트해야 한다
- 새로운 요소를 추가하고 불필요한 요소를 제거해야 한다
- 요소의 속성만 변경되었을 때 요소를 재사용해야 한다
- 요소의 타입이 변경되었을 때 새로운 요소를 생성해야 한다
과제 셀프회고
면접 단골 질문~~ 가상 돔~~ 직접 만들어볼 기회가 생길 줄 몰랐어요! 사실 지난 주 과제에 지쳐서 이번 주에는 컴퓨터 앞에 앉기가 두려웠는데요(..) 막상 과제를 시작하니 가상 돔이 어떤 과정을 통해 동작하는지, 그 안에서 함수들은 어떤 역할을 하고 있는지 알아가는 게 정말 재밌었습니다!
기술적 성장
Boolean attribute
Boolean attribute는 setAttribute로 할당하지 않고 DOM 프로퍼티에 직접 할당해야 한다는 사실을 알게 되었습니다.
el.checked = true/false, el.selected = true/false 식으로 처리해야 실제 요소의 상태가 즉시 반영되고,
removeAttribute로 완전히 제거할 수 있어 렌더링과 자바스크립트 로직이 완전히 일치한다는 것을 알게 되었습니다.
(질문)
언젠가 실무에서 <div readOnly></div> 이런 식으로 코드를 짠 적이 있어요.
그 때 사수가 readOnly={true} 이렇게 항상 명시해주는 게 좋다고 말씀해주셨어요.
어.. 그런데 오늘 보니까 어떤 의도로 그렇게 말씀하신 건지 잘 이해가 안 갑니다..!!!
WeakMap eventManager에서 event를 저장하기 위한 자료구조로 WeakMap을 사용했습니다. 객체가 메모리에서 없어지면 자동으로 가비지 컬렉션이 된다는 점이 흥미로웠어요. 특히 dom에서 사라지면 unmount 되기 때문에 딱 맞는 자료구조라고 생각했습니다. 적절한 자료구조를 쓰는 것만으로 손으로 쓰는 코드를 얼마나 줄일 수 있던지! 자료구조의 중요성에 대해 알 수 있었던 시간이었습니다. ><
가상 돔 동작 과정
createVNode, normalizeVNode 등 각 함수들이 어떤 역할을 하는지 알 수 있었습니다.
그 과정에서 몇 가지 의문이 생기기도 했는데요. 그 질문들은 아래와 같았습니다!
1 . createVNode에서 왜 자식을 평탄화(flatten)해야 하지? 어떤 경우에 중첩되는 거지? -> 아래와 같은 경우가 흔하게 발생한다!
<div>
{selecetedItems.map((item) => <div>{item}</div>}
{unselectedItems.map((item) => <div>{item}</div>}
</div>
이 경우 평탄화하지 않는다면 vNode.children에 중첩 배열이 생기고, 이후에 재귀적으로 순회하거나 비교하기 어려워진다. 그래서 children.flat(Infinity)로 평탄화해서 한 번에 처리 가능한 배열로 만들어줘야 한다.
- createVNode를 거쳤다면 전부 객체일 텐데, normalizeVNode에서 왜 객체가 아닌 타입에 대한 처리를 해줘야 하지? -> createVNode를 거치지 않고 직접 값이 들어오는 경우가 있다.
<div>
Hello
<span>World</span>
</div>
위 코드는 결국 createVNode('div', null, 'Hello', createVNode('span', null, 'World'))으로 파싱되기 때문에,
children 배열에 'Hello' 같은 순수 문자열이 들어올 수 있다.
또한, 사용자 정의 컴포넌트가 반환하는 값이 string, number, null일 수도 있다.
function MyComponent() {
return "Hello world";
}
코드 품질
- 조건 분기 시 typeof 나 복잡한 null 체크 대신, isNil, isBoolean, isArray 등의 의미가 명확한 헬퍼 함수를 사용했습니다.
- 덕분에 normalizeVNode 내부의 로직이 한눈에 읽히는 선언형 스타일로 바뀌어 가독성이 크게 향상되었습니다.
export function normalizeVNode(vNode) {
if (isNil(vNode) || isBoolean(vNode)) {
return "";
}
if (isString(vNode) || isNumber(vNode)) {
return vNode.toString();
}
if (isArray(vNode)) {
return vNode.map(normalizeVNode).filter((v) => !isEmptyString(v));
}
if (isFunction(vNode.type)) {
const props = { ...(vNode.props ?? {}), children: normalizeChildren(vNode.children) };
return normalizeVNode(vNode.type(props));
}
return {
...vNode,
children: normalizeChildren(vNode.children),
};
}
- 단일 책임 원칙을 적용해 각 함수가 하나의 일만 하도록 분리했습니다.
- setAttributes: 필요한 속성을 추가하거나 수정
- removeAttributes: 더 이상 필요 없는 속성을 제거
- 이로 인해 코드의 가독성이 높아졌고, 이후 유지보수나 리팩토링이 쉬워졌습니다.
const updateAttributes = (target, originNewProps, originOldProps) => {
const newProps = originNewProps || {};
const oldProps = originOldProps || {};
setAttributes(target, newProps, oldProps);
removeAttributes(target, newProps, oldProps);
};
const setAttributes = (target, newProps, oldProps) => {
for (const [key, newValue] of Object.entries(newProps)) {
const oldValue = oldProps[key];
if (newValue === oldValue) continue;
...중략...
};
const removeAttributes = (target, newProps, oldProps) => {
for (const key in oldProps) {
if (key in newProps) continue;
...중략...
target.removeAttribute(key);
}
};
- updateChildren 최적화
- minLength를 기준으로 newChildren과 oldChildren의 공통 구간에서만 children을 update하고,
- 이후 남은 부분은 삽입/제거로 분리했습니다.
- 불필요한 연산을 줄이고, 가독성을 높일 수 있었습니다.
학습 효과 분석
이렇게 바닐라 자바스크립트로 가상 돔을 만들어 보니까, 과정을 이해하면 다 논리적으로 이해가 되는구나~ 깨닫게 됐어요 면접 질문에 대한 답을 달달 외우기 보다 이렇게 공부하면 좋겠다!는 생각이 들었습니다.
과제 피드백
이번 과제는 구조적으로 틀이 짜져 있어서 너무나 마음이 편안했습니다..!! 과제 제출이 4시간 남은 시점에 PR을 쓰면서 이런 말 웃기긴 하지만(ㅋㅋ) 선택 과제로 좀 더 deep한 주제가 주어졌어도 좋을 것 같아요.
회사 사람한테 자바스크립트로 가상 돔 만든다고 하니까 Reconciliation 구현하는 게 어려울 거다, 제약 조건(예를 들면 react의 key) 같은 것을 두는 게 편할 거다 라고 이야기해서 좀 긴장했는데 그 부분을 경험하지 못해서 좋으면서 아쉬우면서 다행이면서 궁금합니다! (물론 난이도가 엄청 높다고 들었어요! 나왔으면 울었을지도 흑흑)
리뷰 받고 싶은 내용
- (질문) eventManager에서 각 eventType에 대해 handler를 여러 개 걸 수 있어야 할까요? 바닐라 자바스크립트로 가능한 것은 알고 있습니다만, 리액트의 경우에는 한 이벤트에 여러 개의 handler가 전달되지는 않으니.. 이번 과제에서도 하나만 받게 했다가 remove 시 handler를 쓸 데가 없어서 (..) 수정했었습니다!
export function addEvent(element, eventType, handler) {
if (!eventStore.has(element)) {
eventStore.set(element, new Map());
}
const eventMap = eventStore.get(element);
if (!eventMap.has(eventType)) {
eventMap.set(eventType, new Set());
}
eventMap.get(eventType).add(handler);
eventTypes.add(eventType);
}
- eventManager에서 eventTypes를 전역으로 저장해두고 add해서 쓰고 있는데, 다른 친구들 코드를 보니 미리 constant로 선언해두고 쓰는 경우가 많더라구요. constant로 선언해두고 써도 Array의 크기가 크지 않아 메모리, 연산 측면에서도 유의미한 차이는 나지 않을 거라고 생각하는데, 이런 경우에는 그냥 아무거나 (!!) 쓰면 될까요?
const eventTypes = new Set();
...중략...
eventTypes.add(eventType);
과제 피드백
안녕하세요 수민공주(?) ㅋ 수고했습니다. 이번 과제는 React의 핵심 원리인 가상 DOM과 diff 알고리즘을 직접 구현해보면서, 프레임워크가 어떻게 효율적인 렌더링을 수행하는지 이해하는 것이 목표였습니다. 특히 "가상 DOM이란 무엇인가"를 읽고 아는 것과, "가상 DOM을 구현하기 위해 무엇이 필요한가"를 고민하며 공부하는 것은 완전히 다른 깊이의 학습방법이 될 수 있다는 것을 느끼는 시간이 되어주었기를 바랍니다.
회고에서 createVNode의 children 평탄화나 normalizeVNode의 역할 등 구현 과정에서 생긴 의문들을 하나하나 해결해가며 학습하신 과정이 인상적입니다. 특히 WeakMap을 활용한 이벤트 관리와 선언적 스타일의 코드 작성은 메모리 효율성과 가독성을 모두 고려한 좋은 선택이었습니다. 잘하셨습니다.
이렇게 직접 저수준 라이브러리를 구현해보면 단순한 이론이 아닌 실제 동작 원리를 체득하게 됩니다. 앞으로도 필요에 의한 목적 지향적 학습을 계속 이어가시길 바랍니다.
숙제로 아쉬워했던 "...Reconciliation 구현하는 게 어려울 거다, 제약 조건(예를 들면 react의 key) 같은 것을 두는 게 편할 거다..." 를 못해본거 꼭 하기입니다!! ㅋ
그리고 여기에서 최소 useState, useEffect 정도까지만, 조금 더 욕심내면 memo와 context, suspense 정도들만 맛보기로 구현을 해보면 그전에 면접에서 말로 외우던 내용이 아니라 정말로 자신의 언어와 경험을 표현할 수 있을거에요.
수고하셨습니다. 다음 주차 과제도 화이팅입니다!
Q) eventManager에서 각 eventType에 대해 handler를 여러 개 걸 수 있어야 할까요? => 프레임워크 수준에서는 여러 핸들러를 지원하는 것이 확장성 측면에서 좋습니다. 라이브러리 내부적으로 성능 측정, 로깅, 이벤트 합성 등 사용자가 모르는 여러 핸들러를 붙일 수 있어야 하거든요. React도 내부적으로는 여러 처리를 하면서 사용자에게는 하나의 핸들러만 노출합니다. 당장 필요 없더라도 확장 가능한 구조로 만드는 것이 좋은 선택입니다.
Q) eventTypes를 Set으로 관리 vs constant 배열 => 동적으로 이벤트 타입이 추가될 가능성이 있다면 Set이 유리하고, 고정된 이벤트만 다룬다면 constant가 나을 수 있습니다. 유지보수와 확장성을 고려해 Set을 사용하는 것이 좋겠죠? 나중에 새로운 이벤트 타입이 추가되어도 코드 수정 없이 자동으로 처리되니까요.
어떻게 보면 현재 React가 어떻게 하고 있나가 사실상 정답지에 가까워서 이러한 의문에 대한 대답의 힌트는 다른데서는 어떻게 하고 있나? 이런거 찾아보시면 좋아요.
수고하셨습니다. 다음 주차도 화이팅입니다! :)