리액트 렌더링 🆚 브라우저 렌더링 🫨

Jyerr·2024년 10월 7일
post-thumbnail

안녕하세요 👋🏻
오늘은 리액트 렌더링브라우저 렌더링에 대해 이야기 해볼께요
사실 우리는 웹을 개발하면서 렌더링, 리렌더링 이라는 표현을 너무나도 자주 , 쉽게 사용해서

리액트 렌더링과
브라우저 렌더링.

" 그게 그거 아닌가요 ? 🙄"
" 둘이 같은말 아닌가요 ? 😓" 하고 착각 할 수 있다.

그치만 리액트 렌더링과 브라우저 렌더링은 엄연히 전혀 다른 말이다 !! 😱 🤯 🫢





먼저 브라우저 렌더링에 대해 이야기 해보겠다.

⛱️ 브라우저 렌더링

" 브라우저에서의 렌더링 과정을 설명해주세요 "

프론트엔드 면접에서 들을 수 있는 가장 기본적인 질문중 하나이다.

그래서 브라우저 렌더링이 뭔데 ??
➡️ HTML과 CSS 리소스를 기반으로 웹페이지에 필요한 UI를 그리는 과정이다.



우리가 작성한 html,css 코드들을 브라우저는 다음과 같은 순서를 거쳐 최종 출력물을 만들어 내어 사용자에게 보여준다.

📌 순서

  1. 브라우저가 사용자가 요청한 주소를 방문해 HTML 파일을 다운로드

  2. 브라우저의 렌더링 엔진HTML을 파싱해 DOM 노드로 구성된 트리(DOM)를 만든다
    ➡️ DOM 트리 생성

  3. 2번 과정(html 파싱중) CSS 파일을 만나면 해당 CSS 파일도 다운로드 한다

  4. 브라우저의 렌더링 엔진은 이 CSS도 파싱해 CSS 노드로 구성된 트리(CSSOM)를 만든다
    ➡️ CSSOM 트리 생성

  5. 브라우저는 DOM 트리와 CSSOM 트리를 결합하여 렌더 트리를 만든다. 이 트리는 사용자 화면에 표시될 노드만 포함하고 있다.
    ➡️ Render 트리 생성
    ➡️ 즉, display : none과 같이 사용자 화면에 보이지 않는 요소는 방문해 작업하지 X . (더 빠르게 진행하기 위해)

    • 레이아웃(=리플로우) : 렌더 트리를 기반으로 각 노드가 브라우저 화면의 어느 좌표에 정확히 나타나야 하는지 계산하는 과정 . 이 레이아웃 과정을 거치면 반드시 페인팅 과정도 거치게 된다
    • 페인팅 : 레이아웃 단계를 거친 노드에 색과 같은 실제 유효한 모습을 그리는 과정
  6. 복잡한 레이아웃이 여러 레이어로 나뉘어 있다면, 브라우저는 이러한 레이어들을 독립적으로 관리하고, 이를 합성하여 최종적인 화면을 구성한다. (composite 과정 )

이처럼 브라우저는 사용자에게 웹 페이지를 렌더링 하여 보여준다. 그러나 요즘 대다수의 앱은 렌더링 된 후에도 다양한 사용자의 인터렉션을 통해 웹페이지가 계속해서 변경되어야 한다.

렌더링 이후 추가 렌더링 작업은 리액트와 같은 Single Page Appliction에서 더욱 많아진다.

why?

페이지가 변경되는 경우 다른 페이지로 가서 처음부터 HTML을 새로 받아서 다시 랜더링 과정 시작
🆚
하나의 페이지에서 계속해서 요소를 삭제, 삽입등으로 재계산

후자가 SPA , 전자는 일반적인 웹 Appliction이다.
후자가 확실히 DOM을 관리하는 과정에서 부담이 증가하게 된다.

사실..

개발자는 사용자의 인터렉션에 따라 DOM의 모든 변경사항 추적 보다는 결과적으로 만들어지는 DOM 결과물이 필요하다
그래서 생각해낸게 가상 돔 이다


🎀 가상돔

리액트가 관리하는 가상의 DOM (실제 브라우저의 DOM X)

가상돔은 웹페이지가 표시해야할 DOM을 일단 메모리에 저장하고 리액트가 실제 변경에 대한 준비가 완료됐을 때 실제 브라우저의 DOM에 반영하게 된다.

가상돔이라는 것까지 알아보았다.
이제 리액트 렌더링에 대해 이야기해보자.






🌊 리액트 렌더링

일반적으로 우리가 "렌더링" 이라는 단어를 생각할 때, 무엇인가를 화면에 그리는 작업을 떠올릴 수 있지만, React에서의 렌더링은 오히려 화면에 무엇인가를 그리기 전 어떤 과정을 시작하는 듯한 느낌에 가깝다.

