Virtual DOM이 동작하는 과정

Kim-DaHam·2023년 5월 22일
1

React

목록 보기
2/6
post-thumbnail

🔥 학습목표

  • virtual DOM이 무엇인지 설명할 수 있다.
  • 리액트를 다루는 기술 Chapter 을 읽고 학습한다.
  • 🎁 코드스테이츠 블로그 문서를 읽고 학습한다.



🟩 Virtual DOM

React에서 UI의 상태를 추적하고 변화가 일어난 요소들을 빠르게 업데이트 할 수 있도록 만든 가상의 DOM 객체

실제 DOM의 사본 같은 개념이라고 생각하면 된다.

  • Real DOM : 브라우저에서 생성되는 DOM(Document Object Model)을 가리키는 용어. Virtual DOM과 구분하기 위해 사용한다.

🟣 Real DOM (DOM)

DOM은 브라우저가 HTML 문서를 조작할 수 있도록 트리 구조화한 객체 모델

⬜ 돔이란...

  • Document Object = 문서 객체
    └▷ JS와 같은 스크립팅 언어가 <html>, <head>, <body> 와 같은 태그들에 접근하고 조작할 수 있도록 문서를 트리 구조로 객체화 한 것.

우리가 document.querySelector('h1') 으로 접근하는 요소 하나하나가 돔의 트리 노드다.

이렇게 트리 구조를 가지는 이유는 바로 저장된 데이터를 효과적으로 탐색하기 위함인데, JS 같은 스크립트 언어가 접근하고 탐색하는 속도가 빠르며 변경/업데이트 속도를 향상시킨다.


⬜ DOM 업데이트

DOM이 변경되고 업데이트 된다는 것은, 즉 브라우저가 리렌더링 된다는 뜻이다.

그림은 브라우저의 렌더링 과정을 나타낸다.

브라우저의 렌더링 과정

  • DOM - HTML 문서를 DOM 트리를 생성한다.

  • CSSOM - JS에서 CSS를 조작할 수 있는 API 집합인 CSSOM을 만든다. HTML 대신 CSS가 대상인 DOM이라고 생각할 수 있으며, 사용자가 CSS 스타일을 동적으로 읽고 수정할 수 있다.

  • RenderTree - DOM 트리와 CSSOM 트리를 합친 후 브라우저에 표시될 것들만 선별한 RenderTree를 생성한다.

  • Layout - 각 요소가 배치될 공간을 계산한다.

  • Paint - 이를 화면에 그려낸다.


DOM이 변경되면 업데이트 된 요소와 그 자식 요소들에 대해 DOM 트리를 재구축 한다.

└▷ 요소의 관계에 따라 6개의 요소 중 단 하나의 요소만 변경되면 되는데, 변화가 필요 없는 나머지 5개 요소까지 모두 다시 그리게 되는 경우가 있다.

즉 레이아웃 재연산을 수행하는 Reflow, 이를 다시 화면에 그려내는 Repaint 과정을 거쳐야 한다는 뜻이며, 잦은 변경은 성능을 떨어뜨리게 된다.

극단적인 예로 프레임 드롭(Frame Drop)을 야기시킨다.

프레임 드롭(Frame Drop)
웹 애플리케이션에서 Frame Rate(초당 프레임 수)가 떨어져 화면이 버벅대거나 부드럽게 표시되지 않는 현상.

가상 DOM이 생겨난 이유는 바로 이 부분을 해결하기 위해서다.

"바뀐 부분만 골라 렌더링 하자"는 게 가상 돔의 핵심이다.


🟣 Virtual DOM

JS 객체로 이루어진 가상의 DOM 트리를 사용하여, 실제 DOM 조작을 최소화하고 성능을 최적화하는 기술

⬜ 가상 돔 vs 돔

가상 DOM은 실제 DOM과 동기화 되어

상태가 변경될 때마다 가상 DOM을 새롭게 생성하여 이전 상태와 비교한다.

그리고 가상 DOM과 실제 DOM을 비교하여 바뀐 부분만 업데이트 한다.

전체 UI를 다시 그리지 않아도 되는 것이다.


