리액트를 사용해 앱을 개발하다보면 “리액트는 언제 또는 왜 컴포넌트를 렌더링하는지 궁금증을 갖게 됩니다. 렌더링이 언제, 왜 일어나는지 모른다면 리액트 컴포넌트들을 조합하여 사용자가 원하는 UI를 보여주기 어려울 것이고 원하지 않는 렌더링 때문에 사용자에게 좋지 않은 경험을 심어줄 수 있습니다.
따라서 리액트가 언제 컴포넌트를 렌더링 하는지 공부하며 이 글을 적습니다.
“렌더링"이란 무엇입니까?
리액트 컴포넌트의 렌더링은 props 와 state 의 조합으로 리액트가 특정 구역의 UI 를 어떻게 보여줄지 설명하는 것을 이야기합니다.
렌더링 프로세스
리액트는 컴포넌트 트리의 Root부터 시작하여 업데이트가 필요한 것으로 보이는 모든 컴포넌트를 찾아 아래쪽으로 순환합니다. 플래그가 지정된(flagged) 컴포넌트에 대해 리액트는 FunctionComponent() 를 호출하고 렌더링 출력을 저장합니다.
컴포넌트의 렌더링 출력은 일반적으로 JSX 구문으로 작성된 다음, React.createElement() 를 사용하는 JS로 컴파일 됩니다. 그리고 UI의 의도(intent)를 설명하는 일반 JS 객체를 반환합니다.
// JSX 구문:
return <SomeComponent a={42} b="testing">Text here</SomeComponent>
// React.createElement()를 사용하는 JS:
return React.createElement(SomeComponent, {a: 42, b: "testing"}, "Text Here")
// 일반 JS 객체
{type: SomeComponent, props: {a: 42, b: "testing"}, children: ["Text Here"]}
전체 컴포넌트 트리에서 리액트는 새 객체 트리(”가상 DOM”이라고도 부름)를 비교하고 모든 변경 목록을 수집합니다. 그리고 이를 토대로 진짜 DOM을 렌더링합니다. 비교하는 과정을 리액트는 Reconcilation(재조정)이라고 부릅니다.
리액트는 언제 컴포넌트를 렌더링하나요?
ReactDOM.render()를 직접 호출dispatchAction으로 인하여 컴포넌트의 상태가 바뀌었을 경우- 부모 컴포넌트가 렌더링 될 때
ReactDOM.render()를 호출하는 예시
const root = ReactDOM.createRoot(document.getElementById('root'));
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
root.render(element);
}
setInterval(tick, 1000);
root.render(element) 를 통해 매초 전체 UI를 다시 그리도록 만드는 예제입니다.
dispatchAction
useReducer 의 dispatch 함수와 useState 훅의 상태 업데이트 함수는 모두 dispatchAction 을 사용합니다. 이는 컴포넌트의 상태 변경을 위한 업데이트를 예약합니다.
부모 컴포넌트가 렌더링 될 때
리액트는 기본적으로 부모 컴포넌트가 렌더링될 때, 모든 자식 컴포넌트를 재귀적으로 렌더링합니다.
예를들어 컴포넌트 트리가 A > B > C > D 처럼 있고 이미 페이지에 표시되었다고 가정했을 때, B 컴포넌트 내부에서 setState 가 일어났다면 다음과 같은 일이 일어납니다.
B컴포넌트에서setState()를 호출합니다.- 리액트는 컴포넌트 트리 상단에서부터 비교를 시작합니다.
- 리액트는
A컴포넌트의 업데이트가 필요하지 않은 것을 보고 넘어갑니다. - 리액트는
B컴포넌트의 업데이트가 필요한 것으로 표시된 것을 보고 렌더링합니다. C는 업데이트가 필요한 것으로 표시되지 않았지만, 부모B가 렌더링되었기 때문에 다시 렌더링합니다.D도 마찬가지로 부모C가 렌더링 되었기 때문에 리액트가 이를 다시 렌더링합니다.
일반적으로 리액트는 렌더링할 때 “props 가 변경되었는지” 여부를 신경쓰지 않습니다. 부모 컴포넌트가 다시 렌더링되기 때문에 자식 컴포넌트를 렌더링한 것입니다.
렌더링 제외 기준
부모 컴포넌트가 렌더링 될 때, 모든 자식 컴포는트는 재귀적으로 렌더링합니다. 하지만 자식 컴포넌트 중 렌더링 제외 기준을 충족하는 컴포넌트가 있다면 해당 컴포넌트는 렌더링 하지 않습니다.
리액트가 렌더링 하는 모든 컴포넌트는 bigWork 를 호출합니다. bigWork 의 소스코드 의 조건문(if)를 보면 리액트 렌더링 제외 로직을 보실 수 있습니다.
current !== null- 컴포넌트가 이미 마운트된 경우
oldProps !== newProps- 변경된
props가 없는 경우
- 변경된
hasLegacyContextChanged()- 컴포넌트에서 사용하는
context값의 변경이 없는 경우
- 컴포넌트에서 사용하는
!hasScheduledUpdateOrContext- 컴포넌트 자체에서 업데이트를 약하지 않은 경우
이와 같은 렌더링 제외 로직을 조금 더 세부적으로 봐봅시다.
props 변경을 탐지하는 데 사용하는 규칙 변경 방법
oldProps !== newProps 처럼 리액트는 === 를 사용하여 props 를 비교합니다. React.memo 로 감쌀 경우, 리액트는 props 변경을 모든 프로퍼티에 얕은 비교를 수행하여 확인합니다.
이는 개념적으로 Object.keys(prevProps).some(key => prevProps[key] !== nextProps[key]) 와 유사합니다.
context 값을 변경하지 않는 방법
컴포넌트가 어떤 context 값의 consumer인 경우, provider 가 리렌더링 되고 context값이 변경됬을 때(참조적으로만), 컴포넌트는 리렌더링 됩니다.
이 경우, 가장 쉬운 해결 방법은 context 값을 useMemo 로 래핑하여 provider 컴포넌트가 다시 렌더링되더라도 참조적으로 동일하게 유지하는 것입니다.
function Parent({ children, lastChild }) {
const contextValue = {};
const memoizedCxtValue = useMemo(contextValue);
return (
<div className="parent">
<Context.Provider value={memoizedCxtValue}>
<ChildA />
{children}
{lastChild}
</Context.Provider>
</div>
);
}
useMemo 를 사용하여 context 값을 래핑할 필요가 없는 예외가 하나있습니다.
context provider 트리가 컴포넌트 트리의 최상단에 있으면 context의 값을 기억할 필요가 없습니다.
const ContextA = createContext(null);
const Parent = () => {
const [state, dispatch] = useReducer(reducerA, initialStateA);
return (
<ContextA.Provider value={[state, dispatch]}>
<Child1 />
</ContextA.Provider>
);
};
Parent 컴포넌트가 트리의 최상단에 있는 경우(다른 부모 컴포넌트가 없는 경우), 리액트가 Parent 를 렌더링하는 유일한 이유는 dispatch 가 호출되었을 때뿐입니다. 따라서 위의 예시와 같이 값을 직접 전달하는 것이 좋습니다.
암묵적인 전제
위의 모든 이야기는 컴포넌트가 항상 컴포넌트 트리의 같은 위치에 렌더링 된다는 전제하에 이루어졌습니다.
- 동일한 위치에서 다른 컴포넌트 간에 전환
- 같은 컴포넌트를 다른 위치에 렌더링
- 의도적으로
key를 변경
위와 같은 경우 리액트는 하위 컴포넌트 트리를 파괴하고 처음부터 다시 빌드합니다. 컴포넌트가 다시 렌더링될 뿐만아니라 상태또한 소실됩니다.
마무리
리액트는 props 와 state 의 조합으로 리액트 컴포넌트를 렌더링합니다. 그리고 렌더링된 리액트 컴포넌트는 ReactDOM.render() 을 호출했을 경우 혹은 상태가 변경되었을 경우 다시 렌더링됩니다. 리액트는 기본적으로 부모 컴포넌트가 렌더링할 때 자식 컴포넌트 또한 렌더링되는데 여기에 예외 기준들이 존재합니다.
이와 같은 흐름을 통해 리액트 컴포넌트가 렌더링 되는 경우를 알 수 있었습니다. 다양한 상황 속에서 리액트 렌더링으로 인해 어려움을 겪을 때, 이러한 개념들이 큰 도움을 줄 것이라 생각합니다.
참고자료
'FrontEnd > React.js' 카테고리의 다른 글
| React Query와 SWR 비교하기 (0) | 2023.02.18 |
|---|---|
| JSX.Element와 ReactElement 차이 이해하기 (0) | 2023.02.07 |
| React Hook-flow 이해하기 (0) | 2021.11.26 |
| useEffect 이해하기 (1) | 2021.10.28 |
| React Ref 이해하기 (0) | 2021.09.29 |