[Reat] 리액트의 렌더링 과정 (with 가상 DOM, 리액트 파이버)

hansoom·2024년 7월 3일

React

목록 보기
2/2
post-thumbnail

1. 렌더링

흔히 렌더링이라고 하면 브라우저에서 html과 css를 기반으로 웹 페이지에 필요한 UI를 그리는 과정이다.

💡하지만 리액트의 렌더링이라고 하면 조금 다른 의미를 지닌다!

정확히는 브라우저 렌더링리액트 렌더링을 나눠 생각해야한다.

  • 리액트 렌더링 : 브라우저가 렌더링에 필요한 DOM 트리를 만드는 과정

  • 브라우저 렌더링 : HTML과 CSS 리소스를 기반으로 웹 페이지에 필요한 UI를 그리는 과정

1-1. 리액트의 렌더링은 언제 일어날까?

렌더링이 일어나는 경우는 크게 두 가지이다.

  • 최초 사용자가 처음 진입하면 브라우저에게 정보를 제공하기 위해 최초 렌더링이 진행

  • 최초 렌더링이 아닌 리렌더링이 발생하는 경우

    • 첫 번째로는 useState 두 번째 배열 원소인 setter 를 실행하는 경우입니다.

    • 두 번째로는 useReducer 의 두 번째 배열 원소인 dispatch 를 실행하는 경우

    • 세 번째로는 컴포넌트의 key props가 변경되는 경우입니다.

    리액트에서 배열에 key를 왜 써야할까?
    (리액트 파이버 내용은 뒤에 자세히 살펴볼 것이다.)
    이때 key는 리렌더링이 발생하는 동안 형제 요소들 간의 동일한 요소를 식별하는 값이다

    동일한 자식 컴포넌트가 여러 개가 있는 경우 리렌더링이 발생하면 리액트 파이버 트리인 current 트리와 workInProgress 트리 사이에 어떤 컴포넌트의 변경인지 결정하는 값이 key이다!!
    => 렌더링이 발생할 때마다 key 값을 활용해 해당 컴포넌트 만을 강제로 리렌더링이 가능하다

    • 네 번째로는 컴포넌트의 props 가 변경되는 경우입니다.

    • 마지막은 부모 컴포넌트가 렌더링이 되는 경우입니다.
      DOM에 따른 자식과 부모 관계를 가지는데 부모가 렌더링이 일어나면 그의 자식 컴포넌트는 반드시 리렌더링이 발생


이제 브라우저 렌더링과 리액트 렌더링 수행하는 과정을 자세히 살펴보자

2. 브라우저 렌더링 과정

과정을 자세히 살펴 보기 전!

DOM이란?

DOM은 Document Object Model의 약자로, 웹 페이지에 대한 인터페이스이다.
브라우저가 웹 페이지의 컨텐츠와 구조를 어떻게 보여줄 지에 대한 정보를 담는 객체 트리 구조로 표현

자바스크립트에서 DOM을 조작할 때마다 브라우저 렌더링 엔진과 자바스크립트 엔진 사이에서 상호 작용이 발생
=> 상호작용이 발생하면 전체 페이지를 다시 그리려고 하기에 비용이 많이 드는 작업이며, 실행 성능에 큰 영향을 미칠 수 있다

  1. 브라우저가 웹 페이지 접근 요청을 받으면 해당 url을 방문해 HTML 파일 다운로드

  1. 브라우저 렌더링 엔진은 HTML을 파싱해 DOM노드로 구성된 트리(DOM)을 생성
  2. css 코드에 대해서도 다운로드 받아 css 또한 파싱해 css 노드로 구성된 트리(CSSOM)도 생성

  1. 브라우저는 해당 트리에 대한 노드를 순회하는데 css 스타일 정보를 노드에 적용시켜 렌더트리를 생성

  1. 레이아웃 단계로 화면 구성 요소의 위치나 크기를 계산해 해당 위치에 요소를 배치하는 작업을 수행

  1. 화면에 배치된 요소에 색을 채워 넣는 작업이 페인트 단계를 수행
    (브라우저는 효율적인 페인트 과정을 위해 구성 요소를 여러 개의 레이어로 나눠서 작업)
  2. 페인트 단계에서의 여러 개의 레이어를 하나로 합성하는 작업