🌠 그런데 나는 이 부분에서 작은 의문이 들었다. 가상 DOM을 새롭게 생성하는 거나, 실제 DOM을 다시 그리는 거나 무슨 차이가 있지...?

이에 대해 찾아본 결과, Real DOM은 브라우저에서 관리를 하고

Virtual DOM은 리액트가 메모리상에서 관리하기 때문에

Virtual DOM을 새롭게 생성하는 비용 < 브라우저 위에 새로 "그리는" 비용

이라는 거다.

하긴 가상 돔은 브라우저에 그리지 않는데 내가 왜 저런 고민을 했을까


결론적으로 비유하자면 집을 이사할 때 가구를 직접 배치하기 전 미리 구조를 구상하는 것이 Virtual DOM이고, 실제로 배치하는 게 Real DOM이라는 거다.


⬜ 동작 과정

  1. Virtual DOM 에서 Real DOM의 스냅샷을 찍어둔다.

  2. DOM에 변경이 일어난다.

  3. 리액트에서 diffing 알고리즘을 통해 이전 상태와 현재 상태의 가상 DOM을 비교한다.
    └▷ 이 부분에서 Diffing 알고리즘이 상태 변화를 감지할 수 있도록 직접 할당하지 않고 setState 같은 메서드를 사용하는 것이다.

  4. 이전의 가상 DOM과 새로운 가상 DOM을 비교하여 변경이 필요한 부분만 실제 DOM에 반영하여 업데이트 한다. 이를 재조정(Reconciliation)이라 한다.
    └▷ 이 과정에서 여러 개의 상태 변화가 있을 경우 일괄적으로 한 번에 업데이트(Batch Update)를 한다. 성능 최적화와 불필요한 리렌더링을 최소화 한다.


⬜ 결론

하지만 이런 Virtual DOM도 늘 빠르다고 보장할 순 없다.

일반적으론 더 빠른 게 맞지만, 특수한 경우에는 직접 DOM을 조작하는 것이 더 빠를 수도 있다.



🟩 React Diffing Algorithm

기존 가상 DOM과 변경된 새로운 가상 DOM을 비교하는 트리 탐색 알고리즘

🟣 리액트가 DOM을 탐색하는 방법

⬜ 너비 우선 탐색(BFS)

리액트는 기존의 가상 DOM과 변경된 가상 DOM을 비교할 때 트리의 레벨 순서대로 순회한다.

우선 동일 선상에 있는 노드를 파악한 뒤 다음 자식 세대의 노드를 순차적으로 파악하는 것이다.

<body> 부모 아래 <ul> 요소가 2개 있고, 그 아래 자식으로 <li> 요소가 있다면 먼저 직속 자식인 두 개의 <ul> 부터 기존과 똑같은지 비교한다.


⬜ 다른 타입의 DOM 엘리먼트로 변한 경우

<div>
	<Counter />
</div>

//부모 태그가 div에서 span으로 바뀜
<span>
	<Counter />
</span>

위 예시에서 <Counter/> 자식 요소를 가진 <div> 부모 태그가 바뀌어 버렸다.

HTML의 특징으로, 각 태그마다 그 아래에 들어가야 하는 자식 요소가 정해진 경우가 있다.

예를 들어 <ul> 태그 안에는 <li> 태그가 와야하고 <p> 태그 안에 <p> 태그가 또 존재할 수 없다는 규칙이 있다.

반대로 자식 태그의 부모 태그 또한 정해져 있다고 말할 수 있겠다.

따라서 리액트는 부모 태그가 달라진 경우 이전 트리를 버리고 아예 새로운 트리를 구축해버린다.

이전의 DOM 노드들은 전부 파괴되고, <Counter/>는 완전히 해제된다. <div> 속 자식은 파괴되고 <span> 속 새로운 자식이 다시 실행되는 것이다.

이때 기존의 컴포넌트는 완전히 해제(Unmount) 되고 <Counter/>가 갖고 있던 기존 state 또한 파괴된다.


⬜ 같은 타입의 DOM 엘리먼트인 경우

<div className="바뀌기 전"></div>
<div className="바뀐 후"></div>

위 예시는 타입은 <div> 그대로인데 클래스명만 바뀌었다.

