[React] Virtual DOM과 브라우저의 렌더링 과정

IN DUCK KANG·2021년 5월 20일
11

React

목록 보기
3/5
post-custom-banner

리액트의 주요 특징 중 하나는 가상의 DOM인 Virtual DOM을 사용한다는 것인데, 이를 통해 개발의 편의성(DOM을 직접 제어X)과 성능(배치 처리로 DOM 변경)이 개선되었다.

DOM 이란?

브라우저는 문자열 형식의 HTML을 곧바로 이해할 수 없기 때문에, 브라우저가 이해하고 활용할 수 있는 구조로의 변환이 필요하다.
DOM(Document Object Model)은 브라우저 렌더링 엔진의 HTML parser에 의해 생성된 '트리' 구조의 Node 객체 모델이다. DOM의 목적은 JS를 사용하여 문서에 대한 추가, 삭제, 이벤트 처리 등을 처리하는 인터페이스를 제공하는 것이다.

브라우저의 렌더링 과정

1. DOM 트리 생성

HTML을 파싱하여 DOM 객체로 이뤄진 DOM 트리 생성

2. CSSOM(CSS Object Model) 트리 생성

CSS parser는 inline style과 CSS 코드를 파싱 하여 CSSOM 트리을 생성

3. 렌더 트리 생성

DOMCSSOM의 정보를 바탕으로, 실제로 브라우저의 화면에 노출되어야 하는 노드들에 대한 정보인 렌더 트리 생성

실제로 브라우저의 화면에 노출'된다는 것은 display: none 인 보이지 않는 노드들은 렌더 트리를 생성할 때 제외한다는 것을 의미한다. 반면, 공간을 차지하는 visibility: hidden 속성은 렌더 트리에 포함된다.

4. Reflow (Layout)

렌더 트리의 각 노드들이 브라우저의 뷰포트(Viewport) 내에서 어느 위치에 어떤 크기로 배치되어야 하는지에 대한 정보를 계산한다.
Layout 단계를 통해 %, vh, vw 같은 상대적인 속성이 px 단위로 변환된다.

뷰포트(Viewport)는 그래픽이 표시되는 브라우저의 영역, 크기로 디스플레이의 크기, 브라우저 창의 크기에 따라 달라진다. viewport의 크기에 영향을 받는 %, vh, vw와 같은 속성은 viewport 크기가 달라질 경우 매번 계산을 다시해야 한다.

5. Repaint (Paint)

렌더 트리의 각 노드들을 모니터에 실제 픽셀로 그리는 단계.

Reflow가 발생하면 Paint는 반드시 수행되어야 한다.
하지만 Reflow가 발생했을 경우에는 Paint가 발생하는 것은 아니다. background-color, visibility 같이 레이아웃에는 영향을 주지 않는 스타일 속성만이 변경되었을 때는 Reflow를 수행할 필요가 없기 때문에 Paint 과정만 수행된다.

Tip) Layout과 Paint 가 모두 발생하지 않는 속성

transform, opacitiy, cursor, orphans, perspective 등이 해당됩니다. 이러한 속성들은 연산이 절대적으로 줄어들기 때문에 'Paint Only' 속성보다도 렌더링 속도가 더 빠르기 때문에, 가능하면 Repaint, Reflow가 일어나지 않는 속성을 사용하는 것을 권장한다.

브라우저의 렌더링 엔진 별로 CSS 속성 처리 단계가 다를 수 있는데, CSS Triggers 에서 css 속성을 처리할 때 Layout, Paint 를 실행하는지 여부를 확인할 수 있다.

브라우저에서 성능 저하가 발생하는 원인

위의 과정에서 알 수 있듯이, DOM을 수정할 때마다 Render Tree의 생성부터 Reflow, Repaint의 과정을 다시 수행해야 한다. DOM 자체의 수정은 빠르지만, 브라우저가 수행해야하는 이후의 과정이 느리다.

