omg 면접 단골 질문이자나~~
처음 이 주제를 받고는.. 제가요..? 가상돔을요..?
이런 느낌이었어요
뭐랄까 리액트라는 대단한 라이브러리의 중요한 축을 담당하는 역할이라고 생각했는데,
그걸 일주일 만에 둑닥 만들 수 있을 거라고 생각이 되지 않았거든요
고작 2주차긴 하지만요
항해를 하면서 크게 느끼고 있는 것 중 하나는
내가 쓰고 있는 라이브러리나 프레임워크가 대단한 흑마법으로 만들어진 것이 아니라
내가 쓰는 언어로 내가 하는 고민들을 하나씩 해결해나가며 완성된 결과물이구나 라는 거예요
그리고 테오의 과제 피드백 중에서 저런 말이 있었는데요.
가상 돔이랑 무엇인가를 읽고 아는 것과, 가상 돔을 구현하기 위해 무엇이 필요한가를 고민하며 공부하는 것은 완전히 다른 깊이의 학습 방법이 될 수 있다는 것을 느끼는 시간이 되어주었기를 바랍니다.
테오의 바람대로 뼈저리게 느낀 일주일이었어요.
면접 준비하면서 리액트의 가상 돔이 뭔지 여러 번 공부했었는데, 이렇게깊이 이해해본 적은 처음이었거든요.
앞으로 면접 준비나 다른 공부를 할 때에도 생각날 중요한 레슨런이 되었습니다!
첫 번째, 코드를 선언형으로 작성했어요.
isNil
, isBoolean
, isArray
등의 헬퍼 함수를 통해 선언형으로 코드를 작성하여 가독성을 높였어요.
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),
};
}
두 번째, 단일 책임의 원칙을 적용했어요.
updateAttributes
는 2가지 과정을 거쳐요.
새로운 속성을 추가하거나 변경하는 단계와 기존 속성을 제거하는 단계인데요.
이 두 역할을 각기 다른 함수로 분리했답니다!
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);
}
};
정해진 틀 안에서만 공부했어요.
이번 과제는 지난 주와 다르게 어느 정도 틀이 정해진 과제였는데요.
좀 더 extra하게 뭘 해보려는 생각 없이 그저 주어진 과제를 해결하기에 급급했던 ..
이게 어디냐~ 싶으면서도요 좀 더 해볼 수 있는 게 있지 않았을까 하는 아쉬움이 남아요.
하루 정도의 시간을 확보해보기
금요일에 과제 다 하고 PR 쓰느라고 새벽 6시에 취침하는 불상사가 있었어요.
그러다 보니 조금이라도 자고 싶은 마음에 PR도 대충 쓰게 되고...
더 extra하게 생각할 시간도 없었구요..ㅠㅠ
그래서 이번 주는 의식적으로라도 나는 목요일까지 끝낸다!는 마음으로 과제를 해보려고 합니다.
• JSX가 호출될 때 실제 가상 노드를 만드는 함수
• 자식들을 평탄화(flat)하고 null/false 제거
• <div>{condition && <span />}</div>
같이 falsy 값 필터링 처리
export function createVNode(type, props, ...children) {
const newChildren = children.flat(Infinity).filter((child) => !isNil(child) && !isFalse(child));
return { type, props, children: newChildren };
}
• 재귀적으로 children을 정리
• 문자열/숫자/배열/컴포넌트 타입에 따라 각각 다른 처리
• 사용자 정의 컴포넌트를 재귀적으로 평가 (vNode.type(props))
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),
};
}
• 가상 노드를 기반으로 실제 DOM 요소를 생성
• 자식 요소도 재귀적으로 DOM 생성
• props 및 이벤트 등록까지 이 단계에서 함께 처리
export function createElement(vNode) {
if (isNil(vNode) || isBoolean(vNode)) return document.createTextNode("");
if (isString(vNode) || isNumber(vNode)) return document.createTextNode(vNode.toString());
if (isArray(vNode)) {
const $el = document.createDocumentFragment();
vNode.forEach((child) => $el.appendChild(createElement(child)));
return $el;
}
const $el = document.createElement(vNode.type);
updateAttributes($el, vNode.props ?? {});
(vNode.children ?? []).forEach((child) => $el.appendChild(createElement(child)));
return $el;
}
• 첫 렌더링일 경우: createElement로 DOM 생성
• 이후 업데이트일 경우: updateElement로 기존 DOM 비교 갱신
• 이벤트 위임 설정은 최초 1회만
export function renderElement(vNode, container) {
const normalized = normalizeVNode(vNode);
if (container.children.length === 0) {
const $el = createElement(normalized);
container.appendChild($el);
setupEventListeners(container);
} else {
updateElement(container, normalized, prevVNode);
}
prevVNode = normalized;
}
• 새로운 VNode와 이전 VNode를 비교(diff)
• 타입이 다르면 replaceChild, 같으면 patch
• updateAttributes와 updateChildren으로 세분화 처리
export function updateElement(parentElement, newNode, oldNode, index = 0) {
...
updateAttributes(oldElement, newNode.props, oldNode.props);
updateChildren(oldElement, newNode.children, oldNode.children);
}
• 새로 설정해야 할 속성과 제거할 속성을 분리해서 처리
• setAttributes: 새로 넣거나 갱신할 것들
• removeAttributes: 빠진 속성/이벤트를 제거
const updateAttributes = (target, newProps, oldProps) => {
setAttributes(target, newProps, oldProps);
removeAttributes(target, newProps, oldProps);
};
• 최소 길이까지만 updateElement 수행
• 길이가 다른 경우 → 초과된 노드 추가 or 제거
• minLength를 활용해 비교 효율 증가
const updateChildren = (target, newChildren, oldChildren) => {
const min = Math.min(newChildren.length, oldChildren.length);
for (let i = 0; i < min; i++) {
updateElement(target, newChildren[i], oldChildren[i], i);
}
if (newChildren.length > oldChildren.length) {
for (let i = oldChildren.length; i < newChildren.length; i++) {
target.appendChild(createElement(newChildren[i]));
}
}
if (oldChildren.length > newChildren.length) {
for (let i = oldChildren.length - 1; i >= newChildren.length; i--) {
target.removeChild(target.childNodes[i]);
}
}
};
정리 아주 잘해놓으셧네요~
수민님 화이팅입니다!