avatar
yoosioff
2-2. 가상 DOM과 리액트 파이버
2-2. 가상 DOM과 리액트 파이버
May 25
·
19 min read

DOM과 브라우저 렌더링 과정

DOM은 웹 페이지에 대한 인터페이스로 브라우저가 웹 페이지의 콘텐츠와 구조를 어떻게 보여줄지에 대한 정보를 담고 있다.

  1. HTML 파일 다운로드

  2. 브라우저 렌더링 엔진은 HTML을 파싱해 DOM노드로 구성된 트리를 만든다.

  3. 2번 과정에서 CSS 파일을 만나면 해당 CSS 파일도 다운로드한다.

  4. 브라우저 렌더링 엔진은 이 CSS도 파싱해 CSS 노드로 구성된 트리를 만든다.

  5. 2번에서 만든 DOM 노드를 순회하는데, 여기서 모든 노드를 방문하는 것이 아닌 사용자 눈에 보이는 노드만 방문한다. 즉, display:none;과 같이 사용자 화면에 보이지 않는 요소는 방문하지 않는다.

    → 트리를 분석하는 과정을 조금이라도 빠르게 하기 위해서

  6. 5번에서 제외된, 눈에 보이는 노드들 대상으로 CSS 스타일 정보를 노드에 적용한다.

    • 레이아웃: 각 노드가 브라우저 화면의 어느 좌표에 정확히 나타나야 하는지 계산하는 과정

      • 레이아웃 과정을 거치면 반드시 페인팅 과정도 거치게 됨

    • 페인팅: 레이아웃 단계를 거친 노드에 색과 같은 실제 유효한 모습을 그리는 과정

reflow(layout)

뷰포트 내에서 렌더 트리상 노드의 정확한 위치와 크기를 계산하는 과정이다.

  • 윈도우 리사이징

  • 폰트 변화

  • 스타일 추가 또는 제거

  • :hover와 같은 CSS Pseudo Class(의사 클래스)

  • JS를 통한 DOM 동작 변화

  • 등등..