즉, 성능 저하의 주요 원인은 DOM을 수정할 때 발생하는 Reflow, Repaint 과정에 있다. Reflow가 빈번하게 발생하는 경우 브라우저에서는 성능저하가 발생하며, 웹 페이지의 DOM이 복잡하게 구성되어 있고 CSS가 많이 적용된 사이트일수록 더욱 심해진다.

Virtual DOM

DOM을 수정하는 일은 수반되는 비용이 크기 때문에, 성능저하를 최소화하기 위해서는 결국 DOM을 최소한으로 수정해야한다. 이러한 문제점을 해결하기 위해 Virtual DOM이 등장하게 되었다.

Virtual DOM은 실제 DOM의 구조와 비슷한, React 객체의 트리다. 개발자는 직접 DOM을 수정하지 않고 Virtual DOM을 제어하면 React에서 적절하게 Virtual DOM을 DOM에 반영하는 작업을 한다. Virtual DOM 의 장점 중 하나는 직접 DOM을 조작하지 않아도 된다는 점이다. 웹은 점점 더 복잡해져 가는데 수백, 수천개의 DOM을 직접 관리, 조작하는 과정은 복잡하고, 실수가 발생할 가능성도 높아지게 된다. Virtual DOM은 이러한 복잡한 과정들을 자동화, 추상화해 준다는 장점이 있다.

다른 장점은 DOM 의 update를 Batch로 처리로 실제 DOM의 리렌더링 연산을 최소화 할 수 있다는 점이다. 즉, 연쇄적으로 Reflow, Repaint가 발생하는 것을 줄이고, 필요한 연산을 한번에 묶어서 처리하게 전달하게 된다.

React는 특정 부분이 리렌더링 되어야 할 때 그 부분에 해당하는
1. Virtual DOM 트리를 메모리에 새로 생성한다.
2. 그리고 이전 Virtual DOM 트리와 O(n)의 휴리스틱 알고리즘으로 비교하여 차이점을 파악한다.
3. 그리고 그 차이점들을 하나로 모아서 실제 DOM에 전달한다.

이로 인해 실제 DOM의 리렌더링 연산(리플로우, 리페인트)은 단 한 번만 일어나게 되어, 큰 성능의 이득을 얻게 된다. 물론 vanilla js로도 이러한 batch update를 구현 할 수는 있겠지만, 이를 직접 구현하여 관리하는 것은 비효율적이다.

디자인패턴의 Model에 해당하는 state(상태)가 많아질수록 이를 추척하고 변경된 DOM을 찾아서 수정하는 것은 점점 복잡해진다. 따라서 state가 변경되었을 때, 어느 부분이 변경되었는지 추적하는 것 보다 Virtual DOM을 새로 생성하여 비교하는 것이 효율적이다.

Reconciliation (재조정;레컨실리에이션)

전체 DOM 트리를 탐색하고 비교하는 일반적인 알고리즘은 O(n^3)의 아주 느린 복잡도를 갖는다. 따라서 React는 두 가지 가정 아래에서 복잡도를 O(n)에 근사한 휴리스틱 알고리즘을 구현했다.

Reconciliation 두 가지 가정

  1. 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.(엘리먼트가 다르면 비교하지 않는다)

  2. 개발자가 key prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해 줄 수 있다. (key가 지정되어 있으면 key가 같은 노드끼리 비교를 한다)

Diff 방식

Level By Level
트리를 비교할 때 동일한 level의 node들끼리만 비교를 한다.

같은 위치에서 엘리먼트의 타입이 다른 경우
ex) div 태그가 span 태그로 바뀐경우
1. 기존 트리를 제거 후 새로운 트리 만든다.
2. 기존 트리 제거시 트리 내부의 엘리먼트/컴포넌트들은 모두 제거한다.
3. 새로운 트리를 만들 때 내부 엘리먼트/컴포넌트들도 모두 새로 만든다.

