https://pomb.us/build-your-own-react/ 을 기반으로 학습 내용을 정리한 글입니다.
STEP 6 - Reconciliation
Reconciliation이란 재조정이라는 의미이다.
리액트 공식문서에도 해당 내용이 있으니 참고해도 좋을 것 같다.
이전 step까지는 DOM에만 항목을 추가했다.
이번 step부터는 노드를 업데이트하거나 삭제하는 방법에 대해 학습할 것이다.
답을 먼저 말하자면 렌더링 함수에서 받은 요소를 DOM에 커밋한 마지막 fiber tree와 비교해야 한다.
비교를 위해 커밋를 마친 후에 DOM에 커밋한 마지막 fiber tree에 대한 참조를 currentRoot라는 변수로 저장했다.
즉, currentRoot는 이전에 렌더링된, 그리고 DOM에 커밋된 fiber tree의 root다. 현재 화면에 표시되고 있는 UI 상태를 나타낸다.
function commitRoot() {
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let currentRoot = null
let wipRoot = null
위 코드에서 눈여겨봐야 할 것은 모든 fiber에 alternate 프로퍼티를 추가한 부분이다.
alternate는 currentRoot와 wipRoot 사이의 링크다.
alternate는 교체하다라는 의미인데, 교체되기 전 상태를 저장해 비교를 가능하게 해주는 역할이라고 생각하면 된다.
이 링크는 현재 작업중인 fiber tree(wipRoot)와 이전에 커밋된 fiber tree(currentRoot) 사이의 관계를 나타낸다.
alternate 프로퍼티를 사용하면, 이전과 현재의 상태를 비교할 수 있다.
이 비교를 통해 어떤 부분이 변경되었는지를 파악하고, 효율적으로 업데이트를 적용할 수 있다.
이 과정을 reconciliation이라고 부른다.
이제 새로운 fiber를 생성하는 performUnitOfWork 함수에서 create new fibers에 해당했던 영역의 부분을 reconcileChildren() 함수로 분리해 작업해보겠다.
function performUnitOfWork(fiber) {
// 1. add dom node
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
// 2. create new fibers
const elements = fiber.props.children;
reconcileChildren(fiber, elements);
// 3. return next unit of work
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
function reconcileChildren(wipFiber, elements) {
let index = 0;
let olderFiber = wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null;
while (index < elements.length || oldFiber != null) {
const element = elements[index];
let newFiber = null
if (oldFiber) {
oldFiber = oldFiber.sibling
}
if (index === 0) {
wipFiber.child = newFiber;
} else if (element) {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
해당 함수에서 reconciliation 과정을 살펴보면,
각 element(새로운 자식 요소)에 대해 새로운 fiber를 생성한다.
첫 번째 요소는 부모의 자식으로 연결되고, 그 다음 요소들은 이전 요소의 형제로 연결된다.
이제 DOM에 적용할 사항이 있는지 확인하기 위해서는 비교가 필요하다.
새로운 element(새롭게 렌더링하려는 요소)는 배열 형태로, oldFiber(렌더링 된 요소)는 Linked List 구조로 이뤄져있다.
React는 새로운 elements 배열과 기존 oldFiber(wipFiber.alternate)리스트를 동시에 반복하면서 현재 렌더링하려는 UI와 이전에 렌더링된 UI를 비교한다.
이를 통해 React는 이전 상태와 새로운 상태간의 차이점을 파악하고, 그 차이점만을 반영해 DOM을 업데이트 할 수 있다.
비교 방법은 아래와 같다.
1. 이전 Fiber와 새 요소의 type이 동일한 경우 DOM 노드를 유지하고 새 prop으로 업데이트
2. type이 다르고 새 요소가 있으면 새 DOM 노드 생성
3. type이 다르고 오래된 Fiber가 있는 경우 기존 노드를 제거
function reconcileChildren(wipFiber, elements) {
let index = 0;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
while (index < elements.length || oldFiber != null) {
const element = elements[index];
let newFiber = null;
const sameType = oldFiber && element && element.type == oldFiber.type;
if (sameType) {
// TODO update the node
}
if (element && !sameType) {
// TODO add this node
}
if (oldFiber && !sameType) {
// TODO delete the oldFiber's node
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
if (index === 0) {
wipFiber.child = newFiber;
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
React는 더 나은 조정을 위해 key를 사용한다.
예를 들면 element array에서 자식이 위치를 변경하는 경우 key로 감지한다.
fiber에 effectTag라는 새로운 속성을 추가한다. 나중에 commit 단계에서 이 속성을 활용한다.
이제 비교 방법별 코드를 보자.
1. 이전 Fiber와 새 요소의 type이 동일한 경우 DOM 노드를 유지하고 새 prop으로 업데이트 처리
if (sameType) {
// update the node
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
};
}
2. type이 다르고 새 요소가 있으면 새 DOM 노드 생성
if (element && !sameType) {
// add this node
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
};
}
3. type이 다르고 오래된 Fiber가 있는 경우 기존 노드를 제거
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
삭제하려는 노드를 추가하려면 deletions이라는 배열이 필요하다.
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
};
deletions = [];
nextUntilOfWork = wipRoot;
}
let nextUntilOfWork = null;
let wipRoot = null;
let currentRoot = null;
let deletions = null;
그 다음 DOM에 변경 사항을 적용할 때 해당 배열의 fiber도 사용한다.
function commitRoot() {
deletions.forEach(commitWork);
// add nodes to dom
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}
이제 새로운 effectTags를 처리하도록 commitWork 함수를 변경해보자.
effectTag가 PLACEMENT인 경우 -> 이전과 동일한 작업을 수행하고 상위 Fiber의 노드에 DOM 노드를 추가
function commitWork(fiber) {
if (!fiber) {
return;
}
const domParent = fiber.parent.dom;
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
effectTag가 DELECTION인 경우-> 반대로 자식을 제거한다.
else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom);
}
effectTag가 UPDATE인 경우 -> 변경된 props로 기존 DOM 노드를 업데이트 한다.
const isProperty = (key) => key !== "children";
const isNew = (prev, next) => (key) => prev[key] !== next[key];
const isGone = (prev, next) => (key) => !(key in next);
function updateDom(dom, prevProps, nextProps) {
// Remove old properties
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach((name) => {
dom[name] = "";
});
// Set new or changed properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
dom[name] = nextProps[name];
});
}
...
else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
}
업데이트 해야하는 특별한 종류의 prop 중 하나는 이벤트 리스너다. prop 이름이 on 접두사로 시작하는 경우 이를 다르게 처리한다.
const isEvent = (key) => key.startsWith("on");
const isProperty = (key) => key !== "children" && !isEvent(key);
이벤트 핸들러가 변경되면 노드에서 이를 제거한다. 그리고 새로운 이벤트 핸들러를 추가한다.
function updateDom(dom, prevProps, nextProps) {
// Remove old or changed event listeners
Object.keys(prevProps)
.filter(isEvent)
.filter((key) => !(key in nextProps) || isNew(prevProps, nextProps)(key))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.removeEventListener(eventType, prevProps[name]);
});
//...
// Add event listeners
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
});
}
즉, STEP 6을 완료한 상태의 전체 코드는 아래와 같다.
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) =>
typeof child === "object" ? child : createTextElement(child)
),
},
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
};
}
function createDom(fiber) {
const dom =
element.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
const isProperty = (key) => key !== "children";
Object.keys(element.props)
.filter(isProperty)
.forEach((name) => {
dom[name] = element.props[name];
});
return dom;
}
const isEvent = (key) => key.startsWith("on");
const isProperty = (key) => key !== "children" && !isEvent(key);
const isNew = (prev, next) => (key) => prev[key] !== next[key];
const isGone = (prev, next) => (key) => !(key in next);
function updateDom(dom, prevProps, nextProps) {
// Remove old or changed event listeners
Object.keys(prevProps)
.filter(isEvent)
.filter((key) => !(key in nextProps) || isNew(prevProps, nextProps)(key))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.removeEventListener(eventType, prevProps[name]);
});
// Remove old properties
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach((name) => {
dom[name] = "";
});
// Set new or changed properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
dom[name] = nextProps[name];
});
// Add event listeners
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
});
}
function commitRoot() {
deletions.forEach(commitWork);
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}
function commitWork(fiber) {
if (!fiber) {
return;
}
const domParent = fiber.parent.dom;
domParent.appendChild(fiber.dom);
commitWork(fiber.child);
commitWork(fiber.sibling);
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
};
deletions = [];
nextUntilOfWork = wipRoot;
}
let nextUntilOfWork = null;
let currentRoot = null;
let wipRoot = null;
let deletions = null;
function workLoop(deadline) {
let shouldYield = false;
while (nextUntilOfWork && !shouldYield) {
nextUntilOfWork = performUnitOfWork(nextUntilOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextUntilOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
const elements = fiber.props.children;
reconcileChildren(fiber, elements);
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
function reconcileChildren(wipFiber, elements) {
let index = 0;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
while (index < elements.length || oldFiber != null) {
const element = elements[index];
let newFiber = null;
const sameType = oldFiber && element && element.type == oldFiber.type;
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
};
}
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
};
}
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
if (index === 0) {
wipFiber.child = newFiber;
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
const Didact = {
createElement,
render,
};
/** @jsx Didact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
);
const container = document.getElementById("root");
Didact.render(element, container);
여기까지 STEP 6 완료!
'FRONT-END > React' 카테고리의 다른 글
나만의 React 만들기(Build your own react) - STEP 8 (0) | 2024.09.09 |
---|---|
나만의 React 만들기(Build your own react) - STEP 7 (0) | 2024.09.07 |
나만의 React 만들기(Build your own react) - STEP 5 (0) | 2024.09.06 |
나만의 React 만들기(Build your own react) - STEP 4 (1) | 2024.09.05 |
나만의 React 만들기(Build your own react) - STEP 3 (0) | 2024.09.05 |
댓글