FE/리뷰
[모던 리액트 딥다이브] 2.4장 렌더링
따봉치치
2024. 7. 13. 22:20
728x90
브라우저의 렌더링
- HTML과 CSS 리소스를 기반으로 웹페이지에 필요한 UI를 그리는 과정
리액트의 렌더링
- 브라우저가 렌더링에 필요한 DOM 트리를 만드는 과정
- 리액트 애플리케이션 트리 안에 있는 모든 컴포넌트들이 자신들이 가지고 있는 props와 state의 값을 기반으로 어떻게 UI를 구성하고 이를 바탕으로 어떤 DOM 결과를 브라우저에 제공할 것인지 계산하는 일련의 과정
리액트의 렌더링 이유
- 최초 렌더링 : 사용자가 처음 애플리케이션에 진입하면 발생
- 리렌더링 : 최초 렌더링이 발생한 이후로 발생하는 모든 렌더링
- 클래스 컴포넌트의 setState가 실행되는 경우 상태의 변화가 발생하므로 리렌더링 발생
- 클래스 컴포넌트의 forceUpdate가 실행되는 경우
- 함수 컴포넌트의 useState()의 두 번째 배열 요소인 setter가 실행되는 경우 상태의 변화가 발생하므로 리렌더링 발생
- 함수 컴포넌트의 useReducer()의 두 번째 배열 요소인 dispatch가 실행되는 경우
- 컴포넌트의 key props가 변경되는 경우
- props가 변경되는 경우 부모로부터 전달받는 값인 props가 달라지면 이를 사용하는 자식 컴포넌트에서도 변경이 필요하므로 리렌더링 발생
리액트에서 key가 필요한 이유
- 리액트의 key는 명시적으로 선언돼 있지 않더라도 모든 컴포넌트에서 사용할 수 있는 특수한 props임
- 리액트에서의 key는 리렌더링이 발생하는 동안 형제 요소들 사이에서 동일한 요소를 식별하는 값
- 즉, 리렌더링에 필요한 컴포넌트를 최소화하기 위해 필요
리액트의 렌더링 프로세스
- 렌더링 프로세스가 시작되면 리액트는 컴포넌트의 루트에서부터 아래쪽으로 내려가면서 업데이트가 필요하다고 지정돼 있는 모든 컴포넌트를 찾음
- 업데이트가 필요하다고 지정된 함수를 발견하면 클래스 컴포넌트의 경우 클래스 내부의 render() 함수를 실행하고, 함수 컴포넌트의 경우 FunctionComponent() 자체를 호출한 뒤 그 결과물을 저장함
- 렌더링 결과물은 JSX 문법으로 구성돼 있어 자바스크립트로 컴파일되면서 React.createElement()를 호출하는 구문으로 변환됨
- 렌더링 프로세스가 실행되면서 각 컴포넌트의 렌더링 결과물을 수집한 다음, 리액트의 새로운 트리인 가상 DOM과 비교해 실제 DOM에 반영하기 위한 모든 변경 사항을 차례차례 수집함 = 리액트 재조정
- 재조정 과정이 끝나면 모든 변경 사항을 하나의 동기 시퀀스로 DOM에 적용해 변경된 결과물이 보이게 됨
렌더와 커밋
- 리액트의 렌더링은 렌더 단계와 커밋 단계라는 총 두 단계로 분리되어 실행됨
- 렌더 단계
- 컴포넌트를 렌더링하고 변경 사항을 계산하는 모든 작업
- 렌더링 프로세스에서 컴포넌트를 실행해(render() or return) 이 결과와 이전 가상 DOM을 비교하는 과정을 거쳐 변경이 필요한 컴포넌트를 체크하는 단계
- 크게 type, props, key 비교함
- 커밋 단계
- 렌더 단계의 변경 사항을 실제 DOM에 적용해 사용자에게 보여주는 과정
- 이후, 업데이트된 모든 DOM 노드 및 인스턴스를 가리키도록 리액트 내부의 참조를 업데이트
- 다음 생명주기 개념이 있는 클래스 컴포넌트에서는 componentDidMount, componentDidUpdate 메서드 호출, 함수 컴포넌트에서는 useLayoutEffect 훅을 호출
- 리액트의 렌더링이 일어난다고 해서 무조건 DOM 업데이트가 일어나는 것은 아님!
- 렌더 단계에서 아무런 변경 사항이 일어나지 않는다면 커밋 단계를 생략될 수 있음
동시성 렌더링
- 이전 리액트의 렌더링은 항상 동기식으로 작동함
- 따라서 렌더링 과정이 길어질수록 애플리케이션의 성능 저하로 이어지고, 브라우저의 다른 작업을 지연시킬 가능성 존재
- 이를 해결하기 위해 의도된 우선순위로 컴포넌트를 렌더링해 최적화할 수 있는 비동기 렌더링인 동시성 렌더링이 리액트18에서 도입됨
- 동시성 렌더링은 렌더 단계가 비동기로 작동해 특정 렌더링의 우선순위를 낮추거나 필요하다면 중단 후 재시작하거나, 경우에 따라 포기할 수 있음
- 이를 통해 브라우저의 동기 작업을 차단하지 않고 백그라운드에서 새로운 리액트 트리를 준비할 수 있어 사용자는 더욱 매끄러운 사용자 경험을 누릴 수 있음
렌더링 시나리오
import { useState } from "react"
export default function A() {
return (
<div className="App">
<h1>Hello React!</h1>
<B />
</div>
)
};
function B() {
const [counter, setCounter] = useState(0);
function handleButtonClick() {
setCounter((prev) => prev + 1);
}
return (
<>
<label>
<C number={counter} />
</label>
<button onClick={handleButtonClick}></button>
</>
)
};
function C({number}) {
return (
<div>
{number} <D />
</div>
)
}
function D() {
return <>funny!</>
}
- 사용자가 B 컴포넌트의 버튼을 눌러 counter 변수를 업데이트 할 때 렌더링 순서
- B 컴포넌트의 setState가 호출됨
- B 컴포넌트의 리렌더링 작업이 렌더링 큐로 들어감
- 리액트는 트리 최상단에서부터 렌더링 경로를 검사함
- A 컴포넌트는 리렌더링이 필요한 컴포넌트로 표시돼 있지 않으므로 별다른 작업을 하지 않음
- 그다음 하위 컴포넌트인 B 컴포넌트는 업데이트가 필요하다고 체크돼 있으므로 B를 리렌더링함
- 5번 과정에서 B는 C를 반환
- C는 props인 number가 업데이트됐기 때문에 업데이트가 필요한 컴포넌트로 체크돼 있고 업데이트 함
- 7번 과정에서 C는 D를 반환
- D는 C의 자식이므로 리렌더링함
728x90