같은 위치에서 엘리먼트가 DOM을 표현하고, 그 타입이 같은 경우
ex) class가 변경된 경우
1. 엘리먼트의 attributes를 비교한다.
2. 변경된 attributes만 업데이트한다.
3. 자식 엘리먼트들에 diff 알고리즘을 재귀적으로 적용한다.

같은 위치에서 엘리먼트가 컴포넌트이고, 그 타입이 같은 경우
ex) <Item price=100 /> -> <Item price=200 />

  1. 컴포넌트 인스턴스 자체는 변하지 않는다. (컴포넌트의 state 유지)
  2. 컴포넌트 인스턴스의 업데이트 전 라이프 사이클 메서드들이 호출되며 props가 업데이트된다.
  3. render()를 호출하고, 컴포넌트의 이전 엘리먼트 트리와 다음 엘리먼트 트리에 대해 diff 알고리즘을 재귀적으로 적용한다.

자식 노드에 대한 재귀적 처리
DOM 노드의 자식들을 재귀적으로 처리할 때, React는 기본적으로 동시에 두 리스트를 순회하고 차이점이 있으면 변경을 생성한다. 이 때 비교하는 대상은 단순히 first-child to last-child로 비교한다.

// before
<ul>
  <li>ItemA</li>  //비교대상1
  <li>ItemB</li>  //비교대상2
</ul>

// after
<ul>
  <li>ItemA</li>  //비교대상1 (ItemA 유지)
  <li>ItemB</li>  //비교대상2 (ItemB 유지)
  <li>ItemC</li>  //비교대상3 (ItemC 추가)
</ul>

따라서, 마지막 node가 추가된 경우에는 마지막 노드만 update 되지만

다음과 같이 맨앞 node가 추가된 경우에는 모든 node에 update가 발생한다. 즉 불필요한 성능 저하가 발생하는 것이다.

// before
<ul>
  <li>ItemA</li>  //비교대상1
  <li>ItemB</li>  //비교대상2
</ul>

// after
<ul>
  <li>ItemC</li>  //비교대상1 (ItemA -> ItemC 로 변경)
  <li>ItemA</li>  //비교대상2 (ItemB -> ItemA 로 변경)
  <li>ItemB</li>  //비교대상3 (ItemB 추가)
</ul>

key
이러한 문제를 해결하기 위해, React에는 key 존재한다. children이 key를 가지고 있으면, React는 동일한 key를 갖는 자식끼리 비교를 수행한다.

// before
<ul>
  <li key="A">ItemA</li>  //비교대상1
  <li key="B">ItemB</li>  //비교대상2
</ul>

// after
<ul>
  <li key="C">ItemC</li>  //비교대상3 (ItemC 추가)
  <li key="A">ItemA</li>  //비교대상1 (ItemA 유지)
  <li key="B">ItemB</li>  //비교대상2 (ItemB 유지)
</ul>

이 경우에는 ItemC 노드만 추가된다.
해당 key는 오로지 형제 사이에서만 유일하면 되고, 전역에서 유일할 필요는 없다.
리스트의 배열이 재정렬되지 않거나 last-child에서만 추가, 변경, 제거가 일어난다면 인덱스를 key로 사용해도 되지만, 순서가 바뀌는 경우가 발생하면 key가 전부 바뀌기 때문에 key를 사용한 의미가 없어진다.

[참고]

https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060
https://youtu.be/muc2ZF0QIO4

profile
Web FE Developer
post-custom-banner

2개의 댓글

comment-user-thumbnail
2023년 12월 6일

thanks for your share!International students face challenges from different cultures and educational systems, and one of the most significant challenges is academic writing. Many international students may encounter difficulties in expressing themselves in English and adhering to academic standards. Reliable paper ghostwriting http://www.cieae.net/liuxuezuoye services in the United States can provide high-quality academic papers, helping international students better understand academic requirements and enhance their writing skills. This contributes to achieving better academic grades.

답글 달기
comment-user-thumbnail
2024년 1월 19일

I spent many hours challenging myself and improving my https://retro-bowl.io/drift-hunters skills in this game.

답글 달기