DOM의 새로운 발견! Virtual DOM 동작 원리

제이밍·2023년 2월 22일
10
post-thumbnail
post-custom-banner

virtual DOM 의 기원을 살펴보기 위해 브라우저의 렌더링 과정을 이해할 필요가 있어요

브라우저 렌더링 과정

  1. HTML은 렌더링 엔진을 거쳐 DOM 트리를 생성한다.
    렌더링 엔진 (사파리의 WEBKIT, 파이어폭스의 GECOK...)
    DOM 트리 : HTML를 파싱하여 DOM 노드로 이루어진 트리

  2. CSS는 렌더링 엔진 을 거쳐 CSSOM 트리를 생성한다.

  3. DOM과 CSSOM을 결합하여 렌더링 트리를 형성한다.
    ** render tree: DOM + CSSOM

  4. 렌더링 트리에서 레이아웃 처리를 통해 각 노드의 위치와 크기를 계산한다
    브라우저 화면의 어떤 위치에 어떤 크기로 출력될지 계산 하는 단계

  5. 페인팅 개별 노드를 화면에 페인팅한다.

DOM의 변화가 감지될때마다 렌더트리 생성됨을 반복함 -> 이 모든 것의 과정에 연산과정이 포함됨

Q 리플로우와 리페인트중 어느것이 더 부하가 클까?

  • 리플로우 (reflow)
    DOM 요소의 기하학적 속성이 변경될때, 브라우저 사이즈가 변할때, 스타일시트가 로딩되었을때 발생하는 변화들을 다시 계산 해주는 작업을 뜻하고 레이아웃(Layout) 이라고도 한다.

  • 리페인트 (repaint)
    변경된 요소를 실제로 화면에 그려주는 작업을 리페인트라고 한다. 그래서 리플로우가 발생하면 필연적으로 리페인트가 실행된다.

리플로우 >>> 리페인트 ?

리페인트도 굉장히 무거운 작업이긴 하지만 리플로우 처럼 모든 요소들에 대한 기하학적 정보들을 계산해주는 작업은 아니기 때문에 리플로우 보다는 상대적으로 훨씬 가벼운 작업이다

과연 실제로도 그럴까?

실제 크롬 개발자 도구에서 확인해 본 결과 리플로우보다 리페인트의 부하가 더 큰것을 확인할 수 있었어요. 단지 리플로우가 일어나면 많은 부분에 리페인트를 유발하기 때문에 리플로우를 최소화하는 것이 중요한건 변치 않는 사실 📝

하지만 리플로우를 최소화했다고 해도 원래 부하가 큰 리페인트가 있다면 소용 없을 것입니다!

그럼에도 DOM 조작의 중요도는 점점 올라간다.

SPA 싱글 어플리케이션을 사용하는 웹이 증가 함에 따라 페이지를 서버가 아닌 브라우저에서 관리되기 때문에 효율적인 DOM 조작의 필요성이 더욱 증가 되었다 ✅

DOM을 직접 조작하는것은 성능면에서 느려요!

DOM을 직접 조작하는 것이 느리다는 것은 이미 많이 알려진 사실이지만, 막상 그렇지도 않다고해요.

as-is
DOM은 일반적으로 객체 대신 노드(Node)를 사용하여 계층 구조를 형성하고 이를 탐색하며 메모리를 관리하는 방식을 사용했기 때문에 자바스크립트와 DOM 간의 데이터 교환과 상호작용이 불편하고 비효율적이었어요! IE와 같은 구형 브라우저들의 형태이며,

to-be
IE가 아닌 모던 브라우저(Chrome, Safari)는 DOM도 자바스크립트와 완전히 동일한 메모리 모델을 쓰기 때문에 자바스크립트의 객체를 다루는 것과 속도 차이는 거의 없어요! 🧐

모던 브라우저 역시 워낙 똑똑해서 리액트와 마찬가지로 이전 렌더링 결과와 바뀐 게 없다면 딱히 추가 작업을 하지 않는다고 해요

DOM을 최대한 건드리지 않는게 좋다는 건 전부 옛말.

모던 브라우저 유저라면 DOM의 속성 중에 렌더링에 영향을 주지 않는 것은 딱히 자주 갱신한다고 크게 성능의 문제가 되지는 않아요!

그렇다면 왜 리액트를 사용할까 ..?

"성능때문에.. 라고 하기엔 요즘 모던 브라우저는 너무 똑똑하다."

바닐라 자바스크립트로 코드를 짜게되면

const a = document.getElementById("a");
a.style.webKitBorderRadius = "10px";
a.addEventListener("click", console.log);

사소한 스타일일지라도 브라우저에 따른 접두어에 대한 노하우나 예외처리가 필요하고,
이벤트리스너를 다는 단순한 작업조차 브라우저별 호환성을 조사해야하는 경우가 발생!

과거 제이쿼리는 이러한 뷰제어의 노하우와 예외처리를 함수로 감춰 개발자가 그러한 지식이 없어도 안전하게 네이티브 DOM을 다룰 수 있는 전략을 취한 것과 비슷한 이유이지 않을까..

리액트에선 어떤 식으로 DOM을 조작하길래..