리액트에서의 렌더링은
브라우저가 렌더링에 필요한 DOM 트리를 만드는 과정이다.

React에서의 렌더링은 DOM 업데이트를 의미하지 않는다❕❕ 즉, React에서의 렌더링은 실질적 화면 업데이트를 말하는게 아니다. 화면 업데이트를 위한 DOM 트리를 만드는 과정일뿐 ❕❕❕❕

리액트도 브라우저와 마찬가지로 이 렌더링 작업을 위한 자체적인 렌더링 프로세스가 있다.

더 자세히 알아보면
리액트 애플리케이션 트리 안에 있는 모든 컴포넌트들이 현재 자신들이 가지고 있는
propsstate의 값을 기반으로 어떻게 UI를 구성하고 이를 바탕으로 어떤 DOM 결과를 브라우저에 제공할 것인지 계산하는 일련의 과정이다.



그렇다면 이 리액트 렌더링은 언제 일어나는 것일까?

📌 리액트의 렌더링이 일어나는 경우

✏️ 최초 렌더링
: 처음 애플리케이션 집입 후 최초 렌더링

✏️ 리렌더링
: 최초 렌더링이 발생한 이후로 발생하는 모든 렌더링



📌 리렌더링 발생 경우

1️⃣ 함수형 컴포넌트의 useState()의 두 번째 배열 요소인 setter가 실행되거나 클래스형 컴포넌트의 setState가 실행되는 경우

➡️ state의 변화는 컴포넌트의 상태 변화를 의미한다. 컴포넌트의 상태가 변화할때 렌더링을 발생한다.

2️⃣ 클래스형 컴포넌트의 forceUpdate가 실행되는 경우

➡️ state나 props가 아닌, 다른 data을 통해서, render() 함수가 동작하게 되어 있는 경우가 있을 수 있다.
이런 경우, React의 기본적인 component life cycle와 무관하기 때문에, 직접 React에게 해당 component가 re-rendering되야 한다고 알려야 한다.
이때 사용가능한 함수가 이다.
곧바로 render() 함수가 동작하기 때문에 shouldComponentUpdate는 건너뛴다. 또한 render 내부에서 사용시 무한루프에 빠진다. 따라서 React의 공식 document에는 forceUpdate()의 사용을 최대한 피하도록 권고한다.


3️⃣ 함수형 컴포넌트의 useReducer()의 두 번째 배열 요소인 dispatch가 실행되는 경우
➡️ useReducer도 useState와 마찬가지로 상태와 이상태를 업데이트하는 함수를 배열로 제공한다.


4️⃣ 컴포넌트의 key props가 변경되는경우

key = 명시적으로 선언돼 있지 않더라도 모든 컴포넌트에서 사용할 수 있는 특수한 props

일반적으로 key는 다음과 같이 배열에서 하위 컴포넌트를 선언할 때 사용된다.

const arr = [1,2,3]

export default function App() {
	return (
		<ul>
			 {arr.map((index)=>(
				<li key={index}>{index}</li>
			))}
		</ul>

			)
}

리액트에서 배열에 key를 쓰지 않으면 콘솔에 경고가 출력 되기 때문에 key를추가한다..
Q. 근데 왜 추가해야되는걸까?

A . key는 리렌더링이 발생하는 동안 형제 요소들 사이에서 동일한 요소를 식별하는 값 이기 때문이다.

동일한 자식 컴포넌트가 여러개 있는 구조를 상상해 보자.
리렌더링이 발생하면
current 트리(바뀌기전 - 현재 화면에 표시되고 있는 트리)와 workInProgress 트리(바뀐후-업데이트 중인 새로운 트리) 사이에서 어떠한 컴포넌트가 변경이 있었는지 구별해야 하는데,
이 두 트리 사이에서 같은 컴포넌트인지를 구별하는 값이 바로 key 이다.

6️⃣ props가 변경되는 경우
➡️ 부모로부터 전달받는 값인 props가 달라지면 이를 사용하는 자식 컴포넌트에서도 변경이 필요하므로 리렌더링이 일너난다.

7️⃣ 부모 컴포넌트가 렌더링 될 경우
부모 컴포넌트가 리렌더링된다면 자식 컴포넌트도 무조건 리렌더링이 일어난다.





📌 리액트 렌더링 프로세스 (과정)

React는 컴포넌트 트리의 루트에서부터 시작하여 아래쪽으로 순환하며 업데이트가 필요하다고 표시된 컴포넌트를 전부 찾는다.