2-1. 리플로우와 리페인트

브라우저 리소스가 가장 많이 소모되는 부분이 레이아웃과 페인트 과정이다.

두가지 상황을 가정해보자!

🧐처음 화면이 모두 그려진 후, 자바스크립트로 인해 화면 내 어떤 요소의 너비와 높이가 변경되는 상황

=> 브라우저는 해당 요소의 가로와 세로를 다시 계산하여 변경된 사이즈로 화면을 새로 그려야된다

  • 요소의 스타일이 변경됐으니, CSSOM을 새로 만들어야한다.
  • 변경된 CSSOM을 이용해 새로운 렌더 트리를 만든다.
  • 요소의 가로와 세로를 변경했으니, 레이아웃 단계에서 요소의 크기와 위치를 다시 고려
  • 변경된 화면 구성에 알맞게 색을 칠한다.
  • 분할된 레이어를 하나로 합성한다.

이러한 작업을 리플로우라고 한다.
즉, 주요 렌더링 경로의 모든 단계를 모두 재실행함으로써, 브라우저 리소스를 많이 사용한다.

🧐이번에는 한 요소의 가로, 세로 같은 레이아웃 관련 속성이 아니라 글자 색이나 배경 색 등 색상 관련 속성이 변경되는 상황

  • 스타일 속성이 변경됐으니, CSSOM을 새로 만들어야한다.
  • 변경된 CSSOM을 이용해 새로운 렌더 트리를 만든다.
  • 레이아웃 단계는 실행되지X (요소의 위치나 크기에 영향을 주지 않기 때문에)
  • 변경된 화면 구성에 알맞게 색을 칠한다.
  • 분할된 레이어를 하나로 합성한다.

이러한 작업을 리페인트라고 한다.
레이아웃 단계를 건너 뛰었기 때문에 리플로우 작업보다는 조금 더 빠르지만 거의 모든 브라우저 렌더링의 단계를 거치기에 리소스를 많이 사용한다.

따라서 앞의 두가지 상황과 같이 렌더 트리가 변경될 때마다 주요 렌더링 과정이 재실행되는 리플로우와 리페인트가 발생하기 때문에 브라우저 리소스를 많이 소모가 되어 브라우저 성능 문제를 야기할 수 있다.

3. 리액트 렌더링

리액트에서의 렌더링은 리액트 어플리케이션 트리 안에 있는 모든 컴포넌트들의 props, state 값을 기반으로 UI에 어떻게 구성하고 이를 바탕으로 어떤 DOM 결과를 브라우저에게 제공할 것인지를 계산하는 일련의 과정을 의미하며 총 3가지로 나뉜다.

첫 번째로 렌더링을 유발하는 단계로 createRoot의 실행 혹은 state 를 업데이트하게 되면 발생

두 번째는 렌더링으로 컴포넌트를 호출하는 단계이다.
createRoot로 렌더링이 발생했다면 루트 컴포넌트를 호출하고, state의 업데이트로 인한 렌더링이라면 해당 state 가 속해있는 컴포넌트를 호출한다.

마지막은 커밋 단계로 변경사항들을 실제 DOM에 적용하는 작업을 진행한다. 첫 커밋이라면 appendChild를 사용해서 스크린에 있는 모든 노드를 생성한다.
만약 첫 커밋이 아니라면 초소한의 작업을 통해 변경 사항만을 실제 DOM에 적용한다. 이 변경 사항은 렌더링 중에 계산된다.

3-1. 가상 DOM의 등장 배경

V8엔진에서 브라우저 렌더링 과정은 앞에서 설명했듯이, 아래와 같다.

