>[!tip]- > tip 블록 안의 내용은 책의 내용이 아닌 개인적인 궁금증으로 조사한 내용입니다. ## DOM과 브라우저 렌더링 과정 **DOM(Document Object Model)**은 **브라우저가 HTML 문서를 트리 구조로 표현한 모델**이다. 브라우저는 이 DOM을 이용해서 **웹페이지의 콘텐츠를 어떻게 표시할지를 결정하고 제어**한다. 브라우저 렌더링은 **[[Critical Rendering Path]]** 라는 꽤 복잡하고 비싼 과정을 통해서 발생한다. 1. 브라우저가 **HTML 파일을 다운로드**한다. 2. 브라우저의 렌더링 엔진이 HTML 파일을 파싱해서 **DOM 노드로 구성된 트리(DOM)**를 만든다. 3. 2번 HTML 파싱 과정에서 **CSS 파일을 만나면 이 CSS 파일을 다운로드**한다. 4. 브라우저 렌더링 엔진이 CSS 파일을 파싱해서 **CSS 노드로 구성된 트리(CSSOM)**를 만든다. 5. 브라우저는 2번 과정에서 만든 DOM을 순회하는데, 모든 노드를 방문하는게 아니라 **실제로 사용자 눈에 보이는 노드만 방문**한다. `display: none` 과 같은 실제로 보이지 않는 노드들이 이 과정에서 제외된다. 6. 5번 과정에서 제외되지 않은 눈에 보이는 노드들에 CSSOM 규칙을 찾아 적용한다. 이렇게 해서 **화면에 표시되는 모든 노드의 콘텐츠와 스타일 정보를 모두 포함하는 렌더 트리가 생성**된다. 7. 렌더 트리를 이용해서 **레이아웃 단계**가 진행된다. 레이아웃 단계에서는 각 노드가 브라우저 화면에 어느 좌표에 정확히 나타나야 하는지 계산된다. 8. 이어서 **페인트 단계**가 진행된다. 페인트 단계에서는 각 노드가 화면의 실제 픽셀로 변환된다. ## 가상 DOM의 탄생 배경 위에서 봤듯이 **브라우저 렌더링 과정은 복잡하고 많은 비용이 드는 과정**이다. 그런데 요즘 대부분의 애플리케이션들은 콘텐츠를 한번 보여주고 마는 것이 아니라 사용자의 인터랙션을 통해서 다양한 화면을 보여준다. 따라서 **HTML 초기 렌더링 이후에도 페이지가 변경**되는 상황 또한 고려해야 한다. > [!note]- 페이지 변경에 이어지는 작업 > Case 1) **요소의 색깔이 변하는 경우.** 이 경우에는 요소들의 크기를 다시 계산할 필요가 없기 때문에 CRP의 마지막 단계인 **페인트 단계만 다시 수행**되고 비교적 가벼운 작업이 된다. > > Case 2) **요소가 보이지 않게 되거나 크기가 변하는 경우.** 이 경우에는 요소들의 크기를 다시 계산해야 한다. 따라서 **레이아웃 단계가 다시 수행**되고, 레이아웃 단계 이후에는 페인트 단계가 필수적으로 진행되기 때문에 총 2단계가 다시 수행된다. 게다가 변경된 요소 안의 **자식 요소도 또한 같이 변경**해줘야 하기 때문에 만약에 자식 요소가 많은 요소를 변경한 경우에는 비용이 많이 발생하게 된다. 이런 콘텐츠 변경은 하나의 페이지 안에서 모든 작업이 일어나는 **SPA**에서 더더욱 중요하게 되었다. SPA에서는 페이지가 변경되었을 때 HTML 파일을 불러와서 렌더링하는 것이 아닌 기존 화면의 콘텐츠를 변경하기 때문에 초기 렌더링 보다는 재렌더링 프로세스의 비중이 더 높아지게 된 것이다. 개발자의 입장에서 보면, 이렇게 DOM을 변경해야 할 일이 많아진 상황에서 어떤 이벤트가 발생할 때 마다 일일이 DOM 변경 로직을 선언하는 것이 너무나도 수고로운 일이었다. 중요한 것은 변경 과정이 아니라 변경 결과였기 때문에, **변경 후 결과만 선언하는 방식**이 개발자에게도 더 유용한 방식이었다. 이런 문제점들을 해결하기 위해서 등장한 것이 바로 **가상 DOM**이다. 가상 DOM은 웹 페이지에 표시되어야 할 **DOM을 우선 메모리에 저장**해두고, 상태 변경이 일어나면 리액트가 **이전 가상 DOM과 새로운 가상 DOM을 비교(diff)**해서 **변화가 필요한 최소의 부분만 실제 DOM에 반영**한다. (여기서 말하는 리액트는 엄밀히 말하면 'react'가 아니라 'react-dom'이다.) 이렇게 브라우저가 아닌 메모리에서 한번 연산을 거치는 덕분에 실제로는 여러 번 발생했을 DOM 변경으로 인한 재렌더링을 최소화 할 수 있었고, 개발자의 부담 또한 줄일 수 있었다. > 변경 과정이 아닌 최종 상태를 정의하면 > 내부적으로 가상 DOM을 통해 효율적으로 실제 DOM 변경을 수행한다. 하지만 **가상 DOM이 항상 빠른 것은 아니다.** 가상 DOM은 브라우저와 개발자의 편의를 위해서 고안된 방법이고, 이 방법이 대다수의 애플리케이션을 개발할 수 있을 만큼 합리적으로 빠르기 때문에 많은 사람들이 사용하고 있는 것으로 생각하는 것이 옳다. (**절충안**이라고 이해하면 될 듯) > [!tip]- 책에 의하면 댄 아브라모프도 그렇게 말했다고 하는데... > 댄 아브라모프 트위터 계정이 정지되어서 출처 트윗을 알 수가 없다... > [@danabra.mov on Bluesky](https://bsky.app/profile/danabra.mov) ## 가상 DOM을 위한 아키텍쳐, 리액트 파이버 [GitHub - acdlite/react-fiber-architecture: A description of React's new core algorithm, React Fiber](https://github.com/acdlite/react-fiber-architecture) - 가상 DOM은 "무엇이 바뀌었는가"를 표현하는 데이터 구조 - 파이버는 "그 바뀐 내용을 어떻게 언제 반영할 것인가"를 결정하는 작업 단위 파이버(Fiber)는 자바스크립트 객체이다. (구조는 아래에서 자세히 살펴본다.) 그리고 이것을 관리하는 것이 재조정자(Reconciler)이다. 좀 더 정확히 말하면, 재조정자(Reconciler)는 파이버 트리를 생성하고, 비교하고, 갱신하는 역할을 한다. 재조정자는 -> 새로운 React Element 트리를 받아 이전 Fiber 트리와 비교(diff)한 후 -> 어떤 Fiber가 새로 만들어졌고, 어떤 Fiber가 업데이트되거나 삭제되어야 하는지를 계산해서 (변경사항을 수집) -> 그 결과를 react-dom과 같은 Renderer에 넘겨서 실제 DOM을 갱신하도록 요청 하는 역할을 한다. 리액트 파이버의 목표는 리액트 웹 어플리케이션에서 발생하는 애니메이션이나 레이아웃, 사용자 인터랙션에 올바른 결과물을 보여주는 것이다. 이 목표를 달성하기 위한 리액트 파이버의 역할은 아래와 같다. 1. 작업을 작은 단위로 분할하고 쪼갠 다음 우선순위를 매긴다. 2. 이 작업을 일시중지하고 나중에 다시 시작한다. 3. 이전 작업을 재사용하거나 필요하지 않다면 폐기할 수 있다. 중요한 것은 이 작업들이 비동기적으로 일어난다는 것이다. 리액트 파이버 이전의 리액트 조정 알고리즘은 스택 알고리즘이었다. (스택 조정자라고도 한다.) 스택 재조정자는 하나의 스택을 두고 렌더링에 필요한 모든 작업들을 스택에 넣은 다음, 이 작업들을 동기적으로 실행했다. 문제는 동기적으로 실행했다는 것인데, 싱글스레드인 자바스크립트 런타임 환경 때문에 동기적으로 수행되는 작업을 중간에 중단할 수 없었고, 비효율적이었다. 자동완성 입력창을 예로 들면 동기적으로 실행하는 것의 문제점을 이해하기 쉽다. 검색어를 입력할 수 있는 창이 있고, 사용자가 입력하면 연관된 단어를 검색하는 api를 호출해서 데이터를 보여주는 기능이 있다고 하자. 사용자는 빠르게 검색어를 입력할 것이고, 사용자의 입력에 따라서 입력 결과를 보여주는 이벤트, api를 호출하는 이벤트, 로딩 스피너를 보여주는 이벤트들이 한번에 발생한다. 이 많은 이벤트들이 사용자의 입력이 있을 때마다 동기적으로 수행된다면 최악의 경우 사용자 입력에 지연이 발생할 수도 있다. 이렇게 사용자 입력에 따라 동시다발적으로 발생하는 이벤트와 애니메이션 처리는 요즘의 웹 어플리케이션에서는 피할 수 없는 문제이다. 이를 동기적으로 처리하는 것에는 한계가 있었기 때문에 리액트 파이버라는 개념이 등장하게 된 것이다.