DOM, VirtualDOM

DOM, Virtual Dom?

DOM (Document Object Model)

DOM은 Document Object Model로 HTML, XML 등 문서를 위한 API다.
프론트 개발자들은 DOM을 활용해 문서를 작성하고 구조를 분석하며 DOM에 요소를 추가, 삭제 및 수정을 할 수 있다.
모든 웹 브라우저는 HTML문서를 DOM으로 구문 분석을 하는 DOM parser가 있다.
DOM parser는 HTML을 읽고 데이터를 DOM으로 구성하는 개체로 바꾸고 DOM Tree로 배열한다.
DOM Tree에서는 HTML의 각 요소를 Node라고 부르는데, DOM에 변경이 발생하면 DOM Tree의 노드에 변경사항이 반영된다.
예를 들어 <div> 태그의 스타일을 업데이트 한다면 다른 DOM Node들에 영향을 주지 않기 위해 해당 업데이트가 필요한 DOM Node에만 접근하여 스타일을 업데이트 해준다.
하지만 불행하게도 DOM Tree에 변화가 생기면 각 노드들은 재정렬이 되어야 하는 결과를 불러올수 있는데 해당 작업은 비용을 많이 소요하는 작업이기 때문에 랜더링 하는데 리소스와 시간이 소요된다.
즉, DOM에 변화가 생길 때마다 DOM Tree는 매번 재생성 되는것이다.
다른 한 가지 같은 예로 DOM Tree에 하나의 Node를 추가 혹은 삭제하여 페이지 전체 레이아웃이 영향을 받는 경우 웹 페이지의 일부 혹은 전체가 리렌더링 하게 된다.
이런 경우를 Reflow 라고 하는데, 이는 유저의 인터렉션으로 발생하는 hover, 텍스트 입력, 글꼴 크기 변경, 등으로 인하여 발생하게 된다.
위에서 언급 한것 처럼 Reflow는 비용이 많이 드는 작업으로 성능및 속도 저하로 이어질수 있다.
과도한 Reflow를 피하기 위해서는 되도록 DOM에 접근하여 작업하는것을 피해야 한다. 이런 부분은 Virtual DOM을 통해 개선하게 된다.

Virtual DOM

VirtualDOM은 DOM에서 수행하는 모든 변경사항을 VirtualDOM에서 수행한 뒤 실제 DOM에 적용하여 계산 단계를 줄일 수 있게 해준다.
DOM에 여러 변경사항이 발생할 경우 Virtual DOM에서 모든 변경 사항을 하나로 그룹화하여 한 번의 계산만 수행하게 됨으로 직접 DOM을 조작하지 않아도 변경사항을 반영 할 수 있게 된다.
즉, Virtual DOM은 아래 문제점들을 해결해준다.
  • DOM 조작에 의한 렌더링 비효율 문제점
  • SPA 특징으로 DOM 복잡도 증가에 따른 최적화 및 유지보수 어려움

React에서 Virtual DOM의 작동 방식

Virtual DOM 작동방식

  • (1) Data 업데이트 진행시 전체 UI를 Virtual DOM에 리렌더링
  • (2) 이전 Virtual DOM 내용과 현재 내용을 비교 (Diff 알고리즘)
  • (3) 바뀐 부분만 실제 DOM에 적용
notion image
위의 그림은 리액트에서 뷰가 어떻게 업데이트 진행되는지 보여주고 있으며. 실제 코드에서 우리는 React에서 render() 함수를 통해 Virtual DOM을 구현한다. 그리고 render() 함수를 통해 엘리먼트들을 반환하고
state나 props에 변화가 생길 경우 역시 render()를 통해 새로운 엘리먼트 반환하게 되는데. 이 경우 react는 diff 알고리즘을 이용하여 효율적으로 뷰를 업데이트 진행한다.

Reconciliation

두 트리(DOM, VDOM)를 비교할 때 react는 root 요소 부터 비교를 하며 어떤 부분이 달라졌는지 확인한다.
과거의 reconciliation은 react 앱의 규모가 커지면 커질수록 reconciliation 과정 또한 오래 걸리기 때문에 성능에 영향을 주기 시작했다. 이후 react16 버전에서 부터는 fiber라는 것이 도입이 되며 reconciliation 과정이 개선이 되었다. fiber는 여러 setState를 묶어서 한 번에 반영하는 batch 처리를 하기도 하며 우선순위를 두어 우선순위가 높은 것 먼저 처리하고 나머지는 뒤로 미루는 처리를 하며 내부적으로 개선을 하고 있다.
react 16 버전 이전:
stack reconciliation 이라는 방식으로 VirtualDOM을 처리했으며 해당 방식은 전체 컴포넌트를 한 번에 처리하는 동기적인 구조였다.
동작 방식:
  • 동기적 랜더링: state 혹은 props가 변경되면 컴포넌트 트리 전체를 한 번에 렌더링하고 diffing 알고리즘을 적용하여 실제 DOM에 반영한다.
  • 렌더링 중단 불가: 큰 트리에서 많은 변경사항에 발생하게 되는 경우 한 번의 렌더링 작업이 길어지면 react는 이 작업이 끝날때 까지 브라우저 렌더링을 차단한다. 즉, 화면이 멈추거나 애니메이션이 끊기고 사용자 입력에 대한 응답이 느려지게 된다.
  • 재귀적 호출: 재귀방식을 통해 트리를 순회하며 한 번에 모든 노드를 처리하려고 했기 때문에 브라우저의 callstack limit 를 초과할 수 있는 위험이 있다. 즉, 매우 큰 트리에서는 성능문제와 함께 메모리 오류가 발생 할 수 있다.
