흔히 렌더링이라고 하면 브라우저에서 html과 css를 기반으로 웹 페이지에 필요한 UI를 그리는 과정이다.
💡하지만 리액트의 렌더링이라고 하면 조금 다른 의미를 지닌다!
정확히는 브라우저 렌더링과 리액트 렌더링을 나눠 생각해야한다.

리액트 렌더링 : 브라우저가 렌더링에 필요한 DOM 트리를 만드는 과정
브라우저 렌더링 : HTML과 CSS 리소스를 기반으로 웹 페이지에 필요한 UI를 그리는 과정
렌더링이 일어나는 경우는 크게 두 가지이다.
최초 사용자가 처음 진입하면 브라우저에게 정보를 제공하기 위해 최초 렌더링이 진행
최초 렌더링이 아닌 리렌더링이 발생하는 경우
첫 번째로는 useState 두 번째 배열 원소인 setter 를 실행하는 경우입니다.
두 번째로는 useReducer 의 두 번째 배열 원소인 dispatch 를 실행하는 경우
세 번째로는 컴포넌트의 key props가 변경되는 경우입니다.
리액트에서 배열에 key를 왜 써야할까?
(리액트 파이버 내용은 뒤에 자세히 살펴볼 것이다.)
이때 key는 리렌더링이 발생하는 동안 형제 요소들 간의 동일한 요소를 식별하는 값이다동일한 자식 컴포넌트가 여러 개가 있는 경우 리렌더링이 발생하면 리액트 파이버 트리인 current 트리와 workInProgress 트리 사이에 어떤 컴포넌트의 변경인지 결정하는 값이 key이다!!
=> 렌더링이 발생할 때마다 key 값을 활용해 해당 컴포넌트 만을 강제로 리렌더링이 가능하다
네 번째로는 컴포넌트의 props 가 변경되는 경우입니다.
마지막은 부모 컴포넌트가 렌더링이 되는 경우입니다.
DOM에 따른 자식과 부모 관계를 가지는데 부모가 렌더링이 일어나면 그의 자식 컴포넌트는 반드시 리렌더링이 발생
과정을 자세히 살펴 보기 전!
DOM이란?
DOM은 Document Object Model의 약자로, 웹 페이지에 대한 인터페이스이다.
브라우저가 웹 페이지의 컨텐츠와 구조를 어떻게 보여줄 지에 대한 정보를 담는 객체 트리 구조로 표현자바스크립트에서 DOM을 조작할 때마다 브라우저 렌더링 엔진과 자바스크립트 엔진 사이에서 상호 작용이 발생
=> 상호작용이 발생하면 전체 페이지를 다시 그리려고 하기에 비용이 많이 드는 작업이며, 실행 성능에 큰 영향을 미칠 수 있다