표시된 각각의 컴포넌트에 대해서,
클래스형 컴포넌트일 경우 classComponentInstance.render() ,
함수형 컴포넌트일 경우 FunctionComponent()를 호출하고, 렌더 결과물을 저장한다.

컴포넌트의 렌더 결과물은 보통 JSX 구문으로 작성되며, JS가 컴파일되고 배포 준비가 되는 시점에서 React.createElement() 호출로 변환됩니다. 여기서 createElement는 브라우저의 UI 구조를 설명할 수 있는 일반적인 자바스크립트 객체(엘리먼트 객체)를 반환한다.


// 다음과 같은 JSX 문법이:
return <SomeComponent a={42} b="testing">Text here</SomeComponent>

// 이런 식의 호출로 변환됩니다:
return React.createElement(SomeComponent, {a: 42, b: "testing"}, "Text Here")

// 그렇게해서 이런 엘리먼트 객체가 됩니다: -> 얘로 dom 구성
{type: SomeComponent, props: {a: 42, b: "testing"}, children: ["Text Here"]}

렌더링 프로세스가 실행되면서 이런 과정을 거쳐 각 컴포넌트의 렌더링 결과물을 수집한 다음,
리액트의 새로운 트리인 가상 DOM과 비교해 실제 DOM에 반영하기 위한 모든 변경 사항들을 차례차례 수집한다.
이러한 비교 및 계산 과정을 재조정 (Reconciliation) 이라고 한다.

그리고나서 React는 이렇게 계산된 모든 변경사항을 하나의 동기적 시퀀스실제 DOM에 적용시킨다.


이런식으로 진행되는 리액트 렌더링의 과정을 보통 크게 2개로 분리하여 말한다

바로 랜더 단계커밋 단계이다.



🎀 렌더 단계

: 컴포넌트를 렌더링하고 변경 사항을 계산하는 모든 작업이다

  • 컴포넌트를 실행해(render() 또는 return) 이 결과와 이전 가상 DOM을 비교하는 과정을 거쳐 변경이 필요한 컴포넌트를 체크하는 단계다.
    여기서 비교하는 것은 크게 3가지로 type, props , key 이다.

🎀 커밋 단계

: 렌더 단계의 변경 사항을 실제 DOM에 적용해 사용자에게 보여주는 과정이다

이 단계가 끝나면 비로소 브라우저의 렌더링이 발생한다.

리액트가 DOM을 커밋 단계에서 업데이트한다면 이렇게 만들어진 모든 DOM 노드 및 인스턴스를 가리키도록 리액트 내부의 참조를 업데이트 한다.

그 다음 생명주기 개념이 있는 클래스형 컴포넌트에서 componentDidMount, componentDidUpdate 메서드를 호출하고 , 함수형 컴포넌트에서는 useLayoutEffect 훅을 호출한다.



여기서 중요한 사실을 알 수 있다

❗️ 리액트의 렌더링이 일어난다고 해서 무조건 DOM 업데이트가 일어나는 것은 아니다

렌더링을 수행햇은나 커밋단계까지 갈 필요가 없다면
즉 변경 사항을 계산 했는데 아무런 변경 사항이 감지 되지 않은다면 이 커밋단계는 생략될 수 있다.



렌더 단계 - 비동기 🆚 커밋단계 - 동기

렌더 단계는 단순히 컴포넌트의 변경 사항을 계산하고, 어떤 부분이 업데이트 되어야 하는지를 판단하는 단계이기 때문에 비동기적으로 동작하여 성능을 올릴 수 있다.
우선순위가 낮은 작업은 미루거나 작업을 여러 조각으로 나누어 수행할 수도, 중단했다가가 다시 시작할 수 도 있다.

그러나

커밋단계는 동기적으로 동작한다.
이 단계는 사용자에게 직접적으로 영향을 주기 때문에 동기적으로 dom을 업데이트 해야지만 사용자가 UI와 상호작용할때 일관된 결과를 얻을 수 있다.




✨ Referance


https://velog.io/@arthur/%EB%B2%88%EC%97%AD-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%8F%99%EC%9E%91%EC%9D%98-%EA%B1%B0%EC%9D%98-%EC%99%84%EB%B2%BD%ED%95%9C-%EA%B0%80%EC%9D%B4%EB%93%9C-A-Mostly-Complete-Guide-to-React-Rendering-Behavior

[서적] 모던 리액트 Deep Dive

profile
🤯 🎀 😎 🙉 🍡

1개의 댓글

comment-user-thumbnail
2024년 12월 15일

커밋단계에서는 실제 DOM에 반영되지만 브라우저 렌더링의 페인트 전이니, 화면에는 그려지지 않지만 실제 DOM에 반영은 됬다고 보면 되는걸까요?

답글 달기