이번에 2-4장 을 읽으면서 알고자 했던 부분들에 대해서 포스팅합니다.
📌 map 메서드를 사용해 리턴하는 컴포넌트 및 엘리먼트
arr.map((el, idx) => <li>리스트</li>);
위의 예제와 같이 가끔 map
메서드나 반복문을 통해서 엘리먼트 및 컴포넌트를 리턴할 때가 있습니다. 잘알려져있다시피, 반복문을 사용하면 key
값을 설정해주어야하는데요, 만약에 key
값을 설정해주지 않는다면 기본적으로 idx
인덱스가 키값으로 적용된다고 합니다.
📌 리렌더링? 그래서 렌더링 될 때가 언제인데?
제가 알기로, 컴포넌트가 렌더링 되는 때는 아래와 같습니다
- 마운트 될때,
- 부모 컴포넌트가 변경되었을 때, 자식 컴포넌트들도 모두 리렌더링된다.
- 상태값이 변화했을 때,
- props에 변동이 있을 때
이렇게 등.. 여러가지 사항을 고려했었는데요, 제대로 리렌더링되는 부분들을 알아보고자 아래 실험들 및 다른 아티클을 참고하였습니다.
📌 렌더링에 제외되는 기준
- 컴포넌트가 이전에 렌더링 되었어야 함. 즉, 이미 마운트 되었어야 함.
- 변경된 props(참조)가 없어야 함.
- 컴포넌트에서 사용하고 있는 context 값이 변경되지 않아야 함.
- 컴포넌트에 예정된 상태 업데이트가 없어야 함.
제가 본 아티클에서는 렌더링을 두 가지 기준으론 나누고 있었습니다. 바로 능동적인 렌더링
, 수동적인 렌더링
인데요.
능동적인 렌더링은 컴포넌트(혹은 컴포넌트에서 사용한 커스텀 훅)가 능동적으로 상태를 변경하기 위한 업데이트를 예약하는 것을 말합니다. 예를 들어 ReactDOM.render를 직접 호출하는 경우가 있습니다. 이외에도 setState
나 dispatch
를 하는 경우가 능동적인 렌더링에 해당합니다.
반대로 수동적인 렌더링은 부모 컴포넌트(들)이 상태 업데이트를 예약하고 컴포넌트가 렌더링 제외 기준을 충족하지 않는 경우입니다.
능동적인 렌더링은 비교적 이해하기 쉬운데요, 반대로 수동적인 렌더링인 과정은 어떤 예시들이 존재할까요?
function Parent() {
return (
<div>
<Child />
</div>
);
}
function Child() {
return <GrandChild /> // 만약 `Child`가 렌더링 되면 `GrandChild`도 렌더링 됩니다.
}
우리가 잘 알다시피, 부모컴포넌트가 렌더링되면 자식 컴포넌트도 렌더링된다고 알려져 있습니다. 자식 컴포넌트는 props
에 참조/아이덴디티가 변경된 것(이 내용은 아래에 더 자세히 기술합니다.) 외에 의미 있는 변경사항이 없더라도 렌더링 됩니다.
렌더 단계에서는 재귀적으로 컴포넌트 트리를 탐색하기 때문에, 부모가 렌더링되면 자식도 렌더링되고, 그 자식의 자식도 렌더링 되게 됩니다. (계속해서 얘기하지만, 렌더링 제외 기준을 충족하지 않을 경우에 한합니다.)
그렇다면, 렌더링 제외 기준이란 무엇일까요?
이에 답하기 위해 두 가지 예시를 살펴보겠습니다.
🧐 모든 자식 컴포넌트가 동일하게 만들어지지 않았을 때
default function App() {
return (
<Parent lastChild={<ChildC />}>
<ChildB />
</Parent>
);
}
function Parent({ children, lastChild }) {
return (
<div className="parent">
<ChildA />
{children}
{lastChild}
</div>
);
}
function ChildA() {
return <div className="childA"></div>;
}
function ChildB() {
return <div className="childB"></div>;
}
function ChildC() {
return <div className="childC"></div>;
}
위의 예시에서 Parent
의 업데이트가 예정되어있다면 어떤 컴포넌트들이 렌더링 될까요? Parent
자체는 업데이트를 예약한 컴포넌트이기 때문에 리액트에 의해 리렌더링 될 것입니다. 하지만 모든 자식 컴포넌트 ChildA, ChildB, ChildC도 리렌더링 될까요?
그래서 일정 간격으로 리렌더링 하는 useForceRende
함수를 만들어 실험하였습니다.
function useForceRender(interval) {
const render = useReducer(() => ({}))[1];
useEffect(() => {
const id = setInterval(render, interval);
return () => clearInterval(id);
}, [interval]);
}
이 훅을 부모 컴포넌트인 Parent
안에서 사용하고 어떤 컴포넌트가 리렌더링 되는지 확인하였습니다.
function Parent({ children, lastChild }) {
useForceRender(2000);
console.log("Parent is rendered");
return (
<div className="parent">
<ChildA />
{children}
{lastChild}
</div>
);
}
ChildA
는 리렌더링 되었습니다. 이는 부모 컴포넌트가 업데이트를 예약하고 리렌더링 되었다는 것을 알고 있기 때문입니다.
그러나 ChildA
와 달리 ChildB
와 ChildC
는 리렌더링 되지 않았습니다.
자식 컴포넌트란 부모 컴포넌트의 JSX 안에 사용된 모든 컴포넌트들
라고 정의되어있기 때문에 ChildB
와 ChildC
는 App
의 자식 컴포넌트입니다. App 컴포넌트가 리턴하는 JSX에는 Parent, ChildB, ChildC가 있습니다. 즉, Parent, ChildB, ChildC는 App 의 자식 컴포넌트들입니다. ChildC는 App 컴포넌트가 렌더링하여 Parent의 lastChild props으로 전달해주는 것이고, ChildB도 App 컴포넌트가 렌더링하여 Parent의 children props으로 전달해줍니다.
반대로 ChildA
는 부모가 렌더링 되면, 자식 컴포넌트도 렌더링 되어야한다는 그 규칙을 준수하기 때문에, 렌더링 되고 있습니다.
🧐 Context consumer는 provider가 렌더링 될 때마다 렌더링 됩니다
수동적인 렌더링은 컴포넌트가 context consumer
일 때에도 발생할 수 있습니다.
이전 예시를 조금 바꿔서 Parent
컴포넌트가 context provider
로, ChildC
가 context consumer
로 만들어 보겠습니다.
const Context = createContext();
export default function App() {
return (
<Parent lastChild={<ChildC />}>
<ChildB />
</Parent>
);
}
function Parent({ children, lastChild }) {
useForceRender(2000);
const contextValue = {};
console.log("Parent is rendered");
return (
<div className="parent">
<Context.Provider value={contextValue}>
<ChildA />
{children}
{lastChild}
</Context.Provider>
</div>
);
}
function ChildA() {
console.log("ChildA is rendered");
return <div className="childA"></div>;
}
function ChildB() {
console.log("ChildB is rendered");
return <div className="childB"></div>;
}
function ChildC() {
console.log("ChildC is rendered");
const value = useContext(Context);
return <div className="childC"></div>;
}
위의 에제 결과는 아래와 같습니다.
리액트가 Parent
를 렌더링 할 때마다 이전 contextValue
와 다른 참조 값을 갖는 새로운 contextValue
를 생성하게 됩니다. 결과적으로 context consumer
인 ChildC
는 다른 context value
를 갖게 되며, 리액트는 변경 사항을 반영하기 위해 ChildC
를 리렌더링 합니다.
만약 contextValue
가 숫자나 문자열 같은 원시 값이면 리렌더링 시 동등성이 변경되지 않기 때문에 ChildC
가 리렌더링 되지 않는다는 것에 주의하세요. (지금은 객체를 사용하였기 때문에, 참조 값이 달라 렌더링 되는 것 입니다.)
📌 props는 매 랜더링 마다 새로 생성되는가?
이전에 리액트 스터디에서 리액트는 Object.is
와 내부적으로 구현되는 shallow
한 비교를 통해서 렌더링 할지말지에 대한 비교를 한다는 내용이 나온적이 있습니다. 의존성 배열이나 props
를 비교할 때 사용한다고 하는데요, 그때 스터디에서도 그럼 매 렌더링때마다 새로운 props를 만드는 것인가? 에 대한 의문이 있었습니다.
결론적으로 이는 **맞다.**인데요. 아래를 보면 더 쉽게 이해할 수 있습니다.
책 초반에서도 알 수 있듯이 리턴문에있는 jsx
문법은 React.createElement
를 사용해 리액트 엘리먼트를 생성하게 됩니다. 그리고 추후 단계에서 리액트 엘리먼트가 fiber로 변경되게 되는데요.
이때 리액트 엘리먼트는 매번 새로 생성되지만, fiber
같은 경우는 재사용될 수 있습니다.
- 순서 : jsx > react.element > fiber
결론적으로 컴포넌트의 props는 React.createElement
에서 생성한 ReactElement
의 속성입니다. ReactElement는
불변이므로 리액트가 컴포넌트를 렌더링(호출) 할 때마다 React.createElement
가 호출되어 새 ReactElement
가 생성됩니다. 따라서 컴포넌트의 props
는 리렌더링할 때마다 처음부터 생성됩니다.
function Parent() {
return (
<div>
<Child />
</div>
);
}
Parent
에서 반환된 <Child />
는 Babel에 의해 React.createElement(Child, null)
로 컴파일되고 {type: Child, props: {}}
이러한 형태의 ReactElement
가 생성됩니다. props는 자바스크립트 객체이기 때문에, 이전 props
와 현재 props
를 비교하게 되는데요, 이때 Object.is
와 shallow
한 비교가 도입되어 비교됩니다. 따라서 참조값이 다르기때문에 달라졌다고 판단하게 됩니다.
만약 Parent
가 렌더링되면, Child
가 자식이기 때문에 렌더링되겠죠? 추가적으로도 매번 새로운 ReactElement
를 생성하게 되기때문에, 매번 다른 props
의 참조값을 가져, 리렌더링 되게 됩니다. 즉, 렌더링되는 이유가 2가지로 할 수 있겠어욥.
하지만 만약 Child를 Parent의 props로 전달할 수 있다면 어떻게 될까요?
function App() {
return (
<Parent>
<Child />
</Parent>
);
}
function Parent({ children }) {
return <div>{children}</div>;
}
리액트에 의해 Parent
가 렌더링 될 때 Child
에 대한 React.createElement
함수가 호출되지 않습니다. 따라서 Child
의 새로운 props
가 생성되지 않고, 이는 위에서 언급한 네 가지 렌더링 제외 기준을 모두 충족시킵니다.
Reference Doc
[번역] 리액트 렌더링 동작의 (거의) 완벽한 가이드 [A (Mostly) Complete Guide to React Rendering Behavior]
[10분 테코톡] 앨버의 리액트 렌더링 최적화