앞선 글에서 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가 변경된다면 내부적으로는 무슨 일이 발생할까?
current
와, 새로 만들 렌더 트리인 workInProgress
를 만든다.beginWork()
메소드로 루트 컴포넌트(App)부터 재렌더링 필요 여부를 확인한다.<Parent/>
에서 update가 예약되었고, state가 변경되었으므로 <Parent/>
를 다시 렌더링한다.<Child/>
컴포넌트도 렌더링된다.Child 컴포넌트가 변한 게 없어도, Child 컴포넌트는 렌더링된다.
물론 렌더링이 잘못된 것은 아니다. 정상적이다.
성능적으로 문제를 크게 일으키는 것도 아닐 뿐더러,
실제 DOM의 변화가 필요한 지 확인하는 과정을 보내는 것뿐이지만,
이젠 렌더링 과정을 아니깐. 렌더링을 생략함으로써 성능을 최적화할 수 있다.
그럼 이 과정을 따라가서, Child 컴포넌트의 재렌더링을 막아보자.
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는 렌더링에서 생략될 수 있다.
React 공식문서에서 설명하는 합성(Composition) 파트에서 사용되는 방식이다.
컴포넌트 사이의 값은 children이라는 특수한 props로 들어가고, 이를 활용해 앞선 방법과 비슷하게 재렌더링을 방지할 수 있다.
동일하게 Parent 혼자 렌더링된다.
남은 방법인 React.memo와 이를 잘 활용하기 위한 useCallback, useMemo활용법에 대해서는 다음 포스팅에서 정리해야겠다.
기나긴 빌드업이 거의 끝나간다.