이런 경우 리액트는 최대한 렌더링을 하지 않는 방향으로 변경 사항만 업데이트 한다.

virtual DOM 내부의 프로퍼티만 수정한 뒤, 모든 노드에 대한 업데이트가 끝나면 그제야 단 한번 실제 DOM으로 렌더링을 한다.

만약 바뀌기 전 css와 바뀐 후 css의 차이가 오직 color에 대한 것 뿐이라면, 딱 color 스타일만 바꾸고 나머지 스타일 속성은 수정하지 않는다.

하나의 DOM 노드를 처리한 뒤 리액트는 뒤이어 해당 노드의 자식들을 순차적으로(재귀적으로) 동시에 순회한다.


⬜ 자식 엘리먼트 재귀적 처리

<ul>
  <li>first</li>
  <li>second</li>
</ul>

위와 같은 자식 엘리먼트가 존재한다고 했을 때,

리액트는 기존 <ul>과 새로운 <ul>을 비교할 때 자식 노드를 위에서부터 아래로 비교한다.

만약 저 자식요소에서 마지막 위치에 <li>third</li>를 추가한다고 해보자.

첫 번째 자식 <li>first</li>와 두 번째 자식 <li>second</li>가 일치한 걸 확인한 후 세 번재 자식노드를 추가한다.


만약 이런 동작 방식을 이해하지 못 하고 세 번째 자식 노드를 자식들 중 가장 첫 번째 자리에 추가한다고 생각해보자.

처음의 자식 노드를 비교할 때, 기존의 <li>first</li><li>third</li>가 일치하지 않다고 인지하게 되고, 따라서 React는 리스트 전체가 바뀌었다고 받아들인다.

<li>first</li><li>second</li>는 그대로 유지해도 되는데 전부 버리고 새롭게 렌더링 해버리는 것이다.

이런 문제를 해결하기 위해 key 속성 값을 지원한다.


⬜ 키(key) 속성이 필요한 이유

지금까지 map() 메서드 등으로 <li> 요소 같은 걸 만들 때마다 각 요소에 반드시 독립적인 key 속성값을 넣어줘야 한다는 오류를 발견하곤 했다.

그 이유가 바로 key를 이용해 기존 트리 자식과 새로운 트리 자식의 일치 여부를 판가름 하기 위해서다.

새로 추가된 자식이 맨 첫번째 자리에 있든, 중간에 있든, 마지막에 있든 key 값이 일치하면 그대로 두고, 새로우면 추가하는 거다.

그러기 위해선 형제 엘리먼트 사이에서 반드시 유니크한 값을 사용해야 하는데,

나는 종종 그런 유니크한 값을 정해두지 않을 때마다 배열의 인덱스를 key 값으로 정해두었다.

그런데 이럴 경우 배열이 다르게 정렬되어도 인덱스는 그대로 유지 되기 때문에, 인덱스는 그대로지만 그 요소가 바뀌면 리액트는 배열 전체가 바뀌었다고 받아들여 기존의 DOM을 버리고 새로운 DOM을 구축하게 된다...

[{
	id: 'uniquekey1',
  	name: 'kim'
},
 {
	id: 'uniquekey2',
  	name: 'lee'
},
 {
	id: 'uniquekey3',
  	name: 'park'
}]

이런 데이터 모음을 <li> 로 순차 출력한다고 했을 때, 그에따른 key 속성은 id로 지정하게 될 것이다.

이런 경우 배열의 순서가 아무리 뒤바뀌어도 'uniquekey1'는 영원히 namekim인 데이터고, 'uniquekey2'는 언제나 namelee`인 데이터다.

그런데 key 값을 고유한 값인 id로 할당하지 않고 배열의 idx 값으로 준다면 배열의 순서가 뒤바뀌었을 때 문제가 발생한다.

기존 DOM에서 key0<li>namekim인 데이터인데

바뀐 DOM에서 key0<li>namelee인 데이터가 되버리고 마는 것이다.

뭔가 바보짓을 한 기분이다. 앞으로 배열 인덱스를 key 로 넣는 일은 없도록 하자.



profile
다 하자

0개의 댓글