react 16 버전 이후:
기존 문제를 해결하기 위해 설계된 react fiber 가 react 16버전 부터 도입이 됐다. fiber 을 통해 작업을 좀 더 유연하고 효율적으로 처리 할 수 있게 됐다.
*fiber는 기존의 일반적인 트리가 아닌 LCRS(Left Children Right Siblings) Tree로 구현되어 있다.
LCSR Tree?
LCSR Tree는 Left Children Right Siblings 를 의미한다. tree 왼쪽은 자식 노드가 붙고, 오른쪽은 형제 노드가 붙는 형태로 일반 트리 보다 좀 더 쉽게 모든 노드를 참조 할 수 있으며 구현 또한 간단하다는 장점이 있다.
notion image
node 생성
node 추가:
  • parent에 연결할 child는 left가 비어있을 경우 연결이 되지만 아닌 경우 마지막 right로 붙는다.
tree 출력:
특정 값 n의 k번째 child node 찾기
 
동작 방식:
  • 비동기적 렌더링:
    • 이제 한 번에 전체 컴포넌트 트리를 처리 하지 않고, 렌더링 작업을 작은 단위로 나누어 처리한다.
    • fiber는 렌더링 작업을 작게 쪼개어 각 작업을 프레임 단위로 분활하여 브라우저의 렌더링 작업이나 사용자 상호작용 같은 우선순위 작업이 있는 경우 이를 먼저 처리 할 수 있게 한다.
    • 이를 통해 UI가 멈추기 않고 프레임 손실 없이 애니메이션이 부드럽게 동작하며 사용자 입력에 빠르게 반응 할 수 있게 된다.
  • 우선순위 기반 작업 처리: fiber는 작업에 우선순위를 부여하고 높은 우선순위 작업을 우선으로 처리한다.
    • 상호작용: 사용자 입력이나 애니메이션 같은 상호작용 관련 작업은 높은 우선순위를 가지며 즉시 처리된다.
    • 덜 중요한 작업: 비주얼 업데이트 혹은 비동기 데이터 로딩 처럼 덜 중요한 작업은 나중에 처리된다.
    • 렌더링 중단 가능: 렌더링 중 더 중요한 작업이 들어오게 되면 현재 작업을 잠시 중단하고 중요한 작업을 먼저 처리한 뒤 다시 이어서 렌더링 작업을 진행한다.
  • 시간 분할: 렌더링 작업을 시간 단위로 분할하여 처리한다. 이를 통해 작업을 프레임 단위로 처리하고 브라우저가 멈추지 않도록 한다.
    • 작업 중단 및 재개: 긴 작업을 작은 기간 간격으로 나누어 처리한다. 브라우저에서 중요 렌더링 작업이 필요할 경우 fiber는 해당 작업을 우선 처리한 뒤 나머지 작업을 이어 진행한다
    • 인터렉션 유지: 사용자가 애플리케이션과 상호작용 할 때 끊김없이 반응 할 수 있으며 애니메이션도 더 부드럽게 처리된다.
  • 두 단계 렌더링 프로세스:
    • render: 컴포넌트의 state와 props를 바탕으로 트리를 탐색하며 변화가 필요한 컴포넌트를 찾아내는 과정이다. 이 단계에서는 실제 DOM에 변경이 발생하지 않으며 fiber는 이 작업을 비동기적으로 진행한다. - (작업이 중단될 수 있으며, 브라우저가 다른 중요한 작업을 처리해야 될 때 이를 중단하고 나중에 이어 처리 할 수 있다)
    • commit: render 단계에서 계산된 변경 사항을 실제 DOM에 반영하는 단계다. 이 단계는 동기적으로 진행되며 모든 변화가 실제 DOM에 적용된다. commit는 중단되지 않으며 한 번에 완료되어야 한다.

참고

© 2024 dan.dev.log, All right reserved.

Built with NextJS