React는 언제 컴포넌트를 렌더링할까?

컴포넌트 렌더링 이해하기

이 글은 hr][When Does React Render Your Component?
라는 기사를 참고했습니다.


컴포넌트 렌더링

React는 컴포넌트를 정확히 언제, 왜 렌더링하는 걸까?

React는 우선 다음과 같은 상황일 경우에 컴포넌트를 렌더링하게 됩니다.

컴포넌트에 예정된 상태 업데이트가 있는 경우
컴포넌트에서 사용된 커스텀 훅의 예정된 업데이트가 있는 경우도 포함
부모 컴포넌트가 렌더링되고, 리렌더링에서 제외되는 기준에 충족하지 않을 경우(단, 제외되는 기준은 다음의 네 가지 조건을 모두 동시에 충족해야 함)
컴포넌트가 이전에 렌더링, 즉 이미 마운트 되어있어야 한다.
변경된 props(참조)가 없어야 한다.
컴포넌트에서 사용하고 있는 context 값이 변경되지 않아야 한다.
컴포넌트에 예정된 상태 업데이트가 없어야 한다.

리렌더링 확인을 위한 흐름도

React는 성능 문제가 발생할 때까지 불필요한 리렌더링에 대해서는 걱정할 필요가 없습니다. 하지만 만약 성능 문제가 발생한 경우라면 다음의 흐름도를 참고하여 어떤 해결책을 적용해야 할지 선택할 수 있습니다.

예상치 못한 리렌더링 확인을 위한 흐름도

렌더링과 업데이트

React의 컴포넌트란 "UI에서 업데이트를 예약할 수 있도록 React에 의해 강화된 함수"를 의미하는데, 컴포넌트는 스스로의 상태를 능동적으로 변화시킨 것이든, 다른 변화로 인한 것이든 상관없이 React에 의해 호출됩니다.

React의 핵심 디자인 원칙 중 하나로서, React는 UI 스케줄링 및 업데이트를 완전히 제어할 수 있는데, 이는 몇 가지 의미를 갖습니다.

컴포넌트가 만든 하나의 상태 업데이트가 반드시 하나의 렌더링, 즉 React에 의한 한 번의 컴포넌트 호출로 변환되는 것은 아니며, 그 이유는 다음과 같다.
React는 컴포넌트의 상태에 의미 있는 변화가 없다고 생각할 수 있다.(object.is에 의해 결정)
React는 상태 업데이트를 하나의 렌더 패스로 일괄 처리하려고 하지만, React는 promiseresolve되는 타이밍을 제어할 수 없기 때문에 promise에서의 상태 업데이트를 일괄 처리할 수 없으며, setTimeout, setIntervalrequestAnimatonFrame과 같이 별도의 이벤트 루프 콜 스택에서 실행되는 네이티브 이벤트 핸들러도 마찬가지이다.
React는 여러 렌더 패스로 작업을 분할할 수도 있다.
React는 다양한 이유로 컴포넌트를 렌더링(함수 호출) 할 수 있기 때문에 컴포넌트를 한 번 렌더링 하는 것이 UI의 시각적 업데이트로 반드시 변환되지는 않는다.

React 17에서 일부 상태 업데이트는 일괄 처리를 할 수 없습니다.

React 17에서는 promise에서의 업데이트 같은 일부 상태 업데이트는 일괄 처리할 수 없는데, React는 promiseresolve 되는 타이밍을 제어할 수 없기 때문입니다. 같은 맥락에서, 훨씬 나중에 완전히 별도의 이벤트 루프 호출 스택에서 실행되는 네이티브 이벤트 핸들러인 setTimeout, setInterval, requestAnimatonFrame도 마찬가지입니다. 하지만 React 18에서는 모든 상태 업데이트를 자동 일괄 처리할 수 있습니다.

물론 React가 렌더링을 제어할 수 있다고 해서 컴포넌트를 렌더링 하는 시기나 이유에 대해 신경 쓰지 않아도 된다는 것은 아닙니다. React에 모든 것을 의존하는 대신 React가 컴포넌트를 렌더링 하는데 사용하는 기본 메커니즘을 이해한다면 성능 문제에 직면했을 때 쉽게 해결할 수 있습니다.

업데이트란?

"렌더링"이라는 용어와 함게 "업데이트"라는 용어 또한 많이 사용되는데, 여기서의 "업데이트"는 맥락에 따라서 의미가 달라질 수 있습니다.

예를 들어, "컴포넌트가 업데이트를 예약한다"라고 했을 때의 업데이트는 컴포넌트가 자체의 상태를 변경하고 React에 변경 내용을 UI에 반영하도록 요청함을 의미합니다. 여기서 업데이트는 React가 컴포넌트를 렌더링(호출) 하는 이유인데, React가 컴포넌트를 렌더링 할지 여부, React가 컴포넌트를 렌더링 하기로 결정한 횟수 그리고 렌더링 하기로 결정하는 데 발생한 지연 시간은 다양한 요인에 따라 달라질 수 있습니다.

또 다른 예로, "React가 UI를 업데이트한다"라고 했을 때의 업데이트는 React가 기존 DOM 노드를 변환하거나 DOM 트리의 내부 표현과 일치하도록 새로운 DOM 노드를 생성한다는 의미인데, 여기서의 업데이트는 컴포넌트를 렌더링 한 결과를 의미합니다.

React의 렌더링 프로세스

컴포넌트는 두 가지 유형의 렌더링이 발생할 수 있는데, 바로 능동적 렌더링과 수동적 렌더링으로 다음과 같이 정의할 수 있습니다.

  • 능동적 렌더링은 컴포넌트 혹은 컴포넌트에서 사용한 커스텀 훅이 능동적으로 상태를 변경하기 위한 업데이트를 예약하고, ReactDOM.render를 직접 호출한다.
  • 수동적 렌더링은 부모 컴포넌트가 상태 업데이트를 예약하는데, 컴포넌트가 렌더링 제외 기준을 충족하지 않는다.

능동적 렌더링

능동적 렌더링이란 컴포넌트 자체 또는 컴포넌트가 사용하는 커스텀 훅이 업데이트를 예약하기 위해 능동적으로 상태를 변경하는 것을 의미합니다.

  • 클래스 컴포넌트를 사용하는 경우, Component.prototype.setState(즉, this.setState)
  • 함수형 컴포넌트를 사용하는 경우, 훅에 의해 발생한 dispatchAction으로, useReducer 훅의 dispatch 함수와 useState 훅의 상태 업데이트 함수는 모두 dispatchAction을 사용한다.

업데이트를 능동적으로 예약하는 또 다른 방법은, ReactDOM.render를 직접 호출하는 것인데, React 공식 문서에서 다음과 같은 예제를 볼 수 있습니다.

Hello World in React
function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  ReactDOM.render(element, document.getElementById("root"));
}
setInterval(tick, 1000);
렌더링 단계에 대한 구현 상세

