React 렌더링에 대한 깊은 이해 [4] - 재렌더링 생략 과정

최원빈·2022년 10월 18일
1

이 시리즈의 1번 글에서, 렌더링을 실행하는 4가지 조건을 말한 적이 있다.

  1. useState hook의 두 번째 인자인 setState()로 인해 state가 변경되었을 때 해당 컴포넌트는 렌더링된다.
  2. useReducer의 dispatch함수가 실행되어 action으로 state가 변경되었을 때, 해당 컴포넌트는 렌더링된다.
  3. 부모 컴포넌트가 렌더링될 때, 모든 자식 컴포넌트는 렌더링된다.
  4. "key" 라는 특수한 prop값이 변경될 때, 해당 컴포넌트는 새로 렌더링된다.

렌더링이 실행되면 해당 컴포넌트부터 모든 자식 컴포넌트를 재귀적으로 렌더링한다고 한다.
그런데 React.js 개발자가 말하길, 렌더링이 발생하면, React는 전체 앱을 전부 재렌더링한다고 한다.

네? "entire" component요?

분명히 모순적이다. React 개발자가 직접 모든 앱을 전부 재렌더링한다고 하는데,
실제로 자식 컴포넌트의 state가 변경된다고 해서 부모 컴포넌트의 렌더링은 발생하지 않았다.

왜 React는 부모 중에서도 꼭대기인 Root부터 렌더링한다 해놓고 렌더링하지 않는걸까?


모든 컴포넌트를 렌더링하지 않는 이유

state가 변경되거나 하는 이유로 React가 재조정을 해야한다면, 전체 앱을 기준으로 해당 컴포넌트가 렌더링을 필요로 하는지 확인한다.

앞선 글들에서, React는 Fiber Node들로 모은 이전 렌더 트리와, 새로 만든 렌더 트리를 비교해서 재조정을 한다고 했는데
React는 이전 렌더 트리를 current, 새로 만든 렌더 트리를 workInProgress라고 이름지어 사용한다.

그리고 beginWork() 라는 React 내부 함수에서 컴포넌트를 새로 렌더링해야하는지 확인한다.
가장 루트 컴포넌트(보통은 <App/>)로부터, 모든 컴포넌트를 돌면서 beginWork() 를 통해 workInProgress에 "새로 컴포넌트를 렌더링해야하는가?"를 확인한다.

beginWork() 가 "렌더링해야 하는가?"를 판단하는 기준은 state의 변경 여부와 props의 변경 여부이다.
이 내용으로 다시 정리해보자.

  1. React에게 렌더링을 대기열에 담는 컴포넌트의 상태 변경이 일어난다.
  2. React는 모든 컴포넌트를 순회하며 beginWork() 로 재렌더링이 필요한 컴포넌트를 찾는다.
  3. state나 props가 바뀐 렌더링이 필요한 컴포넌트부터 모든 자식 컴포넌트를 렌더링한다.

컴포넌트 Bail Out (탈출, 구제)하기

beginWork() 함수를 좀 더 들여다보자.
아래 네 가지 조건을 통과해야만 attemptEarlyBailoutIfNoScheduledUpdate() 메소드를 호출한다.

current !== null
oldProps === newProps
hasLegacyContextChanged() === false
hasScheduledUpdateOrContext === false

내부적인 구현은 조금 미뤄두고 핵심만 짚어보면,
attemptEarlyBailoutIfNoScheduledUpdate() 은 컴포넌트에 종류에 따라 반환하는 것이 다르다.

Context의 Provider나, Suspense같은 특별한 컴포넌트들을 제외하고,
일반적인 컴포넌트들 대상으로는 bailoutOnAlreadyFinishedWork() 라는 메소드를 호출한다.

이 함수는 함수 이름답게 해당 컴포넌트를 bail out 시킨다.

function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {

  // ...중간생략
  // clone 해서 return 한다.
  cloneChildFibers(current, workInProgress);
  return workInProgress.child;
}

return 하기 직전에 유심히 보자.
누가 봐도 자식 Fiber 노드들을 복사할 것 같은 메소드로 복사해서 반환한다.

이미 작업이 끝나있는 Fiber 노드들은 bail out 시키는 로직이 담겨있다.
기존 current(기존 렌더 트리)의 Fiber 노드를 그대로 복사해서 workInProgress(새로 만든 렌더 트리)에 담으면 다시 렌더링할 필요가 없어지는 것이다.

이러한 과정에 의해 React는 매 렌더링마다 모든 컴포넌트를 렌더링해야 하지만, bail out 기준을 맞춘 컴포넌트들은 렌더링에서 제외된다.


렌더링 제외 기준

앞에서 말한 네 가지 기준을 확인하면서 렌더링을 생략하는 방법을 알아보자.
우리의 목표는 bailoutOnAlreadyFinishedWork() 를 호출해 렌더링조차 최적화하는 것이다.

1. current !== null

current는 기존 렌더 트리를 말한다고 했다.
일단 기존에 렌더링이 된 상태여야 생략할 수 있다는 말이니 이는 당연하다.

2. oldProps === newProps

이전 Props와 이후 Props가 같아야 한다.
props는 객체라 참조 타입이고, 비교 연산자로 비교하면 당연히 다를텐데?
둘이 같은 레퍼런스를 참조할 수 있는듯한데.. 일단 넘어가자.

3. hasLegacyContextChanged() === false

변경된 Context값이 없어야 한다.
Consumer라면 context의 변경에 맞춰서 렌더링되어야 하니 이것도 당연하다.

4. hasScheduledUpdateOrContext === false

이 컴포넌트가 직접 업데이트를 예약한 것이 아니여야한다. 이것도 당연하다.

여기서 알 수 있는 사실은, props만 신경쓰면 재렌더링을 생략할 수 있다는 것이다.

다음 포스팅에서는 컴포넌트에 담을 props에 대해서 자세히 알아보자.


참고
[번역] 리엑트는 언제 컴포넌트를 렌더링하나요?
React 톺아보기 시리즈

profile
FrontEnd Developer

0개의 댓글