과제 체크포인트
배포 링크
https://yunwoo-yu.github.io/front_6th_chapter1-2/
기본과제
가상돔을 기반으로 렌더링하기
- createVNode 함수를 이용하여 vNode를 만든다.
- normalizeVNode 함수를 이용하여 vNode를 정규화한다.
- createElement 함수를 이용하여 vNode를 실제 DOM으로 만든다.
- 결과적으로, JSX를 실제 DOM으로 변환할 수 있도록 만들었다.
이벤트 위임
- 노드를 생성할 때 이벤트를 직접 등록하는게 아니라 이벤트 위임 방식으로 등록해야 한다
- 동적으로 추가된 요소에도 이벤트가 정상적으로 작동해야 한다
- 이벤트 핸들러가 제거되면 더 이상 호출되지 않아야 한다
심화 과제
Diff 알고리즘 구현
- 초기 렌더링이 올바르게 수행되어야 한다
- diff 알고리즘을 통해 변경된 부분만 업데이트해야 한다
- 새로운 요소를 추가하고 불필요한 요소를 제거해야 한다
- 요소의 속성만 변경되었을 때 요소를 재사용해야 한다
- 요소의 타입이 변경되었을 때 새로운 요소를 생성해야 한다
과제 셀프회고
이번엔 Jsx가 추가 된 환경에서 SPA에서 Virtual DOM을 다루는 코어 코드를 학습했습니다. 테스트 코드가 존재해 순서대로 진행할 수 있었지만 만약 테스트 코드가 없는 환경에서 vNode를 생성하고 정규화한 뒤 Element를 생성하는 과정을 생각해낼 수 있었을까? 라고 한다면 시작하기가 굉장히 어렵지 않았을까 생각이 드는 과제였습니다.
AI는 최소한으로 사용하고자 했고 이해하지 못한 코드는 사용하지 않으려 노력했습니다! 개인적으로는 diff, event쪽 구현을 했지만 아직 꽤 어려운 부분인 것 같습니다.😅
전체적인 구조를 이해하려고 Jsx 플러그인 문서를 보며 커스텀 함수로 트랜스파일링되어 실행이 되는 부분부터 차근차근 되짚어보며 복기했고 개인적으로 한번 더 살펴보며 리팩토링을 진행해봤지만 반복 코드 한곳밖에 개선한 게 없는 것 같아 아쉬움이 조금 남는 것 같습니다.
이제 또 블로그에 기록하며 한번 더 살펴봐서 이해도를 높일 생각입니다. 유익하고 재밌는 과제 감사합니다. 🙇♂️
기술적 성장
WeakMap 활용
// WeakMap의 Key는 객체이며 객체를 참조하는 곳이 없다면 객체는 메모리에서 제거된다.
let john = { name: "John" };
let weakMap = new WeakMap();
weakMap.set(john, "...");
john = null; // 참조를 덮어씀
// john을 나타내는 객체는 이제 메모리에서 제거
Virtual DOM + JSX 구조의 전체적인 실행 플로우
function main() {
initRender();
router.start();
}
main 함수 호출로 initRender 실행
/**
* 렌더링 초기화 - Store 변화 감지 설정
*/
export function initRender() {
// 각 Store의 변화를 감지하여 자동 렌더링
productStore.subscribe(render);
cartStore.subscribe(render);
uiStore.subscribe(render);
router.subscribe(render);
}
- 각 Store를 구독, 렌더함수 전달
- 이후 router.start() 실행하여 현재 url에 해당하는 router를 찾아 알림 전송 (notify 함수 실행)
/** @jsx createVNode */
import { createVNode } from "../lib";
const HomePage = withLifecycle(
return (/* JSX 문법...*/)
)
export const render = withBatch(() => {
const rootElement = document.getElementById("root");
if (!rootElement) return;
const PageComponent = router.target;
// App 컴포넌트 렌더링
renderElement(<PageComponent />, rootElement);
});
// /** @jsx createVNode */ JSX 커스텀 함수 이용으로 인해 컴포넌트는 트랜스파일링 된다.
renderElement(<PageComponent />, rootElement); -> renderElement(createVNode(<PageComponent />), rootElement);
- notify 함수 실행으로 첫 PageComponent인 HomePage render 실행
- 컴포넌트는
createVNode(<HomePage />)형태로 변환되어 renderElement 함수의 vNode로 전달된다. - JSX 플러그인 문서
export function renderElement(vNode, container) {
const currentNodeTree = currentNodeMap.get(container);
const progressWorkInNodeTree = normalizeVNode(vNode);
if (!currentNodeTree) {
container.appendChild(createElement(progressWorkInNodeTree));
} else {
updateElement(container, progressWorkInNodeTree, currentNodeTree);
}
currentNodeMap.set(container, progressWorkInNodeTree);
setupEventListeners(container);
}
- 이후 전달된 vNode를 normalize 하고 기존 Virtual DOM Tree가 있다면 updateElement를 (diff)
- 없다면 container에 appendChild를 통해 Tree대로 Element를 생성하여 붙여준다.
- 이후 새로 EventListeners를 Setup한다.
- 만약 기존 트리가 존재한다면 updateElement를 실행하여 기존 트리와 비교하며 변경 된 부분만 업데이트를 진행한다.
코드 품질
특히 만족스러운 구현
createElement, updateElement 에서 반복되지만 따로 만들어져있던 AttributeUpdate 함수를 하나로 합쳐 재사용해봤습니다.
export function setAttribute(target, key, value) {
if (key.startsWith("on") && typeof value === "function") {
const eventType = key.slice(2).toLowerCase();
addEvent(target, eventType, value);
} else if (key === "className") {
target.setAttribute("class", value);
} else if (key === "style") {
Object.assign(target.style, value);
} else if (value === true) {
target[key] = true;
} else if (value === false) {
target[key] = false;
} else {
target.setAttribute(key, value);
}
}
export function createElement(vNode) {
if (vNode.props) {
Object.entries(vNode.props).forEach(([key, value]) => {
setAttribute($element, key, value);
});
}
// ...etc
}
function updateAttributes(target, newProps, oldProps) {
// newProps, oldProps가 없을경우 빈 객체로 초기화
const safeNewProps = newProps || {};
const safeOldProps = oldProps || {};
// 기존 속성 중 새 속성에 없는 것들 제거
Object.keys(safeOldProps).forEach((key) => {
if (!(key in safeNewProps)) {
removeAttribute(target, key, safeOldProps[key]);
}
});
// 새 속성 추가/업데이트
Object.keys(safeNewProps).forEach((key) => {
if (key.startsWith("on")) {
const eventType = key.slice(2).toLowerCase();
if (safeOldProps[key]) {
removeEvent(target, eventType, safeOldProps[key]);
}
if (safeNewProps[key]) {
addEvent(target, eventType, safeNewProps[key]);
}
} else {
setAttribute(target, key, safeNewProps[key]);
}
});
}
export function updateElement(parentElement, newNode, oldNode, index = 0) {
updateAttributes(currentElement, newNode.props, oldNode.props);
// ...etc
}
리팩토링이 필요한 부분
- 가독성이 좋지않은 조건문들을 한눈에 들어오게 만들기
- updateElement 시 조건별로 돌아가는 for문 개선
학습 효과 분석
- 가장 큰 배움이 있었던 부분
- Virtual DOM을 이용한 SPA의 전체적인 흐름과 구현방식
- 추가 학습이 필요한 영역
- 더 많은 문제들을 해결한 React의 Fiber 아키텍처
과제 피드백
- 과제에서 좋았던 부분
- 테스트를 보며 하나씩 코어 함수들을 만들어가며 어떤 부분이 부족한지 빠졌는지 적절한 고민과 확인이 가능해서 너무 좋았습니다!
리뷰 받고 싶은 내용
- 현재 updateElement, createElement 모두 재귀를 이용해 구현했습니다! 선택해볼법한 더 좋은 알고리즘이 있을까요?
- 조건문이 많아져서 가독성이 많이 떨어지게 되었는데 if, else if 를 줄이거나 혹은 다른 선택지가 있을까요? 그리고 if(조건) { ... 내용 return } 이런 여러개의 if return 형태와 if else if 중 뭐가 더 나은 코드라고 생각하시나요?
- eventManager 쪽에서 "이벤트 위임" 은 작성했지만 while 루프를 돌며 부모 엘리먼트를 재할당하며 이벤트가 있는 요소까지 찾아가는 이벤트 버블링 형태로 처리를 했습니다. 정상적으로 동작은 하지만 이게 맞는건지 잘 모르겠습니다.. 매번 이벤트마다 while 루프가 실행된다고 생각하면 성능이 좋지 않은 것 같다는 생각이 듭니다!
- updateElement에서 children을 처리할 때 조건별로 돌아가는 for문이 3개 있습니다. 아무래도 3개의 for문이 거부감이 좀 드는데 적절한 코드가 맞을까요?
- 이벤트가 제거됐는지 확인하는 방법이 궁금합니다! 테스트는 통과했는데 현재 코드레벨에서 확인해볼 방법이 있을까요? (메모리가 누수되는지 체크)
과제 피드백
안녕하세요 윤우님! 2주차 과제 잘 진행해주셨네요 ㅎㅎ 너무 고생하셨습니다. 바벨 플러그인 문서도 직접 확인해보셨군요!!
현재 updateElement, createElement 모두 재귀를 이용해 구현했습니다! 선택해볼법한 더 좋은 알고리즘이 있을까요?
저도 재귀 말고는 떠오르는 방법이.. 딱히 없네요 ㅋㅋ; 다만 생각해볼 수 있는건, 변경에 대한 커맨드? 라고 해야하나, 그런걸 수집하는 방법이 있을 것 같아요.
insert, update, delete가 있고 이 명령어가 정확히 어떤 구간의 dom을 변화시키는지 정보를 수집할 수 있따면 재귀 방식이 아니라 특정 노드의 일부분만 수정하는 방식으로 코드를 작성할 수도 있을 것 같네요!
그럴라면 component의 정보와 component를 변경하는 코드를 연결시켜야 하는데 그게 사실 굉장히 어렵답니다... ㅎㅎ
조건문이 많아져서 가독성이 많이 떨어지게 되었는데 if, else if 를 줄이거나 혹은 다른 선택지가 있을까요?
새로운 함수로 만들어서 분리하는 방법이 있어요!
eventManager 쪽에서 "이벤트 위임" 은 작성했지만 while 루프를 돌며 부모 엘리먼트를 재할당하며 이벤트가 있는 요소까지 찾아가는 이벤트 버블링 형태로 처리를 했습니다. 정상적으로 동작은 하지만 이게 맞는건지 잘 모르겠습니다.. 매번 이벤트마다 while 루프가 실행된다고 생각하면 성능이 좋지 않은 것 같다는 생각이 듭니다!
다른 방법을 생각해보자면... 이벤트를 등록할 때, 이벤트 타겟의 부모들을 미리 만들어서 관리할 수 있지 않을까 싶어요 ㅎㅎ
const createParentList = e => { let parent = e.parentNode; const parentList = new Set() while(!parent){ parentList.add(parent); parent = parent.parentNode; } return parentList }
const parentMap = new WeakMap(); const addEvent = (el, type, callback) => { if (parentMap.has(el)) { parentMap.set(el, createParentList(el)) }
/// 다른 로직 실행 }
이렇게 렌더링 시점에 미리 만드는 방법도 있고 아니면 최초에 한 번 이벤트가 트리거될 때 부모 목록을 만들어서 재활용하는 방법도 있을 것 같아요 ㅎㅎ
updateElement에서 children을 처리할 때 조건별로 돌아가는 for문이 3개 있습니다. 아무래도 3개의 for문이 거부감이 좀 드는데 적절한 코드가 맞을까요?
일단 문제를 해결하기 위해선 어쩔 수 없겠죠..? 논리적으로 생각해보면, 이걸 for 하나로 만든다고 해도 그 내부에서 분기를 3번 해야 할 수 있어요. 결국 실행해야 하는 코드의 총량은 비슷하죠 ㅎㅎ
진짜로 변경된 attribute에 대한 정보를 렌더링시점에 전달하는 방법이 있다면 좋을텐데... 그런 시스템을 만드는게 더 복잡하고 어렵답니다 ㅎㅎ 맨 위의 답변과 겹쳐요
이벤트가 제거됐는지 확인하는 방법이 궁금합니다! 테스트는 통과했는데 현재 코드레벨에서 확인해볼 방법이 있을까요? (메모리가 누수되는지 체크)
글쎄요.. 코드레벨에서 확인할 수 있는 방법이 있을지.. 코드레벨에서 확인한다는건 결국 값을 조회한다는건데, 값을 조회하려고 시도한다는건 다시 메모리상에 무언가가 있는지 확인하는 과정이라서요.
덤프 떠서 확인해야 하지 않을까 싶어요..!