리액트 공식문서의 virtual DOM 정의

Virtual DOM (VDOM)은 UI의 이상적인 또는 “가상”적인 표현을 메모리에 저장하고 ReactDOM과 같은 라이브러리에 의해 “실제” DOM과 동기화하는 프로그래밍 개념
-REACT

  • virtual DOM 과 DOM의 차이점
    DOM 이랑 비슷한 형태의 오브젝트 속성을 가졌지만 실제 DOM이 가지고 있는 api addEventListener(), querySelector()를 갖지 않음

보통 React로 구현된 애플리케이션은 일반적으로 모든 엘리먼트를 React DOM 하나로 관리하기 때문에 하나의 루트 DOM 노드로 이루어져 있다.

React를 기존 앱에 통합하려는 경우 원하는 만큼 많은 수의 독립된 루트 DOM 노드가 있을 수 있습니다.

<div id="root"></div>

React 엘리먼트를 렌더링 하기 위한 FLOW
1. DOM 엘리먼트를 ReactDOM.createRoot()에 전달
2. React 엘리먼트를 root.render()에 전달

const root = ReactDOM.createRoot(
  document.getElementById('root')
);
const element = <h1>Hello, world</h1>;
root.render(element);

실제로 대부분의 React 앱은 렌더링 된 출력값을 변경하기 위해 root.render()를 한 번만 호출합니다

그 이후 데이터 변경이 일어나면 render 실행되고 전체 UI가 virtual DOM에 렌더 되요!

하.지.만 이제부턴 리액트가 처리하지

React 16버전 이상부터 핵심🔑 은 증분 렌더링

Fiber는 React 16의 새로운 재조정 엔진
Fiber의 주요 목표는 virtual DOM의 증분 렌더링을 활성화하는 것입니다!!
증분 렌더링의 핵심 알고리즘 중 하나는 diffing 알고리즘

증분렌더링 엔진 내부를 살펴보면..

const viewModel = {
  target:img,
  prop:{width:10},
  render() {
		Object.assign(this.target, this.prop);
	}
};

render 함수는 무조건적으로 prop을 target에 업데이트

const viewModel = {
  target:img,
  prop:{width:30},
  old = {width:20, height:20}, //이전 상태
  render(){
 	//불변성에 의해 바로 비교하지 않고 클론 후 비교 
    //이전에 존재했다가 사라지는 키를 찾기 위한 코드
    const oldKeys = Object.keys(this.old);
    
    //현재 prop을 복사하기 위한 객체
    const cloneProp = {};
 
    //현재 prop의 상태를 old와 비교
    const diff = Object.entries(this.prop).reduce((diff, [k, v])=>{
      //cloneProp에 복사
      cloneProp[k] = v;
 
      //old에 없거나 값이 다르면 diff에 포함
      if(!this.old[k] || this.old[k] !== v) diff[k] = v;
 
      //oldKey에서 지금 키를 제거하고 남은게 old에만 있는 키
      const i = oldKeys.indexOf(k);
      if(i !== -1) oldKeys.splice(i, 1);
 
      return diff;
    }, {});
 
    //old를 다 썼으므로 갱신함
    this.old = cloneProp;
 
    //old에만 있는 키에 대해 제거작업을 진행
    oldKeys.forEach(k=>delete this.target[k]);
 
    //변화가 있는 diff만 적용
    if(Object.keys(diff).length) Object.assign(this.target, diff);
  }
};

virtual dom은 메모리 상에서 동작하며, 실제로 렌더되는것이 아니기 때문에 연산 비용을 최소화 할 수 있어요.

우리가 작성한 JSX 문법이 해석되는 과정

function Component(){
  return(
    <div classname="title">
    	<h1>HELLO</h1>
    </div>
    );
}

// 1단계 바벨은 JSX를 React의 createElement 로 컴파일되며

function Component(){
  return React.createElement("div", {
    className: 'title'
  }, React.createElement("h1", null, "HELLO"));
}

// createElement는 내부적으로 아래와 같은 형태를 나타낸다. 
// React elements 는 DOM 요소를 virtual DOM으로 표현
// * 이때 virtual DOM은 메모리에 저장되며 실제 렌더되지 않으므로 연산비용이 작아요!
const element = {
  type: 'div',
  props: {
    className: 'title',
    children: [
      {
        type: 'h1',
       	children: 'HELLO',
      }
      ]
  }
};

// 여기서 Diffing 알고리즘을 통해 업데이트된 내용이 반영되어 렌더된다

// 궁극적으로 render를 만나 실제 DOM 요소가 됩니다.
ReactDOM.render(element, document.getElementById('title'));

🍪 쿠키

Rendering TAB

Shadow DOM? Virtual DOM?

Shadow DOM은 주로 웹 컴포넌트의 범위 지정 변수 및 CSS용으로 설계된 브라우저 기술
virtual DOM은 브라우저 API 위에 있는 JavaScript 라이브러리에서 구현되는 개념

✍🏻Reference

모델 렌더의 관계성
리액트 공식문서 rendering-elements
우아한형제들 가상돔

profile
모르는것은 그때그때 기록하기
post-custom-banner

0개의 댓글