DOM트리, CSSOM 생성 => 렌더트리 생성 => 레이아웃 => 페인트 => 컴포지트

렌더 트리의 변경이 될 때마다 브라우저의 리소스가 많이 소모되는 레이아웃, 페인트 단계가 다시 재실행되는 리플로우 또는 리페인팅이 발생한다.

=> 따라서 렌더 트리 변경을 최소화하면 브라우저 성능을 최적화가 가능하다!

브라우저의 성능을 최적화하기 위한 것이 가상 DOM이다!
가상 DOM은 브라우저의 DOM이 아닌 리액트에서 관리하는 DOM이다.

웹에 표시할 DOM을 메모리에 저장해 변경에 대한 준비가 완료되었을 경우(가상 DOM과 재조정 (재조정은 뒤에서 자세히 살펴볼것이다))에 실제 DOM에 반영함으로써 렌더 트리 변경을 최소화한다

따라서 가상 DOM으로부터 페이지 내부의 DOM의 모든 변경 사항을 추적하는 것이 아닌 DOM의 결과물에만 초점을 둘 수 있게 되었다

그렇다면 이런 가상 DOM을 어떻게 관리할까?

4. 가상 DOM을 위한 아키텍처, 리액트 파이버

리액트는 가상 DOM 트리 변화를 감지해 브라우저에게 변화 정보를 전달한다.
브라우저는 변화 정보를 담은 노드만을 렌더링함으로써 성능을 최적화할 수 있다.

그런데 리액트 팀은 React 16 버전부터 리액트 파이버를 추가함으로써 렌더링 프로세스에 변화를 주었다.

4-1. 리액트가 왜 파이버를 도입했을까?

React 16 이전에는 Stack Reconciler 의 작동 방식은 동기적으로 이뤄졌으며, 모든 작업을 스택으로 처리했다.

예를 들어 <input type="text" /> 의 내용으로 자동 검색을 하는 UI를 생각해보자

사용자는 빠르게 검색어를 타이핑할 것이다.
이때 필요한 fetch 작업, 로딩 스피너, 자동 검색을 위한 UI 등 작업이 모두 스택에 쌓여 동기식으로 처리한다고 생각하면...

작업에 많은 시간이 소요되며, 최악의 경우에는 글자 입력에 지연이 생길 수 있다...!!

따라서 메인 스레드에 과부하가 걸릴 정도의 고연산 작업이라면 유저 경험을 심각하게 저해시킬 정도의 렌더링 문제가 유발될 수 있다!!

이런 기존 렌더링 스택의 비효율성을 타파하기 위해 리액트 팀은 파이버를 도입하게 되었다.

4-2. 리액트 파이버의 역할

4-2-1. 재조정

이전에 리액트 렌더링 프로세스는 DOM 결과를 브라우저에게 제공할 것인지 계산하는 과정이라고 말했다.

이러한 결과를 어떻게 리액트는 어떻게 계산할까?

실제 DOM을 추상화하여 만들어진 가상 DOM의 변화를 감지해서 변경 결과를 도출한다!!

=> state 나 props 가 변경되면 리액트는 이 변경을 감지하고 기존 가상 DOM트리와 변경된 가상 DOM트리를 비교한다.
이러한 알고리즘을 재조정 알고리즘으로 가상 DOM의 탐색 성능을 최적화했다.

재조정은 다음과 같은 특징을 갖고 있다.

  • 컴포넌트 유형이 다르면 diff를 진행하지 않고 트리 자체를 새로운 트리로 대체한다.
  • 리스트의 diff는 key 를 기준으로 수행된다. 이때 key는 안정적이고 예측 가능하며 유니크한 값이어야 한다.

4-2-2. 리액트 파이버

