리액트의 컴포넌트 렌더링
리액트를 사용해 앱을 개발하다보면 “리액트는 언제 또는 왜 컴포넌트를 렌더링하는지 궁금증을 갖게 됩니다. 렌더링이 언제, 왜 일어나는지 모른다면 리액트 컴포넌트들을 조합하여 사용자가 원하는 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()
을 호출했을 경우 혹은 상태가 변경되었을 경우 다시 렌더링됩니다. 리액트는 기본적으로 부모 컴포넌트가 렌더링할 때 자식 컴포넌트 또한 렌더링되는데 여기에 예외 기준들이 존재합니다.
이와 같은 흐름을 통해 리액트 컴포넌트가 렌더링 되는 경우를 알 수 있었습니다. 다양한 상황 속에서 리액트 렌더링으로 인해 어려움을 겪을 때, 이러한 개념들이 큰 도움을 줄 것이라 생각합니다.