reflow 최적화

  1. 인라인 스타일 자제

    인라인 스타일은 HTML이 파싱될 때, 레이아웃에 영향을 미쳐 추가 리플로우를 발생시킨다.

  2. js를 통해 스타일 변화를 주어야할 경우 가급적 한번에 처리해라.

    특정 요소에 여러 스타일 변화를 지정해야 할 경우 한번에 처리하면 중복된 reflow를 최소화할 수 있다.

    var toChange = document.getElementById('elem');
    toChange.style.background = '#333';
    toChange.style.color = '#fff';
    toChange.style.border = '1px solid #ccc';
    
    /* CSS */
    #elem { border:1px solid #000; color:#000; background:#ddd; }
    .highlight { border-color:#00f; color:#fff; background:#333; }
    /* js */
    document.getElementById('elem').className = 'highlight';
    
  3. 하위 선택자를 최소화 한다.

    CSS 하위 선택자를 최소화 하는 것이 렌더링 성능에 더 좋다.

    CSS 하위 선택자가 많아지면 CSSOM 트리의 깊이(Depth)가 깊어지게 되고 결국 렌더 트리를 만드는 시간이 더 오래 걸리게 된다.

    /* 잘못된 예 */
    .reflow_box .reflow_list li .btn{
      display:block;
    }
    /* 올바른 예 */
    .reflow_list .btn {
      display:block;
    }
    
  4. 숨겨진 노드의 스타일을 변경한다.

    display: none 으로 숨겨진 노드를 변경할 때는 reflow가 발생하지 않는다. 숨겨진 노드를 표시하기 전에 노드의 컨텐츠를 먼저 변경한 후 화면에 나타내면 reflow를 줄일 수 있다.

repaint

레이아웃에 영향을 미치지 않고 요소의 외관을 변경하는 과정이다

  • 요소 색상 지정 (background-color, color)

  • visibility

Reflow와 Repaint에 대하여

CSS Triggers List - What Kind of Changes You Can Make

CLS (Cumulative Layout Shift) → 누적 레이아웃 시프트

Layout Shift란 페이지 콘텐츠가 예기치 않게 이동하는 현상이다.

보통 리소스가 비동기 식으로 로드되거나 DOM 요소가 동적으로 추가되어 발생한다.

CLS는 페이지 수명 동안 발생하는 모든 예상치 못한 레이아웃 변경에 관한 레이아웃 변경 점수의 가장 큰 버스트를 측정한 것입니다. → layout shift 현상을 측정한 것

계산방법

브라우저는 표시 영역 크기 및 렌더링된 두 프레임 간 표시 영역 내 불안정한 요소의 이동을 고려합니다.

레이아웃 변경 점수는 이동의 두 가지 측정값, 즉 영향 비율과 거리 비율의 곱입니다. = 영향 비율 * 거리 비율

layout shift score = impact fraction * distance fraction

기존 영역 = 50%

이동된 거리 비율 = 25%

영향 비율 = 75%

0.75 * 0.25 = 0.1875의 레이아웃 변경 점수 측정

💡 좋은 CLS 점수는 0.1 이하여야 한다.

Cumulative Layout Shift (CLS)  |  Articles  |  web.dev

Virtual DOM (가상 돔)

가상돔의 배경

앞서 본것처럼 브라우저가 웹 페이지를 렌더링 하는 과정은 매우 복잡하고 많은 비용이 든다.

MPA의 경우는 다른 페이지로 가서 처음부터 HTML을 새로 받아서 다시금 렌더링 과정을 시작한다. 하지만 SPA의 경우 계속해서 요소의 위치를 재계산하기 때문에 렌더링 이후 추가 렌더링 작업이 많아진다.

SPA는 하나의 페이지로 페이지 이동 깜빡임 없이 자연스러운 탐색이 가능하지만, 그만큼 DOM을 관리하는 과정에서 부담해야 할 비용이 커진다.

개발자 입장에서 생각해보면 개발자는 인터렉션에 모든 DOM의 변경보다는 결과적으로 만들어지는 DOM 결과물 하나만 알고 싶을 것이다. 이러한 문제점을 해결하기 위해 탄생한 것이 바로 가상 DOM이다.

가상돔

가상 DOM은 실제 브라우저의 DOM이 아닌 리액트에서 관리하는 가상의 DOM 객체을 의미한다.

가상 DOM은 웹페이지가 표시해야 할 DOM을 일단 메모리에 저장하고, 리액트가 실제 변경에 대한 준비가 완료됐을 때 실제 브라우저의 DOM에 반영한다.

💡 리액트의 이러한 방식이 일반적인 DOM을 관리하는 브라우저보다 빠르다는 오해가 있다.

가상 DOM 방식은 브라우저 DOM보다 무조건 빠른 것이 아니라 웬만한 애플리케이션을 만들 수 있을 정도로 충분히 빠르다는 것이다.

리액트 파이버

그렇다면 이러한 가상 DOM을 만드는 과정을 리액트에서 어떻게 처리하고 있을까?

이러한 가상 DOM과 렌더링 과정 최적화를 가능하게 해주는 것이 바로 리액트 파이버다.

리액트 파이버란?

리액트에서 관리하는 평범한 자바스크립트 객체다.

  • 파이버 재조정자에 의해 관리된다.

  • 재조정이라는 용어는 가상 DOM과 실제 DOM을 비교하는 작업(알고리즘)이라고 이해하면 된다.

  • 가상 DOM과 실제 DOM을 비교하며 변경사항을 수집한다

    • 이 둘 사이에 차이가 있으면 변경에 관련된 정보를 가지고 있는 파이버를 기준으로 화면에 렌더링을 요청한다.

파이버의 배경

파이버는 다음과 같은 일을 할 수 있다.

  1. 작업을 작은 단위로 분할하고 쪼갠 다음, 우선순위를 매긴다.

  2. 이러한 작업을 일시중지하고 나중에 다시 시작할 수 있다.

  3. 이전에 했던 작업을 다시 재사용하거나 필요하지 않은 경우에는 폐기할 수 있다.

💡 여기서 중요한 것은 이러한 모든 과정이 비동기로 일어난다는 것이다.

과거의 알고리즘

이전에는 조정 알고리즘이 스택 알고리즘으로 이루어있었다.

렌더링에 필요한 작업이 쌓이면 해당 스택이 빌 때 까지 동기적으로 작업이 이루어지게 된다.

한계

자바스크립트의 특징인 싱글 스레드라는 점으로 인해 동기 작업이 중단될 수 없고, 다른 우선순위가 높은 작업이 수행되고 싶어도 중단할 수 없어 리액트의 비효율성으로 이어졌다.

이러한 기존 렌더링 스택의 비효율성을 해결하기 위해 파이버라는 개념이 탄생된 것이다.

파이버의 동작 과정(리액트의 생명주기)

  1. 파이버는 일단 하나의 작업 단위로 구성되어 있다.

  2. 이러한 작업 단위를 하나씩 처리하고 finishedWork() 라는 작업으로 마무리 한다.

  3. 이 작업을 커밋해 실제 브라우저 DOM에 가시적인 변경사항 만들어 낸다.

이러한 단계는 다음 두 단계로 나눌 수 있다.

  • 렌더 단계

    랜더 단계에서 사용자에게 노출되지 않는 모든 비동기 작업을 수행한다. 앞서 말한 파이버의 작업, 우선순위 지정/중지/삭제 등의 작업이 일어난다.

  • 커밋 단계

    DOM에 실제 변경 사항을 반영하기 위한 작업니다.

    commitWork()가 동기적으로 실행되어 해당 과정은 중단될 수 없다.

💡 파이버의 실제 구조를 보면 리액트의 요소와 유사하다고 느낄 수 있지만, 중요한 차이점은 리액트 요소는 렌더링이 발생할 때 마다 새롭게 생성되지만, 파이버는 가급적이면 재사용된다는 사실이다.

파이버는 컴포넌트가 최초로 마운트되는 시점에 생성되어 이후에는 가급적이면 재사용된다.

  1. state가 변경

  2. 생명주기 메서드가 실행

  3. DOM의 변경이 필요한 시점

등에 실행된다.

최적화

  • 작은 단위로 나눠서 처리

  • 우선순위가 높은 작업은 가능한 한 바르게 처리

  • 낮은 작업을 연기

시키는 등 좀 더 유연하게 처리된다.

리액트 파이버 트리

파이버 트리는 리액트 내부에서 두 개가 존재한다.

  1. 현재 모습을 담은 파이버 트리

  2. 작업 중인 상태를 나타내는 workInProgress 트리

리액트 파이버 작업이 끝나면 리액트는 단순히 workInProgress 트리를 현재 트리로 변경한다.

이러한 기술을 더블 버퍼링이라고 한다.

그래픽 처리의 문제점

그래픽을 통해 화면에 표시되는 것을 그리기 위해서 내부적으로 처리하던 중, 사용자에게 미처 다 그리지 못한 모습을 보는 경우가 발생하게 된다.(한번에 모든 작업을 마무리할 수 없기 때문)

더블 버퍼링?

그래서 이러한 상황을 방지하기 위해 보이지 않는 곳에서 그림을 미리 그린 다음, 해당 그림이 완성되면 현재 상태를 새로운 그림으로 바꾸는 기법을 의미한다.

리액트에서도 마찬가지로 불완전한 트리를 노출시키지 않도록 더블 버퍼링 기법을 사용한다. 이러한 더블 버퍼링을 위해 두개의 트리가 존재하며, 더블 버퍼링은 커밋 단계에서 수행된다.

즉, 먼저 현재 렌더링을 위해 존재하는 트리인 current를 기준으로 모든 작업이 시작된다.

  1. 업데이트가 발생하면, 파이버는 리액트에서 새로 받은 데이터로 새로운 workInProgress 트리 빌드

  2. workInProgress 빌드가 완료되면, 다음 렌더링에 해당 트리를 사용한다.

  3. workInProgress 트리가 UI에 최종적으로 렌더링이 반영되면

  4. current가 workInProgress로 변경된다.

파이버의 작업 순서

  1. 리액트는 beginWork() 함수를 실행해 파이버 작업을 수행하는데, 더 이상 자식이 없는 파이버를 만날 때 까지 트리 형식으로 시작된다.

  2. 1번에서 작업이 긑난다면 그 다음 completeWork() 함수를 실행해 파이버 작업을 완료한다.

  3. 형제가 있다면 형제로 넘어간다.

  4. 2, 3번이 끝났다면 return으로 돌아가 자신의 작업이 완료됐음을 알린다. → commitWork()가 수행되어 변경사항이 DOM에 반영

이제 여기서 setState 등으로 업데이트가 발생하면 어떻게 될까?

  1. 앞서 만든 current 트리가 존재함

  2. setState로 인한 업데이트 요청을 받아 workInprogress 트리를 다시 빌드하기 시작한다.

  3. 최초 렌더링 시에는 모든 파이버를 새롭게 만들어야 했지만, 최초 렌더링 이후에는 파이버가 이미 존재하므로 되도록 새로 생성하지 않고 기존 파이버에서 업데이트된 props를 받아 파이버 내부에서 처리한다.

💡 가급적 새로운 파이버를 생성하지 않는다.

새롭게 파이버 자바스크립트 객체를 만드는 것은 리소스 낭비라고 볼 수 있다.

마무리

컴포넌트에 대한 정보를 1:1로 가지고 있는 것이 파이버이며, 이 파이버는 내부에서 비동기로 이루어진다.

실제 브라우저 구조인 DOM에 반영하는 것은 동기적으로 일어난다.

또 처리하는 작업이 많을 때 DOM 트리가 불완전할 수 있으므로, 메모리상에서 먼저 수행 후 최종적인 결과물만 실제 브라우저 DOM에 적용한다.

💡 화면에 표시되는 자바스크립트의 문자열, 배열 등과 마찬가지로 값으로 관리하고 이러한 흐름을 효율적으로 관리하기 위한 메커니즘이 바로 리액트의 핵심이다. (값으로 UI를 표현하는 것)

새로 알게 된 점

  • CLS의 계산 방법

  • reflow 최적화 방법

  • 파이버의 두가지 동작 과정 (렌더/커밋 단계)

  • 더블 버퍼링







안녕하세요 :)