이런 기존 렌더링 스택의 비효율성을 해결하기 위해 다음과 같은 일을 수행한다.

  • 작업을 작은 단위로 분할하고 쪼개 각기 역할 마다 다른 우선 순위를 부여할 수 있는 기능
  • 작업 일시 중지하고 나중에 다시 수행할 수 있는 기능
  • 이전에 완료된 연산을 재사용할 수 있는 기능
  • 필요가 없어진 연산을 중간에 취소하는 기능

재조정 과정을 렌더링과 동시에 진행하지 않음으로써 리액트 파이버는 업데이트될 변경 사항에 우선 순위를 부여할 수 있게 되었다.(=> 증분 렌더링)

렌더링 작업을 점진적으로 처리할 수 있는 더 작고 우선순위가 지정된 청크(객체)로 가상 DOM을 업데이트하는 작업을 분할하는 방식으로 작동하는데 이를 위해서 재조정과 렌더링이 분리되었다.

=> 이 각 두단계가 곧 렌더 단계와 커밋 단계이다!!

  • 렌더링 단계 1: 리액트는 UI에 나타나야할 모든 변경 사항들을 리스트로 저장한다. 이때 리액트는 언제든 작업을 중지할 수 있으며 다른 단계로 넘어갈 수도 있다.

  • 렌더링 단계 2: 변경사항 리스트가 완성됐으면 리액트는 다음 단계에서 실행되어야할 변경 사항을 예약해놓는다.

  • 커밋 단계 1: 리액트는 렌더링 단계 2에서 예약해 놓은 변경 사항 중 특정 사항만 렌더링 할 수 있게 지정할 수 있다.

  • 커밋 단계 2: 커밋되면 React는 변경 사항을 렌더링하도록 브라우저에게 알린다.

4-3. 리액트 파이버의 작동 원리

증분 렌더링이 각 단계에서 어떻게 작동하는 지 한번 알아보자.

파이버는 자바스크립트 객체로서 파이버를 노드로 갖는 트리를 파이버 트리라고 한다 => 이것이 웹 환경에서 가상 DOM이다!!

리액트 파이버는 여러 요소가 있지만 type, child, sibling..등이 있다

  • type: 생성된 function Component
  • child: 해당 Component의 가장 왼쪽에 있는 자식 노드
  • sibling: 해당 Component의 형제 노드

리액트 파이버 트리

파이버 트리는 리액트 내부에서 2개가 존재한다

  • current 트리 : 현재 모습을 담은 파이버 트리
  • workInProgress 트리 : 작업 중인 상태를 나타내는 파이버 트리

리액트 파이버의 작업이 끝나면 리액트는 단순히 포인터만 변경해 workInProgress 트리를 현재 트리로 바꿔버린다(이를 더블 버퍼링이라고 하며 커밋 단계에서 수행된다.)

🧐 그렇다면 파이버와 파이버 트리가 어떤 식으로 작동하는 지 살펴보자

  • setState 로 인해 업데이트가 발생
  • 현재 UI 렌더링을 위해 존재하는 current 트리를 기준으로 시작
  • setState 로 인한 업데이트가 발생할 경우 리액트에서 새로 받은 데이터로 workInProgress 트리를 다시 빌드

트리 빌드
이때 빌드는 새롭게 트리를 만드는 과정인데 이때 모든 파이버를 새롭게 만들는 것이 아닌 기존 존재하는 파이버에서 업데이트 된 정보만을 가지고 내부에서 처리

파이버는 자바스크립트 객체이니까 객체를 매번 새롭게 만들지 않아 리소스 낭비를 덜어줄 수 있다!

  • 빌드가 끝나면 다음 렌더링에 해당 workInProgress 트리를 사용

  • workInProgress 트리가 UI에 최종적으로 렌더링이 반영이 완료가 되면 current 가 workInProgress 트리로 대체된다

4-4. 리액트 파이버의 도입으로 얻은 효과

가장 첫 번째로 성능을 향상시켰다. 재조정하는 동안 다른 작업을 중지하지 않기 때문에 React는 필요할 때마다 작업을 일시 중지하거나 렌더링을 시작할 수 있게됐다.