브라우저 리소스가 가장 많이 소모되는 부분이 레이아웃과 페인트 과정이다.
두가지 상황을 가정해보자!
🧐처음 화면이 모두 그려진 후, 자바스크립트로 인해 화면 내 어떤 요소의 너비와 높이가 변경되는 상황
=> 브라우저는 해당 요소의 가로와 세로를 다시 계산하여 변경된 사이즈로 화면을 새로 그려야된다
이러한 작업을 리플로우라고 한다.
즉, 주요 렌더링 경로의 모든 단계를 모두 재실행함으로써, 브라우저 리소스를 많이 사용한다.
🧐이번에는 한 요소의 가로, 세로 같은 레이아웃 관련 속성이 아니라 글자 색이나 배경 색 등 색상 관련 속성이 변경되는 상황
이러한 작업을 리페인트라고 한다.
레이아웃 단계를 건너 뛰었기 때문에 리플로우 작업보다는 조금 더 빠르지만 거의 모든 브라우저 렌더링의 단계를 거치기에 리소스를 많이 사용한다.
따라서 앞의 두가지 상황과 같이 렌더 트리가 변경될 때마다 주요 렌더링 과정이 재실행되는 리플로우와 리페인트가 발생하기 때문에 브라우저 리소스를 많이 소모가 되어 브라우저 성능 문제를 야기할 수 있다.
리액트에서의 렌더링은 리액트 어플리케이션 트리 안에 있는 모든 컴포넌트들의 props, state 값을 기반으로 UI에 어떻게 구성하고 이를 바탕으로 어떤 DOM 결과를 브라우저에게 제공할 것인지를 계산하는 일련의 과정을 의미하며 총 3가지로 나뉜다.
첫 번째로 렌더링을 유발하는 단계로 createRoot의 실행 혹은 state 를 업데이트하게 되면 발생
두 번째는 렌더링으로 컴포넌트를 호출하는 단계이다.
createRoot로 렌더링이 발생했다면 루트 컴포넌트를 호출하고, state의 업데이트로 인한 렌더링이라면 해당 state 가 속해있는 컴포넌트를 호출한다.
마지막은 커밋 단계로 변경사항들을 실제 DOM에 적용하는 작업을 진행한다. 첫 커밋이라면 appendChild를 사용해서 스크린에 있는 모든 노드를 생성한다.
만약 첫 커밋이 아니라면 초소한의 작업을 통해 변경 사항만을 실제 DOM에 적용한다. 이 변경 사항은 렌더링 중에 계산된다.
V8엔진에서 브라우저 렌더링 과정은 앞에서 설명했듯이, 아래와 같다.
DOM트리, CSSOM 생성 => 렌더트리 생성 => 레이아웃 => 페인트 => 컴포지트
렌더 트리의 변경이 될 때마다 브라우저의 리소스가 많이 소모되는 레이아웃, 페인트 단계가 다시 재실행되는 리플로우 또는 리페인팅이 발생한다.
=> 따라서 렌더 트리 변경을 최소화하면 브라우저 성능을 최적화가 가능하다!
브라우저의 성능을 최적화하기 위한 것이 가상 DOM이다!
가상 DOM은 브라우저의 DOM이 아닌 리액트에서 관리하는 DOM이다.
웹에 표시할 DOM을 메모리에 저장해 변경에 대한 준비가 완료되었을 경우(가상 DOM과 재조정 (재조정은 뒤에서 자세히 살펴볼것이다))에 실제 DOM에 반영함으로써 렌더 트리 변경을 최소화한다
따라서 가상 DOM으로부터 페이지 내부의 DOM의 모든 변경 사항을 추적하는 것이 아닌 DOM의 결과물에만 초점을 둘 수 있게 되었다
그렇다면 이런 가상 DOM을 어떻게 관리할까?
리액트는 가상 DOM 트리 변화를 감지해 브라우저에게 변화 정보를 전달한다.
브라우저는 변화 정보를 담은 노드만을 렌더링함으로써 성능을 최적화할 수 있다.
그런데 리액트 팀은 React 16 버전부터 리액트 파이버를 추가함으로써 렌더링 프로세스에 변화를 주었다.
React 16 이전에는 Stack Reconciler 의 작동 방식은 동기적으로 이뤄졌으며, 모든 작업을 스택으로 처리했다.
예를 들어 <input type="text" /> 의 내용으로 자동 검색을 하는 UI를 생각해보자
사용자는 빠르게 검색어를 타이핑할 것이다.
이때 필요한 fetch 작업, 로딩 스피너, 자동 검색을 위한 UI 등 작업이 모두 스택에 쌓여 동기식으로 처리한다고 생각하면...
작업에 많은 시간이 소요되며, 최악의 경우에는 글자 입력에 지연이 생길 수 있다...!!
따라서 메인 스레드에 과부하가 걸릴 정도의 고연산 작업이라면 유저 경험을 심각하게 저해시킬 정도의 렌더링 문제가 유발될 수 있다!!
이런 기존 렌더링 스택의 비효율성을 타파하기 위해 리액트 팀은 파이버를 도입하게 되었다.
이전에 리액트 렌더링 프로세스는 DOM 결과를 브라우저에게 제공할 것인지 계산하는 과정이라고 말했다.
이러한 결과를 어떻게 리액트는 어떻게 계산할까?
실제 DOM을 추상화하여 만들어진 가상 DOM의 변화를 감지해서 변경 결과를 도출한다!!
=> state 나 props 가 변경되면 리액트는 이 변경을 감지하고 기존 가상 DOM트리와 변경된 가상 DOM트리를 비교한다.
이러한 알고리즘을 재조정 알고리즘으로 가상 DOM의 탐색 성능을 최적화했다.
재조정은 다음과 같은 특징을 갖고 있다.
이런 기존 렌더링 스택의 비효율성을 해결하기 위해 다음과 같은 일을 수행한다.
재조정 과정을 렌더링과 동시에 진행하지 않음으로써 리액트 파이버는 업데이트될 변경 사항에 우선 순위를 부여할 수 있게 되었다.(=> 증분 렌더링)
렌더링 작업을 점진적으로 처리할 수 있는 더 작고 우선순위가 지정된 청크(객체)로 가상 DOM을 업데이트하는 작업을 분할하는 방식으로 작동하는데 이를 위해서 재조정과 렌더링이 분리되었다.
=> 이 각 두단계가 곧 렌더 단계와 커밋 단계이다!!
렌더링 단계 1: 리액트는 UI에 나타나야할 모든 변경 사항들을 리스트로 저장한다. 이때 리액트는 언제든 작업을 중지할 수 있으며 다른 단계로 넘어갈 수도 있다.
렌더링 단계 2: 변경사항 리스트가 완성됐으면 리액트는 다음 단계에서 실행되어야할 변경 사항을 예약해놓는다.
커밋 단계 1: 리액트는 렌더링 단계 2에서 예약해 놓은 변경 사항 중 특정 사항만 렌더링 할 수 있게 지정할 수 있다.
커밋 단계 2: 커밋되면 React는 변경 사항을 렌더링하도록 브라우저에게 알린다.
증분 렌더링이 각 단계에서 어떻게 작동하는 지 한번 알아보자.

