TL;DR
- 리액트 렌더링은 렌더 단계와 커밋 단계로 나눌 수 있다.
- 렌더 단계는 재조정으로 가상 DOM 요소의 변화를 감지하고 필요한 업데이트를 결정하는 단계이다.
- 커밋 단계에서는 렌더 단계에서 결정된 변경 사항들을 실제 DOM에 반영한다.
렌더링이라고 하는 것은 사용자 화면에 콘텐츠를 그려내는 것을 의미하는데요, 화면에 그려지기까지 내부적으로 여러 과정을 거치지만 크게 리액트 렌더링 과정과 브라우저 렌더링 과정으로 나눌 수 있어요. 리액트 렌더링 단계라고 하면 렌더 단계(Render Phase)와 커밋 단계(Commit Phase)가 있는데요, 렌더 단계에서는 화면에 그릴 것들을 "파악"하고 커밋 단계에서는 직전에 파악한 것들을 화면에 "적용"해요. 이번 글에서는 리액트가 초기 렌더와 리렌더에 각 단계에서 어떤 것들 수행하는지 간략하게 살펴봅니다.
1.1 렌더 단계와 커밋 단계
트리거 단계(Trigger Phase)
컴포넌트가 그려지기 위해서는 어떤 액션, 즉 "트리거"가 필요합니다. 가령 사용자가 사이트를 방문한다거나, 상품 목록의 필터 버튼을 클릭했을 때처럼요.
리액트에서 렌더링 하는 이유는 크게 두 가지가 있습니다 :
- 사용자가 페이지에 처음 방문했을 때 초기 렌더
- 상태가 업데이트됐을 때 리렌더
1. 트리거 단계 - 초기 렌더
사용자가 처음 사이트에 방문하면 리소스를 서버에 요청하고 앱이 실행되는데요, 엔트리 파일에서 ReactDOM의 render() 메소드를 호출하고 루트 컴포넌트를 화면에 그립니다.
index.ts1ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
2 <App />
3);
2. 트리거 단계 - 리렌더
앱이 실행되고 다음 렌더는 상태 업데이트 함수가 호출되어 상태가 변했을 때 발생하는데요, 상태 업데이트 함수가 호출되면 리액트는 렌더링 해야 되는 것을 큐에 입력합니다. 그러고 나서 큐를 확인하고 순차적으로 렌더링 작업을 수행해요.
한 번의 렌더 사이클에서 하나의 상태 업데이트를 처리하는 것은 상대적으로 많은 연산과 리소스를 많이 필요로 하기 때문에 한 번의 DOM 업데이트를 통해서 처리하려고 시도합니다.
트리거 단계는 초기 렌더 또는 다음 렌더를 알리는 동작이자 단계라고 볼 수 있어요.
렌더 단계(Render Phase)
렌더가 "트리거"되면 렌더 단계로 넘어가 DOM에 그려질 요소들을 파악하는 과정을 거치게 됩니다. 요소들을 파악하는 단계 또한 초기 렌더와 리렌더로 구분을 할 수 있는데요.
- 초기 렌더에서 렌더 단계는 render() 메소드의 루트 컴포넌트를 호출해요.
- 리렌더에서 렌더 단계는 상태 업데이트가 발생한 컴포넌트를 호출해요.
리액트는 컴포넌트를 호출하고 모든 자식 요소들을 재귀적으로 처리해 트리의 구성 요소들을 파악해요. 여기서 재귀적으로 처리한다는 것은 컴포넌트를 호출했을 때 리턴값으로 컴포넌트가 반환되면 그 반환된 컴포너트를 다시 호출하는 과정을 의미합니다.
함수가 연쇄적으로 호출될 때 내부의 JSX는 React.createElement() 함수로 JSX를 리액트 요소로 변환하는데요, 재귀적으로 생성된 리액트 요소들은 UI의 구조를 나타내는 객체이자 DOM의 가상 복사본인 가상 DOM으로 유지돼요. 트리를 따라 호출을 반복하다가 최종적으로 더 이상 컴포넌트가 반환되지 않으면 비로소 가상 DOM 트리가 그려지겠죠.
첫 렌더에는 가상 DOM과 실제 DOM이 동기화되고 추후 렌더링에는 렌더마다 새로운 가상 DOM을 만들어요.
렌더 단계에서의 "렌더"는 컴포넌트를 호출하는 것을 의미합니다.
1. 렌더 단계 - 초기 렌더
초기 렌더에는 브라우저가 엔트리 파일을 읽으면서 루트 요소부터 파악해요.
index.ts1function App() {
2 return (
3 <main>
4 <h1>hello world</h1>
5 <Item />
6 <Item />
7 </main>
8 );
9}
10function Item() {
11 return <div>I am a Child</div>;
12}
13ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
14 <App />
15);
위 코드에서 render() 메소드가 호출되면 리액트는 createElement()로 <main>, <h1>, <div> 태그명의 HTML 요소들을 생성해요.
1.2 렌더 단계 - 초기 렌더
2. 렌더 단계 - 리렌더
리렌더에서 렌더 단계는 이전에 생성한 가상 DOM 트리와 새로 만든 가상 DOM 트리를 비교해 실제 DOM에 반영할 변경 사항들을 파악하는데요, 최소한의 변경 사항만 파악하기 위해 상태 업데이트가 발생한 컴포넌트를 호출하고 새로운 가상 DOM 트리를 만들어요. 리액트가 이전 렌더와 다음 렌더의 변화를 비교하는 과정을 재조정이라고 합니다.
1.3 렌더 단계 - 리렌더
리렌더가 발생하면 리액트는 렌더 간 어떤 요소와 속성들이 변했는지를 파악하고, 이 정보를 커밋 단계에서 사용합니다.
커밋 단계(Commit Phase)
커밋 단계에서는 직전 렌더 단계에서 두 가상 DOM 트리 간 변화를 실제 DOM에 적용하는 단계입니다. 커밋 단계 또한 초기 렌더와 리렌더에 다르게 동작하는데요.
- 초기 렌더에서의 커밋 단계는 렌더 단계에서 파악한 DOM 노드들을 DOM에 반영해요.
- 리렌더에서의 커밋 단계는 렌더 간 발생한 최소한의 변경 사항들을 DOM에 반영해요.
렌더 단계에서 계산한 변경 사항들을 실제 DOM에 적용할 때 "적용"이라는 것은 DOM 노드를 새로 생성, 수정 또는 삭제해 새로운 컴포넌트 트리와 동기화하는 과정을 의미해요. DOM의 조작이 발생하면 전체 UI를 다시 렌더링 하는 것처럼 보이지만 실제로는 변경된 DOM 노드만 파악해서 최소한의 변경만 실제 DOM에 반영합니다.
아래 예제에는 현재 시간을 나타내는 Clock 컴포넌트가 있는데요, 시간을 나타내는 <h1>과 <input> 요소로 구성돼 있습니다.
해당 컴포넌트는 현재 시간을 나타내기 때문에 1초에 한 번 상태가 업데이트됩니다. 리액트 렌더링 관점에서 봤을 때 렌더 단계에서는 1초에 한 번씩 상태 업데이트 전과 후의 가상 DOM 트리를 비교하고 변경 사항들을 커밋 단계에서 실제 DOM에 적용합니다.
위 예제에서 렌더 간 변한 것은 h1의 children인 time값이기 때문에 리액트는 다음 렌더에 h1 요소만 변경 사항으로 인식합니다, 즉 커밋 단계에는 h1 요소만 업데이트되어 DOM에 새로운 요소로 그려지는 것이죠.
정리해 보자면 앱이 실행되거나 리렌더가 발생하면 리액트는 컴포넌트 트리를 따라 컴포넌트를 연쇄적으로 호출하는데요, 반환된 JSX는 리액트 요소로 변환되어 가상 DOM 트리로 만들어져요. 리액트는 렌더 간 변경된 부분을 가상 DOM 트리를 비교해서 찾아내고 실제 DOM에 반영해요. 최종적으로 브라우저 렌더링 단계에서 화면에 UI가 그려지죠.