본문 바로가기

3주차

DOM과 React 랜더링

안녕하세요. Web YB 김고은입니다.

이번 주제는 React의 랜더링 방식과,  DOM에 대해서 다뤄보고자 합니다. 

특히, react의 랜더링 방식을, JavaScript와 비교하여 왜 요즘의 FE 기술로 react를 사용하는지 살펴보고자 합니다. 

 

1. 브라우저의 랜더링 과정 

우선 두 방식을 살펴보기 전에 프론트엔드 개발을 공부하고 있는 만큼 가장 기본인 브라우저 랜더링 과정을 정리하자면 다음과 같다.

`HTML 파싱 => DOM 트리 생성` ➡️ `CSS 파싱 => CSSOM 트리 생성` ➡️ `DOM과 CSSOM 결합을 통해 =>  랜더 트리 생성` ➡️
`JS 파싱 => AST => 바이트코드로 변환 후 실행 (필요시 DOM이나 CSSOM로 변환되고 랜더트리에 반영)` ➡️
`레이아웃 (요소의 위치와 크기 계산)` ➡️ `페인트 (화면에 그래픽 요소 그리기)`

 

1-1 HTML 파싱 → DOM 트리 생성

가장 먼저 브라우저는 HTML 문서를 위에서부터 시작해서 쭉 읽어내려가며 요소를 파싱하여 DOM 트리를 생성하는데, 이떄의 트리 구조는 계층 트리 구조로 요소 간의 부모-자식 관계를 포함한다. 

 

1-2 CSS 파싱 → CSSOM 트리 생성

다음으로 HTML 파싱과 동시에, 브라우저는 CSS 파일을 다운로드하고 파싱하여 CSSOM 트리를 생성하는데, 이 안에는 스타일 정보를 담고 있다. 

 

1-3 랜더 트리 생성

이후 DOM 트리와 CSSOM 트리를 결합하여 랜더 트리를 만드는데, 이는 실제 화면에 표시될 요소들로 구성되어 있다!

(단, display: none은 제외)

 

1-4 JavaScript 파싱 및 실행

브라우저는 JavaScript 코드를 파싱하고, 이를 토대로 AST(추상 구문 트리) 를 생성하여, 이 AST를 최종적으로 컴퓨터가 읽을 수 있는  코드인 바이트코드로 변환하여 실행한다. 이때, 실행된 JS 는 DOM 혹은 CSSOM을 조작할 수 있고, 이러한 변경 사항은 랜더 트리에 반영되게 된다. (바이트코드 실행 이후, DOM을 업데이트하는 부분에 대해서는 2번 파트에서 작성해보겠다!)

 

1-5 레이아웃 (리플로우)

랜더트리가 완성되면 브라우저는 요소들의 위치와 크기를 계산하는 레이아웃 단계를 수행한다. 

이때 요소의 좌표 배치가 확정되는데, 브라우저가 요소들의 위치와 크기를 화면에 맞게 조정하는 리플로우 과정을 수행하기도 한다.

 

1-6 페인트

마지막으로, 레이아웃을 마치면, 각 요소의 스타일이 그려지고 컴포지팅 과정을 통해 모든 그래픽 요소가 결합되어 화면에 최종적으로 나타나게 된다.

 


2  JS의 랜더링 (JS의 DOM 조작 )

 

JS 파싱 후, AST 생성 그리고 바이트 코드의 실행까지 거치고 나서 JS 는 DOM과 CSSOM을 직접적으로 조작할 수 있다고 앞서 설명한 바 있다. 이때, JS 에 의해 DOM이나 CSSOM이 변경되면 브라우저는 요 변경 사항을 기반으로 다시 레이아웃페인트 과정을 수행한다.

 

특히, DOM 조작에 있어서, 설명을 해보자면 JS는 실제 DOM에 직접적으로 접근하여 요소를 조작한다. 

( Vanilla JS와 jQuery가 여기에 해당한다!)

// DOM 요소를 선택하여 수정 
const element = document.getElementById("example");
element.innerText = "웨비가 최고야";
element.style.color = "blue";

// DOM 요소 추가 및 삭제
const newElement = document.createElement("div");
newElement.innerText = "New Element";
document.body.appendChild(newElement); // 추가
document.body.removeChild(newElement); // 삭제

 

