React 렌더링에 대한 깊은 이해 [5] - 재렌더링 생략 조건

최원빈·2022년 10월 18일
1

앞선 글에서 React는 매 렌더링이 예약될때마다 전체 앱의 재렌더링을 시도하고,
렌더링을 생략하기 위해선 결국 props가 같아야 한다고 정리했다.

컴포넌트의 props를 비교하는건 workInProgress와 current를 두고 비교한다.
이 둘은 Fiber 노드의 트리이며, 그 이전에 React Element의 트리이다.

ReactElement에서 props를 정의하는 모습을 다시 살펴보자.


JSX의 ReactElement로의 변환과정

createElement로 파싱되는 과정을 유심히 보자. createElement의 인자의 구조는 다음과 같다.

const element = React.createElement(
  'h1',                    // type, (태그명)
  {className: 'greeting'}, // props, (attribute, 속성들)
  'Hello World'            // children, 태그 사이의 값
)

createElement가 반환하는 구조를 보면, 결국 props객체가 포함된 element를 반환한다.

// JSX
const Temp = () => <div></div>;

// => createElement (props가 없으면 해당 인자는 생략한다)
const Temp = React.createElement('div', null)

// => ReactElement
const Temp = {
  type: 'div',
  props: {}
}

여기서의 props를 유심히 보자.
렌더링의 생략을 담당하는 beginWork() 메소드는 props의 비교를 단순하게 일치 연산자(===)로 비교했다.

oldProps === newProps 가 참이 되려면 두 객체의 레퍼런스가 같아야하므로 createElement가 실행되면 새로운 props객체가 생성되기에, 두 props는 같을 수 없다.

이해하면 당연한 말이다. 컴포넌트의 렌더링이 곧 createElement의 재실행이고, 렌더링되는 컴포넌트는 생략될 수 없다.
중요한건, 재귀적으로 실행되는 createElement에 있다.

이게 무슨 말이냐, 온라인 babel 컴파일러를 사용한 예제를 살펴보자.

// 이 컴포넌트를 컴파일러에 담아보자.
function Parent() {
  return (
    <div className="container">
      <Child />
    </div>
  );
}

// * - 이런 결과를 받아올 수 있다.
function Parent() {
  return React.createElement(
    "div",                           // type
    { className: "container" },      // props
    React.createElement(Child, null) // children
  );
}

만약 Parent의 state가 변경된다면 내부적으로는 무슨 일이 발생할까?

  1. React는 이전 렌더 트리인 current와, 새로 만들 렌더 트리인 workInProgress를 만든다.
  2. beginWork() 메소드로 루트 컴포넌트(App)부터 재렌더링 필요 여부를 확인한다.
  3. <Parent/>에서 update가 예약되었고, state가 변경되었으므로 <Parent/>를 다시 렌더링한다.
  4. 바로 위 코드블럭의 * 부분이 실행되어 렌더링된다.
  5. 자연스럽게 <Child/> 컴포넌트도 렌더링된다.

Child 컴포넌트가 변한 게 없어도, Child 컴포넌트는 렌더링된다.
물론 렌더링이 잘못된 것은 아니다. 정상적이다.

성능적으로 문제를 크게 일으키는 것도 아닐 뿐더러,
실제 DOM의 변화가 필요한 지 확인하는 과정을 보내는 것뿐이지만,
이젠 렌더링 과정을 아니깐. 렌더링을 생략함으로써 성능을 최적화할 수 있다.

그럼 이 과정을 따라가서, Child 컴포넌트의 재렌더링을 막아보자.


자식 컴포넌트를 컴포넌트의 props로 담기

babel 컴파일러에서, 컴포넌트의 자식 요소로 컴포넌트가 오면 재귀적으로 React.createElement를 호출한 것을 볼 수 있었다.
그럼 props로 준다면 어떨까?

// #1 Child를 그냥 자식 요소에 넣을 때
function Parent() {
  return (
    <div>
      <Child/>
    </div>
  );
}
// #1 컴파일 결과
function Parent() {
  return React.createElement("div", null, React.createElement(Child, null));
}

우선 기본적으로 렌더링했을 때다.
Parent를 렌더링하면, 곧바로 Child의 렌더링이 따라온다.

하지만 props로 준다면?

// #2 Child를 props로 넘길 때
function Parent({childComponent}) {
  return (
    <div>
      {childComponent}
    </div>
  );
}

// 컴파일 결과
function Parent({childComponent}) {
  return React.createElement("div", null, childComponent);
}

childComponent는 유지되고, Parent컴포넌트만 렌더링된다.
이를 실제로 테스트해보면 확실히 비교가 된다.

스타일링은 생략했다.

Parent에 state가 있는 구조고, Parent의 재렌더링은 Child 컴포넌트의 재렌더링을 불러온다.

이제 #2처럼 props로 주어서 테스트해보자.

Parent 혼자 렌더링된다.

이제 Parent의 렌더링은 Parent 컴포넌트에 대해서만 createElement가 실행되므로, Child는 렌더링에서 생략될 수 있다.


children prop 사용하기

React 공식문서에서 설명하는 합성(Composition) 파트에서 사용되는 방식이다.

컴포넌트 사이의 값은 children이라는 특수한 props로 들어가고, 이를 활용해 앞선 방법과 비슷하게 재렌더링을 방지할 수 있다.


동일하게 Parent 혼자 렌더링된다.


남은 방법인 React.memo와 이를 잘 활용하기 위한 useCallback, useMemo활용법에 대해서는 다음 포스팅에서 정리해야겠다.
기나긴 빌드업이 거의 끝나간다.

profile
FrontEnd Developer

0개의 댓글