파이버는 자바스크립트 객체로서 파이버를 노드로 갖는 트리를 파이버 트리라고 한다 => 이것이 웹 환경에서 가상 DOM이다!!
리액트 파이버는 여러 요소가 있지만 type, child, sibling..등이 있다

파이버 트리는 리액트 내부에서 2개가 존재한다
리액트 파이버의 작업이 끝나면 리액트는 단순히 포인터만 변경해 workInProgress 트리를 현재 트리로 바꿔버린다(이를 더블 버퍼링이라고 하며 커밋 단계에서 수행된다.)
🧐 그렇다면 파이버와 파이버 트리가 어떤 식으로 작동하는 지 살펴보자

트리 빌드
이때 빌드는 새롭게 트리를 만드는 과정인데 이때 모든 파이버를 새롭게 만들는 것이 아닌 기존 존재하는 파이버에서 업데이트 된 정보만을 가지고 내부에서 처리파이버는 자바스크립트 객체이니까 객체를 매번 새롭게 만들지 않아 리소스 낭비를 덜어줄 수 있다!

가장 첫 번째로 성능을 향상시켰다. 재조정하는 동안 다른 작업을 중지하지 않기 때문에 React는 필요할 때마다 작업을 일시 중지하거나 렌더링을 시작할 수 있게됐다.
두 번째로 훨씬 깔끔한 방식으로 오류를 처리할 수 있게 됐다. 자바스크립트 런타임 오류가 발생할 때마다 흰색 화면을 표시하는 대신 Error Boundary를 설정하여 문제가 발생할 경우 백업 화면을 표시할 수 있게 됐다.
💡 리액트 파이버의 도입으로 Error Boundary, Suspense, React.Lazy, Fragements 그리고 Concurrency Mode가 가능해졌다.
리액트는 컴포넌트의 루트에서 부터 차근 차근 아래 쪽으로 내려 가면서 업데이트가 필요하다고 지정된 컴포넌트를 찾는다
해당 컴포넌트를 찾았으면 호출을 하면서 JSX문법으로 결과물을 저장합니다.
자바스크립트 런타임이 이해할 수 있도록 컴파일을 하여 자바스크립트 객체로 반환
=> 자바스크립트 객체는 브라우저의 UI 구조를 설명해주는 형태가 된다
이렇게 렌더링 프로세스가 실행되면서, 각 컴포넌트의 렌더링 결과물을 수집한다
리액트의 가상 DOM과 비교해 실제 DOM에 반영하기 위한 모든 변경사항을 수집한다 (리액트 파이버 ⇒ 재조정 알고리즘)
이런 과정이 거치면 변경사항들을 실제 DOM에 적용해 최종 결과물이 보이게 한다
리액트의 렌더링은 렌더 단계와 커밋 단계 두 단계로 분리되어 실행된다
렌더 단계
리액트 렌더링 프로세스를 통해 컴포넌트를 실행해 이 결과와 이전 가상 DOM과 비교하는 과정을 거쳐 변경이 필요한 컴포넌트를 체크하는 단계
이때 비교하는 것은 type, props, key 입니다. 셋 중 하나라도 변경 된 것이 있으면 변경이 필요한 컴포넌트로 체크
커밋 단계
렌더 단계의 변경 사항을 실제 DOM에 적용해 사용자에게 보여주는 작업
파이버는 가상 DOM과 실제 DOM을 비교하여 어디를 새롭게 렌더링 할 지를 결정하는 작업이 비동기적으로 수행이 된다
그렇게 변경 사항을 실제 브라우저 DOM에 적용하는 것은 동기적으로 일어나기 때문에 처리하는 작업이 많아 불완전하게 화면에 표시될 수 있습니다.
그래서 이러한 작업들은 가상에서 즉, 메모리 상에서 먼저 수행해서 최종적인 결과물만 실제 브라우저 DOM에 적용하게 되는 것입니다.