업데이트를 예약하기 위해 어떤 함수를 사용했는지에 관계없이 모든 함수는 재조정을 담당하는 reconciler에서 scheduleUpdateOnFiber를 사용하는데, 이름을 통해 알 수 있듯이 Fiber의 업데이트를 예약합니다.

Fiber는 React 16에서 도입되었는데, 새로운 조정 알고리즘이자 React 내부의 작업 단위를 나타내는 새로운 데이터 구조입니다. Reconciler에 의해 ReactElement에서 fiber 노드가 생성되는데, 일반적으로 모든 ReactElement는 해당하는 fiber 노드가 있지만 몇 가지 예외가 있습니다. 예를 들면, Fragment 타입의 ReactElement는 해당하는 fiber 노드가 없습니다.

Fiber 노드와 ReactElement의 한 가지 중요한 차이점은 ReactElement는 변경이 불가능하기 때문에 항상 다시 생성되는 반면, fiber 노드는 변경이 가능하고 재사용할 수 있습니다. 즉 React는 컴포넌트를 렌더링에서 제외할 때 새로운 노드를 만드는 대신 구성하고 있는 fiber 트리에서 현재 해당하는 fiber 노드를 재사용할 수 있는 것이죠.

수동적 렌더링

수동적 렌더링은 React가 일부 부모 컴포넌트를 렌더링 하고 컴포넌트가 렌더링 제외 기준을 충족하지 않았을 때 발생합니다.

function Parent() {
  return (
    <div>
      <Child />
    </div>
  );
}

위 코드에서 부모 컴포넌트가 React에 의해 렌더링 되면, 자식 컴포넌트는 props에 참조 또는 아이덴디티가 변경된 것 외에 의미 있는 변경사항이 없더라도 렌더링이 되는데, 렌더링 단계에서 React는 재귀적으로 컴포넌트 트리를 탐색하여 컴포넌트를 렌더링하기 때문에, 만약 자식 컴포넌트에게 또 다른 자식 컴포넌트가 있다면 해당 자식 컴포넌트도 렌더링됩니다.

function Child() {
  return <GrandChild />; // 만약 `Child` 가 렌더링 되면 `GrandChild`도 렌더링 됨
}

답글 남기기