다음과 같이 DOM을 직접적으로 조작할떄마다 브라우저는 DOM 트리를 새롭게 업데이트 해야 하고, 리플로우(다시 레이아웃을 진행)과 페인트 과정을 반복할 가능성이 크기 때문에 성능적으로 좋지 못하다 (아무래도 재랜더링 시간이 더 길어질듯 )

DOM 업데이트 → 리플로우 → 리페인트의 반복

 

따라서, 이러한 성능적 단점을 핸들링하기 위해서, 업데이트 효율을 높여야 할 필요가 있는데 바로 수동으로 업데이트를 관리하는 전략이다.

DOM 변경을 한번에 모아서 조작한다던지, 변경 사항이 많을 경우 일정 시간마다 업데이트 하는 방법을 고려할 수 있다.

 

 

3  React 랜더링 (가상 DOM 조작 )

이때 React는 직접적으로 DOM을 조작하지 않는다는 점에서 JS와 차별점을 가진다. 

React는 Virtual DOM 을 통해, DOM 조작을 간접적으로 진행한다.

 

Virtual DOM 이 뭔데요? 

Virtual DOM 메모리 내에서 실제 DOM의 가벼운 복사본!! 🖨️

HTML 문서의 DOM 구조와 동일하게 요소들을 계층 트리로 나타내어 내용은 완전히 같지만, 메모리 상에만 존재하기 때문에 가볍고 빠르다! 이때 메모리라고 함은, JavaScript 엔진의 힙 메모리에 만들어진다고 한다. 

 

 

1️⃣ 아무튼 React 이 처음 로드될때, 실제 DOM이 아닌  Virtual DOM 에 컴포넌트 트리가 랜더링되고, 이후에 Virtual DOM 이 메모리 상에서 초기상태를 나타낸다.

2️⃣ 이후 상태(state)나 속성(props)이 변경되면, React는 새로운 상태를 반영한 새로운  Virtual DOM 트리를 생성하게 되는데

⭐️이때 기존  Virtual DOM 과 <-> 새로운  Virtual DOM  을 Diffing 알고리즘을 통해 비교하여 변경 사항을 파악한다. ⭐️

3️⃣ 이에 따라, 변경된 부분만을 실제 DOM 에 반영하기 떄문에 최소한의 업데이트만을 수행한다는 점에서 성능이 최적화 된다.

 

Diffing 알고리즘 더 궁금해 

📍알고리즘 주요 단계 

 

(1) 트리비교 -> (2) 동일 타입 노드 비교 -> (3) 재귀적 비교 

 

트리비교 

react 에서는 부모 노드부터 자식 노드로 내려가며 같은 계층끼리 서로를 비교하게 되는데, 이떄 부모 요소가 변경된 경우 하위 요소 전체가 새로운 부분으로 간주된다. 

 

동일한 타입의 노드 비교 

동일한 타입의 요소가 발견되면 속성 비교를 수행하여 달라진 속성만을 업데이트하게 된다. 

 

재귀적 비교

노드 비교시, 자식 노드가 있으면 재귀적으로 자식노드에 대해서 같은 방식의 비교를 수행한다고 한다. 

 

📍알고리즘 기본 원리와 전제 

전제 1: 다른 타입의 요소는 다른 트리로 간주한다.
전제 2: 같은 타입의 DOM 노드는 속성만 비교하여 변경한다.
전제 3: 리스트 노드에서의 변경은 고유한 key로 구분한다.

 

전제 1: 

 노드의 타입이 달라지면, React는 해당 노드를 재사용하지 않고 새롭게 생성

  ex) <div>가 <p>로 변경되면 <div> 전체를 삭제하고 <p>를 다시 생성

 

전제2: 같은 타입 요소 발견시, 속성만 빠르게 비교하고 업데이트 (ex. class 속성 )

 

전제3:  li 요소에서 순서가 바뀌거나 항목 추가가 이루어지는 경우, key를 통해서 빠르게 고유성을 판별하고 식별한다. 

하지만, key 가 없으면 성능이 저하되기도 하고, 예상치 못하게 작동하는 경우에 있어서 주의해야 한다. 

그래서 li에 key 값 안 적어주면 콘솔에 오류가 나왔던 이유이다!