Fluent React 1 - 컴포넌트 설계 패턴
2024/02/19
n°44
category : React
☼
2024/02/19
n°44
category : React

최근에 출간된 Fluent React를 읽고 나서 인상 깊었던 내용을 정리해보고자 한다. 이전에 'Learning React'는 React 18과 관련된 최신 내용이 부족해 아쉬웠지만, 이 책을 통해 그러한 부분을 완전히 보완할 수 있었다.
가령 책에서는 그동안 궁금했던 React Fiber의 상세한 설명과 서버 컴포넌트에 대한 깊이 있는 내용을 다룬다. React 18의 기본 개념부터 효율적인 컴포넌트 설계 패턴에 이르기까지 다양한 주제를 쉽고 상세하게 설명한다. 책을 독해하면서 특히 인상 깊었던 부분과 평소 궁금했던 주제에 대해 주관적인 해석을 덧붙여 메모한다.
이번 글에서는 리액트의 익숙한 개념들에 대해서 다루기 때문에, 기본적인 개념의 설명보다는 새롭게 알게된 사실 위주로 작성하였다.
챕터5의 내용이다. 효율적인 컴포넌트 설계 패턴에 대해 알고자 하는 갈망이 있기에, 가장 먼저 살펴본다. 글이 길어질 것 같으니 먼저 앞의 항목들 중 기억할만한 내용을 정리해본다.
TLDR; React.memo
우리는 "재렌더링(re-render)"이 함수 컴포넌트를 재호출하는 것을 의미한다는 것을 알고 있습니다. React.memo로 감싸진 경우, 그 함수는 그것의 props가 변경되지 않는 한 조정(reconciliation) 도중에 다시 호출되지 않습니다. 함수형 컴포넌트를 메모이제이션함으로써, 우리는 불필요한 재렌더링을 방지할 수 있습니다 ... React는 props와 함께 함수 컴포넌트들을 재귀적으로 호출하여 vDOM 트리를 생성하고, 이 트리는 조정되는 두 개의 Fiber 트리의 기반이 됩니다. 때때로, 렌더링(즉, 컴포넌트 함수를 호출하는 것)은 함수 컴포넌트 내부의 강도 높은 계산 또는 DOM에 배치하거나 업데이트 효과를 적용할 때 강도 높은 계산으로 인해 오랜 시간이 걸릴 수 있습니다. 메모이제이션은 비용이 많이 드는 계산의 결과를 저장하고, 같은 입력이 함수에 전달되거나 같은 props가 컴포넌트에 전달될 때 그 결과를 반환함으로써 이를 피하는 방법입니다.
React.memo에 대한 익숙한 설명이다. memo는 컴포넌트 전체에서 props의 변경을 감지하고, 변경이 없을 시 메모아이즈된 컴포넌트를 보여준다는 내용이다. 추가적으로 알게된 사실은, 재조정reconciliation 동안 memo로 감싸진 컴포넌트가 호출되지 않는다는 점이다. 컴포넌트 함수의 재귀적인 호출 이후, vDOM 트리가 Firber 트리의 기반이 되어 reconciliation이 일어난다는 점은 새롭게 알게 되었다. (재조정과 관련해서 후속 글에서 더 깊이 살펴볼 예정이다.)
React에서 업데이트가 발생하면, 여러분의 컴포넌트는 이전 렌더링에서 반환된 vDOM의 결과와 비교됩니다. 이 결과가 다르다면—즉, 그것의 props가 변경되었다면—재조정자(reconciler)는 요소가 호스트 환경(보통 브라우저 DOM)에 이미 존재하는 경우 업데이트 효과를 실행하거나, 그렇지 않은 경우 배치 효과를 실행합니다. (중략) … React는 React.memo를 사용하여, 만약 그것의 props가 동일하게 유지된다면 우리가 우리의 컴포넌트들이 재렌더링되기를 원하지 않는다는 힌트를 그것의 재조정자(reconciler)에게 제공합니다. 이 함수는 단지 React에게 힌트를 제공합니다. 궁극적으로, React가 하는 일은 React에 달려 있습니다.
props의 변경 여부를 vDOM과 비교하고, 재조정 단계에서 리렌더링 여부를 결정한다는 내용. 렌더링될 필요가 없을 경우 재조정 단계에서 렌더링을 멈추고, 필요하다면 리액트가 렌더링 하도록 한다는 의미로 이해된다.
그렇다면 ‘재조정자’에서 어떤 일이 일어나는 걸까? 아래의 함수는 reconciliation에서 구현되는 memo로 감싸진 함수이다.
function updateMemoComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes
): null | Fiber {
if (current === null) {
const type = Component.type;
if (
isSimpleFunctionComponent(type) &&
Component.compare === null &&
Component.defaultProps === undefined
) {
let resolvedType = type;
if (__DEV__) {
resolvedType = resolveFunctionForHotReloading(type);
}
workInProgress.tag = SimpleMemoComponent;
workInProgress.type = resolvedType;
if (__DEV__) {
validateFunctionComponentInDev(workInProgress, type);
}
return updateSimpleMemoComponent(
current,
workInProgress,
resolvedType,
nextProps,
renderLanes
);
}
if (__DEV__) {
const innerPropTypes = type.propTypes;
if (innerPropTypes) {
checkPropTypes(
innerPropTypes,
nextProps,
"prop",
getComponentNameFromType(type)
);
}
if (Component.defaultProps !== undefined) {
const componentName = getComponentNameFromType(type) || "Unknown";
if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) {
console.error(
componentName
);
didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true;
}
}
}
const child = createFiberFromTypeAndProps(
Component.type,
null,
nextProps,
null,
workInProgress,
workInProgress.mode,
renderLanes
);
child.ref = workInProgress.ref;
child.return = workInProgress;
workInProgress.child = child;
return child;
}
if (__DEV__) {
....
}
const currentChild = ((current.child: any): Fiber);
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes
);
if (!hasScheduledUpdateOrContext) {
const prevProps = currentChild.memoizedProps;
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
}
workInProgress.flags |= PerformedWork;
const newChild = createWorkInProgress(currentChild, nextProps);
newChild.ref = workInProgress.ref;
newChild.return = workInProgress;
workInProgress.child = newChild;
return newChild;
}
memo는 이전 렌더링된 컴포넌트와 비교하여 작동한다. props의 변화가 없다면 동일한 컴포넌트를, 있다면 새 컴포넌트를 반환한다. 이 복잡한 함수는 memo가 reconciliation에서 이루어지는 단계를 설명한다.
1. 초기 검사
함수 updateMemoComponent는 현재 및 진행 중인 Fiber, 컴포넌트, 새로운 props, 그리고 렌더 레인(업데이트의 우선순위와 타이밍을 나타냄)을 포함한 여러 매개변수를 받습니다. 초기 검사(if (current === null))는 컴포넌트의 초기 렌더링 여부를 결정합니다.
2. 타입 및 빠른 경로 최적화
그런 다음 컴포넌트가 단순 함수 컴포넌트인지 그리고 Component.compare와 Component.defaultProps를 검사하여 빠른 경로 업데이트가 가능한지 확인합니다. 이러한 조건이 충족되면, 진행 중인 Fiber의 태그를 SimpleMemoComponent로 설정하여, 더 효율적으로 업데이트할 수 있는 간단한 컴포넌트 타입을 나타냅니다.
3. 개발 모드 검사
개발 모드(__DEV__)에서는, 함수가 prop 타입을 검증하고 함수 컴포넌트의 defaultProps 같은 사용되지 않는 기능에 대해 경고하는 추가적인 검사를 수행합니다.
4. 새로운 Fiber 생성
초기 렌더링인 경우, createFiberFromTypeAndProps로 새로운 Fiber가 생성됩니다. 이 Fiber는 React 렌더러의 작업 단위를 나타냅니다. 참조를 설정하고 자식(새로운 Fiber)을 반환합니다.
5. 기존 Fiber 업데이트
컴포넌트가 업데이트되고 있을 때(current !== null), 비슷한 개발 모드 검사를 수행합니다. 그런 다음 얕은 비교(shallowEqual) 또는 제공된 사용자 정의 비교 함수를 사용하여 이전 props와 새로운 props를 비교하여 컴포넌트에 업데이트가 필요한지 확인합니다.
6. 업데이트에서 벗어나기
props가 동일하고 ref가 변경되지 않았다면, bailoutOnAlreadyFinishedWork를 사용하여 업데이트에서 벗어날 수 있습니다. 이는 이 컴포넌트에 대한 추가 렌더링 작업이 필요 없음을 의미합니다.
7. 진행 중인 Fiber 업데이트
업데이트가 필요한 경우, 함수는 진행 중인 Fiber에 PerformedWork 플래그를 설정하고 현재 자식을 기반으로 하지만 새로운 props를 가진 새로운 진행 중인 자식 Fiber를 생성합니다.
하지만 React.memo는 함수 컴포넌트 전체를 memoize하는 것이기 때문에, 다음과 같은 경우에는 일반적으로 useMemo와 useCallback을 사용한다.
function ParentComponent({ allFruits }) {
const [count, setCount] = React.useState(0);
const favoriteFruits = React.useMemo(
() => allFruits.filter((fruit) => fruit.isFavorite),
[]
);
// const favoriteFruits = allFruits.filter((fruit) => fruit.isFavorite);
// favoriteFruits가 다음과 같이 작성된다면 계속 리렌더링 될 것이다.
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<List items={favoriteFruits} />
</div>
);
}