두 번째로 훨씬 깔끔한 방식으로 오류를 처리할 수 있게 됐다. 자바스크립트 런타임 오류가 발생할 때마다 흰색 화면을 표시하는 대신 Error Boundary를 설정하여 문제가 발생할 경우 백업 화면을 표시할 수 있게 됐다.

💡 리액트 파이버의 도입으로 Error Boundary, Suspense, React.Lazy, Fragements 그리고 Concurrency Mode가 가능해졌다.

5. 리액트 렌더링 프로세스

5-1. 리액트의 렌더링 프로세스

  1. 리액트는 컴포넌트의 루트에서 부터 차근 차근 아래 쪽으로 내려 가면서 업데이트가 필요하다고 지정된 컴포넌트를 찾는다

  2. 해당 컴포넌트를 찾았으면 호출을 하면서 JSX문법으로 결과물을 저장합니다.

  3. 자바스크립트 런타임이 이해할 수 있도록 컴파일을 하여 자바스크립트 객체로 반환
    => 자바스크립트 객체는 브라우저의 UI 구조를 설명해주는 형태가 된다

  4. 이렇게 렌더링 프로세스가 실행되면서, 각 컴포넌트의 렌더링 결과물을 수집한다

  5. 리액트의 가상 DOM과 비교해 실제 DOM에 반영하기 위한 모든 변경사항을 수집한다 (리액트 파이버 ⇒ 재조정 알고리즘)

이런 과정이 거치면 변경사항들을 실제 DOM에 적용해 최종 결과물이 보이게 한다

리액트의 렌더링은 렌더 단계와 커밋 단계 두 단계로 분리되어 실행된다

5-2. 렌더와 커밋

  • 렌더 단계
    리액트 렌더링 프로세스를 통해 컴포넌트를 실행해 이 결과와 이전 가상 DOM과 비교하는 과정을 거쳐 변경이 필요한 컴포넌트를 체크하는 단계

    이때 비교하는 것은 type, props, key 입니다. 셋 중 하나라도 변경 된 것이 있으면 변경이 필요한 컴포넌트로 체크

  • 커밋 단계
    렌더 단계의 변경 사항을 실제 DOM에 적용해 사용자에게 보여주는 작업

파이버는 가상 DOM과 실제 DOM을 비교하여 어디를 새롭게 렌더링 할 지를 결정하는 작업이 비동기적으로 수행이 된다

그렇게 변경 사항을 실제 브라우저 DOM에 적용하는 것은 동기적으로 일어나기 때문에 처리하는 작업이 많아 불완전하게 화면에 표시될 수 있습니다.

그래서 이러한 작업들은 가상에서 즉, 메모리 상에서 먼저 수행해서 최종적인 결과물만 실제 브라우저 DOM에 적용하게 되는 것입니다.


요약

  • 브라우저가 HTML을 다운로드하고 초기 DOM을 구축하며 CSSOM을 생성하여 초기 렌더링을 시작 (브라우저 렌더링)
  • state나 props의 변경이 되면 파이버가 실행되어 실제 DOM과 가상 DOM을 비교하며 변경이 필요한 컴포넌트들을 수집 (리액트 렌더링 : 렌더)
  • 작업이 마무리되면 변경 사항은 실제 DOM에 변경 사항들을 적용 (리액트 렌더링 : 커밋)
  • React에 의해 업데이트된 실제 DOM이 브라우저에 의해 다시 렌더링되어 화면에 반영 (브라우저 렌더링)

참고

https://medium.com/@sht02048/%EC%83%9D%EA%B0%81%EB%B3%B4%EB%8B%A4-%EB%8D%94-%EC%84%AC%EC%84%B8%ED%95%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B3%BC%EC%A0%95-feat-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%8C%8C%EC%9D%B4%EB%B2%84-44075084381a
모던 리액트 Deep Dive

0개의 댓글