영문 블로그 글을 번역했습니다. 허가를 받으면 시리즈를 이어갈 예정입니다. 원문링크: https://jser.dev/react/2022/02/04/how-React-handles-empty-values
ℹ️React Internals Deep Dive 에피소드 18, 유튜브에서 제가 설명하는것을 시청해주세요.
⚠React@18.2.0기준, 최신 버전에서는 구현이 변경되었을 수 있습니다.
💬 역자 주석: JSer의 코멘트는 ❗❗로 표시 해뒀습니다.
그 외 주석은 리액트 소스 코드 자체의 주석입니다.
... 은 생략된 코드입니다.
Booleans, Null, Undefined는 무시된다는 것을 모두 알고 계시겠지만, 아래와 같은 예제는 모두 동일하게 렌더링합니다.
<div />
<div></div>
<div>{false}</div>
<div>{null}</div>
<div>{undefined}</div>
<div>{true}</div>
하지만 이러한 값은 React 내부에서 정확히 어떻게 처리될까요? 알아봅시다.
이러한 값은 컴포넌트가 아니어서 일부 컴포넌트의 자식으로만 존재하므로 reconcileChildren()을 사용해 보겠습니다.
ℹ 이 포스팅은 저도 배우고 있는 시리즈의 일환이기도 한데, 당황스러운 부분이 있으시다면 먼저 제 과거 포스팅이나 영상을 보시면 될 것 같습니다. 위와 같이 이미 알고 있는 함수이기 때문에 그냥 넘어가는데, 그 이유를 설명하기는 어렵습니다.
reconcileChildren()에서 첫 번째 렌더링에는 mountChildFibers()
가, 이후 렌더링에는 reconcileChildFibers()
가 사용됩니다.
하지만 실제로 이 두 기능은 부작용을 추적할지 여부만 다를 뿐 동일합니다.
(소스)
export const reconcideChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
ChildReconciler()에서 side effect는 '삭제'를 의미한다는 것을 알 수 있습니다.
ChildReconciler()
는 위의 플래그 아래 몇 가지 클로저 함수로 구성되며, 실제 reconcileChildFibers()를 내보냅니다(export).
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes
): Fiber | null {
if (typeof newChild === "object" && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes
)
);
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(returnFiber, currentFirstChild, newChild, lanes)
);
case REACT_LAZY_TYPE:
if (enableLazyElements) {
const payload = newChild._payload;
const init = newChild._init;
// TODO: This function is supposed to be non-recursive.
return reconcileChildFibers(
returnFiber,
currentFirstChild,
init(payload),
lanes
);
}
}
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes
);
}
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes
);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
if (
(typeof newChild === "string" && newChild !== "") ||
typeof newChild === "number"
) {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
"" + newChild,
lanes
)
);
}
// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
이 것은 4단계로 구성되어 있습니다.
$$typeof
를 기반으로 단일 엘레먼트 타입을 처리합니다.배열 또는 이터레이터를 처리합니다.
비어 있지 않은 문자열 및 숫자
나머지는 비어있는 것(empty)으로 처리되어 이전 파이버가 있으면 삭제됩니다.
따라서 파이버를 만들 때 null, undefined 및 boolean은 단순히 무시된다는 것을 알 수 있습니다.
배열에 있는 경우는 어떨까요, reconcideChildrenArray()를 살펴보겠습니다.
reconcideChildrenArray()
에는 나중에 다루고 싶은 몇 가지 알고리즘이 있습니다.
코드를 살펴보면 새로운 파이버가 생성될 수 있는 두 곳을 확인할 수 있습니다.
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes
);
reconcileChildrenArray()
에서는 배열 항목을 반복하여 새로운 링크드 파이버 리스트를 구성합니다.
newFiber가 null
이면 단순히 무시되고, 파이버 트리에 추가되지 않습니다.
updateSlot() 및 updateFromMap()에서 빈 값이 무시되고 null
이 반환되는 유사한 패턴을 발견할 수 있습니다.
function updateSlot(
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
...
}
if (typeof newChild === 'object' && newChild !== null) {
...
}
return null;
}
그게 다입니다. 이제 React에서 빈 값이 어떻게 처리되는지 알게 되었습니다. - 이것들은 간단하게 무시됩니다.
한 가지 작은 문제는 실제로 reconcileChildrenArray()
의 조정(reconciling) 알고리즘에 영향을 미친다는 것인데, 이에 대해서는 곧 포스팅을 작성할 예정이니 계속 지켜봐 주시기 바랍니다.
(원본 게시일: 2022-02-04)