## 렌더링이란
- 브라우저에서 렌더링이란 : HTML과 CSS를 이용해서 페이지 UI를 그리는 과정이다.
- 리액트에서 렌더링이란 : 브라우저의 렌더링에 필요한 DOM 트리를 만드는 과정이다.
둘이 단어는 같지만 다른 의미를 가지고 있다!
리액트의 렌더링이 발생하더라도 브라우저의 렌더링이 발생하지 않을 수 있다.
리액트의 렌더링이란 컴포넌트들이 자신들의 props나 state 값을 가지고 어떻게 UI를 구성하고, 이를 바탕으로 어떤 DOM 결과를 브라우저에 제공할 것인지를 계산하는 과정을 의미한다. 이 때 컴포넌트에 props와 state가 없다면 이 컴포넌트가 반환하는 JSX 값을 이용해서 계산한다.
## 렌더링이 언제 발생하는가
**시나리오 1. 최초 렌더링**
사용자가 어플리케이션에 처음 진입했을 때 보여줄 UI를 그리는 과정. 리액트는 브라우저에 정보를 제공하기 위해서 최초 렌더링을 수행한다.
시나리오 2. 리렌더링
리렌더링이란 최초 렌더링 이후 발생하는 렌더링을 모두 의미한다.
리렌더링은 아래 상황에서 발생한다.
- **클래스 컴포넌트의 setState가 실행되었을 때**
-> state 변화는 컴포넌트 상태의 변화 -> 리렌더링
- **클래스 컴포넌트의 forceUpdate가 실행되었을 때**
-> render가 자동으로 실행되지 않는 상황에서 forceUpdate를 호출해서 임의로 리렌더링을 발생시킬 수 있다.
- **함수형 컴포넌트의 useState의 setter가 실행되었을 때**
- 함수형 컴포넌트의 useReducer 훅의 dispatch가 실행되었을 때
- **컴포넌트 key props가 변경되었을 때**
컴포넌트의 key props는 헨더링이 일어나는 동안 형제 요소들을 식별하는 값이다.
리렌더링 과정에서 현재 트리와 새로운 트리 사이에서 변경사항을 확인할 때, 동일한 컴포넌트가 여러개 있으면 key를 기준으로 구별한다.
- **props가 변경되는 경우**
-> props가 변경되는 경우 이를 사용하는 자식 컴포넌트도 변경되어야 하므로 리렌더링이 일어난다.
- **부모 컴포넌트가 리렌더링될 경우**
-> 자식 컴포넌트도 무조건 리렌더링이 일어난다.
> [!note]- forceUpdate
> 클래스 컴포넌트에서 렌더링을 수행하는 것은 render 메서드이다.
> 이 render 메서드가 state나 props 말고 다른 값에 의존되어서 자동으로 리렌더링이 되지 않는 경우에는 forceUpdate를 이용해서 재렌더링을 발생시킬 수 있다.
> forceUpdate를 임의로 호출한 것은 렌더링이 무조건 필요한 상황을 의미한다고 간주해서 shouldComponentUpdate를 건너뛴다. 이는 자기 자신 뿐 아니라 하위 모든 컴포넌트들에 적용된다.
> 권장되는 방식은 아님. [forceupdate – React](https://react.dev/reference/react/Component#forceupdate)
> [!tip]- key props
> 리렌더링 시 어떤 요소를 재사용할지 식별하는 기준이라고 생각하면 좋을듯?
> key가 동일하면 새로 컴포넌트를 만들지 않고 이미 만들어뒀던 컴포넌트를 그대로 쓴다.
## 리액트의 렌더링 프로세스
렌더링 프로세스가 시작되면 리액트는 컴포넌트의 루트에서부터 차근차근 아래쪽으로 내려가면서 업데이트가 필요하다고 지정되어 있는 모든 컴포넌트를 찾는다.
> [!tip] 렌더링 프로세스의 시작점
> 렌더링 프로세스는 리렌더링이 필요한 최상위 컴포넌트(업데이트가 발생한 루트)에서부터 시작해서, 그 하위 트리만 순회한다.
> 예를 들어
> ```jsx
> <App>
> <Header/>
> <Footer/>
> </App>
> ```
> 이런 구조에서 Header의 상태가 바뀌면 렌더링 프로세스는 Header 부터 그 하위 컴포넌트만 순회한다.
> [!tip] 업데이트가 필요하다고 지정되어 있는 컴포넌트 ― 이걸 어떻게 알 수 있을까?
> 리액트에서는 업데이트 큐를 내부적으로 관리한다.
> 업데이트 큐에는 다음과 같은 상황에서 작업이 쌓인다.
> - setState, useState의 setter, useReducer의 dispatch 호출
> - 부모로부터 props가 변경되었을 때
> - forceUpdate 호출
> 이런 호출이 일어날 때 마다 해당 컴포넌트의 Fiber Node를 "업데이트 필요" 상태로 표시하고
> 이 Fiber가 랜더링 스케줄러에 등록되어서 다음 렌더링 루프에서 처리된다.
업데이트가 필요하다고 지정되어 있는 컴포넌트를 찾으면 클래스 컴포넌트에서는 render 함수를 실행하고, 함수형 컴포넌트에서는 컴포넌트 함수 자체를 호출한 뒤에 그 결과물을 저장한다.
```javascript
React.createElement(
'div',
{ className: 'hello world', id: 'say hello' },
'안녕하세요'
)
```
이 결과물은 아래와 같다.
```javascript
{type: 'div', props: {className: 'hello world', id: 'say hello', children: '안녕하세요'}}
```
이렇게 각 컴포넌트의 렌더링 결과물을 수집한 뒤에 이전 Fiber Tree와 새 Fiber Tree를 비교해서 실제 DOM에 반영하기 위한 변경사항을 수집한다. 이렇게 계산하는 과정을 [[2.2 가상 DOM과 리액트 파이버|리액트의 재조정]] 이라고 한다. 이렇게 재조정 과정이 모두 끝나면 모든 변경 사항을 하나의 동기 시퀀스로 DOM에 적용해서 변경된 결과물이 보이게 된다.
리액트의 렌더링 과정은 렌더 단계와 커밋 단계로 분리되어 실행된다.
## 렌더와 커밋
렌더 단계 (Render Phase)는 컴포넌트를 렌더링하고 변경 사항을 계산하는 모든 작업을 말한다. 즉 렌더링 프로세스에서 컴포넌트를 실행해(render() 또는 return) 이 결과와 이전 가상 DOM을 비교하는 과정을 거쳐 변경이 필요한 컴포넌트를 체크하는 단계이다. 여기서 비교하는 것은 type, props, key이다. 이 세가지 중에 하나라도 변경된 것이 있으면 변경이 필요한 컴포넌트로 체크해둔다.
> 렌더 단계
> 리액트는 업데이트 된 컴포넌트를 찾아서 실행한다. (render나 컴포넌트 함수 실행)
> JSX는 React Element 객체로 바뀌고 (`{type: '', props: {}}`), 이걸 기반으로 새 Fiber Tree를 만든다. (WorkInProgress Tree)
> 이전 Fiber Tree와 새 Fiber Tree를 비교해서 달라진 것이 무엇인지를 계산한다.
> 변경된 내용은 실제 DOM을 수정하기 전에 'Effect List'라는 구조에 저장한다.
렌더 단계는 비동기적으로 실행되고, 중단이 가능하다.
다음으로 커밋 단계(Commit Phase)는 렌더 단계의 변경 사항을 실제 DOM에 적용해서 사용자에게 보여주는 과정을 말한다. 이 단계가 끝나야 비로소 브라우저의 렌더링이 발생한다.
그리고 이렇게 만들어진 모든 DOM 노드와 인스턴스를 가리키도록 리액트 내부의 참조를 업데이트 하고, 클래스 컴포넌트에서는 componentDidMount, componentDidUpdate 메서드를 호출하고 함수 컴포넌트에서는 useLayoutEffect 훅을 호출한다.
> 커밋 단계 - 렌더 단계의 계산 결과를 실제 브라우저 DOM에 적용하는 과정
> 이 단계는 동기적으로 실행된다.
> 1. Before mutation phase (DOM 조작 전)
> - DOM 변경 전, 읽기만 가능한 시점.
> - 이 시점에 getSnapshotBeforeUpdate() 메서드가 실행된다.
> 2. Mutation phase (DOM 조작)
> - 렌더 단계에서 모아 둔 Effect List를 순회하며 실제 DOM을 수정한다.
> - 변경이 끝난 뒤에 React 내부의 Fiber 참조 (current 포인터) 가 새 Fiber tree로 교체된다.
> 3. Layout phase (DOM 조작 후)
> - 화면이 실제로 업데이트 된 직후 DOM이 완성된 상태에서 실행되어야 하는 작업들을 처리한다.
> - 이 시점에서 아래 메서드와 훅이 호출된다.
> - componentDidMount, componentDidUpdate
> - useLayoutEffect
> - 이 다음에 이어서 Reflow와 Repaint가 발생한다.
참고
- [React as a UI Runtime — overreacted](https://overreacted.io/react-as-a-ui-runtime/)
> **리액트의 렌더링이 일어난다고 해서 무조건 DOM 업데이트가 일어나는 것은 아니다!**
> 변경사항을 계산햇는데, 아무런 변경이 감지되지 않았다면 커밋 단계는 생략될 수 있다.
이 두가지 과정으로 이뤄진 리액트의 렌더링은 항상 동기식으로 작동했다. 어쩌면 동기식으로 동작하는게 당연할 수도 있는데, 하나의 상태에 대해서 일관적인 UI를 볼 수 있게 하려면 변경사항을 모두 확인한 뒤에, 변경 내역을 반영해야 해야 할 것이다.
하지만 컴포넌트 하나의 렌더링이 너무 오래 걸려 상대적으로 빠르게 렌더링 할 수 있는 컴포넌트를 먼저 변경해서 보여줘야 하는 상황이 유용할 때도 있을 것이다. 이렇게 의도된 우선순위로 컴포넌트를 렌더링해서 최적화할 수 있는 비동기 랜더링이 리액트 18에 도입되었다. 비동기 랜더링은 렌더 단계가 비동기로 동작해서 특정 렌더링의 우선순위를 낮추거나, 중단하거나, 재시작하거나, 포기할 수도 있다. (자세한 내용은 [[10.2 리액트 18 버전 살펴보기]]에서)
## 일반적인 렌더링 시나리오