1.2가상 DOM과 리액트 파이버
변경될 요소들을 메모리에서 계산해 실제 DOM과 동기화하는 가상 DOM과 이를 위한 데이터 구조 및 스케줄링 단위인 Fiber
추억의 쪽지 시험
React의 가장 큰 특징으로 종종 언급되는 것은 가상 DOM을 운영한다는 것이다. 이 가상 DOM이 무엇이고 어떤 이점을 위해 React가 쓰는지에 대해 알아보자.
1. DOM과 브라우저 렌더링 과정
DOM(Document Object Model)은 웹페이지에 대한 인터페이스로, 브라우저가 웹페이지의 컨텐츠와 구조를 어떻게 보여줄지에 대한 정보이다.

브라우저가 웹사이트 접근 요청을 받고 화면을 그리는 과정은 다음과 같다:
- 브라우저는 사용자가 요청한 주소를 방문해 HTML 파일을 다운로드한다.
 - 브라우저의 렌더링 엔진은 HTML을 파싱해 DOM 노드로 구성된 트리를 만든다.(DOM)
 - 이 과정에서 CSS 파일을 만나면 해당 CSS 파일도 다운로드한다.
 - 브라우저의 렌더링 엔진은 CSS도 파싱해 CSS 노드로 구성된 트리를 만든다.(CSSOM)
 - 브라우저는 2번으로 만든 DOM 노드를 순회하는데, 모든 노드를 방문하는 건 아니고 사용자 눈에 보이는 노드만 방문한다.
- 즉, 
display: none과 같은 요소는 방문하지 않는다. 이는 트리 분석 과정을 조금이라도 빠르게 하기 위함이다. 
 - 즉, 
 - 브라우저는 눈에 보이는 DOM 노드에 CSSOM에 있는 스타일 정보를 적용한다. 이 과정은 아래의 두 가지로 이루어진다.
- 레이아웃(layout, reflow): 각 노드가 브라우저 화면의 어느 좌표에 나타나야 하는지 계산하는 과정.
 - 페인팅(painting): 레이아웃 단계를 거친 노드에 색 등 실제 유효한 모습을 그리는 과정.
 
 
2. 가상 DOM의 탄생 배경
위에서 본 것과 같이 브라우저가 HTML과 CSS를 분석해 웹페이지를 렌더링하는 과정은 매우 복잡하고 많은 비용이 든다. 또, 요즘 웹앱은 렌더링된 이후 정보를 보여주는 것에서 그치지 않고 사용자와 상호작용을 하며 계속 변경된다.
특히, React가 지향하는 SPA(Single Page Application)은 한 페이지에서 모든 작업이 일어나기에 초기 렌더링 이후 추가 렌더링 작업이 아주 많이 일어난다. 페이지가 변경될 때 다른 페이지로 가서 HTML을 다시 받는 이전의 일반적 웹사이트들과 달리, React에선 라우팅이 변경되는 경우 사이드바나 헤더와 같은 요소를 제외하고 대부분의 요소를 삭제하고, 새 요소를 삽입하고 위치를 계산하는 작업을 수행한다. 물론 이 덕에 사용자가 페이지의 깜박임 없이 자연스럽게 웹페이지의 이곳저곳을 탐색할 수 있지만 그만큼 DOM은 부담해야할 비용이 커진다.
개발자 입장에서, 사용자의 상호작용에 따라 DOM의 모든 변경 사항을 추적하기보단 '그렇게 해서 결과적으로' 만들어지는 DOM 결과물만 알고 싶다. 이걸 도와주는 것이 바로 가상 DOM으로, React의 과거의 것이지만 여전히 유효한 개념들을 포함하고 있는 공식 문서에 따르면 UI의 이상적인, 또는 "가상"적인 표현을 메모리에 저장하고 react-dom과 같은 라이브러리로 실제 DOM과 동기화하는 프로그래밍 개념이다.
보다 구체적으론, 가상 DOM은 웹페이지가 표시해야할 DOM을 메모리에서 계산한다. 즉, 어떤 호스트 노드를 생성/갱신/삭제할지 변경 계획(diff)를 계산한다. 이후, React가 실제 변경을 할 준비가 완료되었을 때 실제 브라우저의 DOM에 반영한다.(그리고 브라우저 엔진이 이 DOM을 레이아웃/페인트 한다)
이러한 변경 계획 수립 및 동기화 과정을 재조정(Reconciliation)이라고 한다. 이렇게 브라우저에서 DOM 계산을 하나하나 하지 않고 메모리에서 계산한다면 실제로는 여러번 발생했을 렌더링 과정을 최소화할 수 있다.
가상 DOM 방식은 일반적인 DOM 관리보다 반드시 빠른것은 아니다. 다만, 대부분의 상황에서 애플리케이션을 개발할 수 있을 만큼 합리적으로 빠르기에 React에서는 이 방식을 채용한 것이다.
3. 가상 DOM을 위한 아키텍처, 리액트 파이버
3-1. 리액트 파이버란?
React는 이런 가상 DOM을 만들고 렌더링 최적화를 하기 위해 React Fiber라는 데이터 구조이자 스케줄링 단위를 쓴다. Fiber는 Fiber Reconciler(파이버 재조정자)가 관리하는 자바스크립트 객체로, 각 Fiber는 하나의 작업 단위를 처리한다.
좀 더 구체적으로 살펴보자. Fiber는 React에 이런 식으로 구현되어 있다:
javascriptfunction FiberNode(tag, pendingProps, key, mode){ // Instance this.tag = tag this.key = key this.elementType = null this.type = null this.stateNode = null // Fiber this.return = null this.child = null this.sibling = null this.index = 0 this.ref = 0 this.pendingProps = pendingProps this.memoizedProps = null this.updateQueue = null this.memoizedState = null this.dependencies = null this.mode = mode // ... 이하 생략 }
그리고, React에는 이 FiberNode들을 생성하는 함수들이 여럿 정의되어 있다.
javascriptvar createFiber = function(tag, pendingProps, key, mode){ return new FiberNode(tag, pendingProps, key, mode) } function createFiberFromElement(element, mode, lanes){ var owner = null { owner = element._owner } var type = element.type var key = element.key var pendingProps = element.pendingProps var fiber = createFiberFromTypeAndProps( type, key, pendingProps, owner, mode, lanes ) { fiber._debugSource = element._source fiber._debugOwner = element._owner } return fiber } // ... 생략
3-1-1. tag: Fiber가 1:1로 연결된 정보
위의 함수명 createFiberFromElement에서 보듯이 Fiber는 하나의 element에 대응하는 정보를 가지고 있다. 이 element는 JSX로 반환되는 React Element 객체, 즉 HTML의 DOM 노드일 수도 있고, React의 컴포넌트일 수도 있다. 실제로, Fiber의 tag 속성은 아래와 같은 값들을 가질 수 있다고 정의되어 있는데, 이름만으로도 익숙한 값들이 보인다.
javascriptvar FunctionComponent = 0 var ClassComponent = 1 var IndeterminateComponent = 2 var HostRoot = 3 var HostPortal = 4 var HostComponent = 5 var HostText = 6 var Fragment = 7 // ... var SuspenseComponent = 13 var MemoComponent = 14 // ...
친숙한 FunctionComponent나 ClassComponent도 보이는데, 5번 HostComponent가 웹의 <div/>와 같은 요소를 의미한다.
3-1-2. stateNode
React가 개별 Fiber에 접근하기 위한 참조(reference) 정보를 여기서 보관한다.
3-1-3. Fiber 트리를 만들기 위한 정보들을 포함한 속성들
FiberNode의 속성들 중 child, sibling, return은 Fiber 간의 관계를 나타내는 속성으로, Fiber는 트리 형식을 가지게 되는데 이 트리를 만들기 위한 정보들이 이 속성들에 정의된다. 여기서 눈여겨보아야 할 점은 React Component Tree와는 다르게 children이 아니라 child, 즉 단일 자식 Fiber만 가진다는 점이다.
그렇다면 만약 여러개의 자식을 가진 형태의 구조는 어떻게 Fiber로 표현될까?
jsx<ul> <li>하나</li> <li>둘</li> <li>셋</li> </ul>
Fiber의 자식은 항상 첫 번째 자식의 참조로 구성된다. 따라서 <ul> Fiber의 자식은 첫번째 <li> Fiber가 된다. 그리고 나머지 두 개의 <li>들은 첫번째 <li>의 형제 Fiber, 즉 sibling으로 구성된다. return 속성은 부모 Fiber를 의미하니, 여기서 세 개의 <li> Fiber는 모두 상위의 <ul> Fiber를 return으로 갖는다.
이 관계도를 자바스크립트 코드로 정리하면 대략 아래와 같다. index 속성은 형제들 사이에서 자신이 몇 번째 위치인지를 숫자로 표현한다.
javascriptconst l3 = {return: ul, index: 2} const l2 = {sibling: l3, return: ul, index: 1} const l1 = {sibling: l2, return: ul, index: 0} const ul = { // ... child: l1 }
3-1-4. pendingProps와 memoizedProps
pendingProps는 아직 작업을 미처 처리하지 못한 props다. memoizedProps는 렌더링이 완료된 이후에 pendingProps를 저장하기 위한 속성이다.
3-1-5. updateQueue
상태 업데이트, 콜백 함수, DOM 업데이트 등에 필요한 작업을 담아두는 큐다.
3-1-6. memoizedState
함수 컴포넌트의 훅 목록이 저장되는 속성으로, useState, useEffect 등 모든 훅 리스트가 여기에 저장된다.
이런 식으로 다양한 속성을 가지고 생성된 Fiber들은 state가 변경되거나 생명주기 메서드가 실행되거나(클래스 컴포넌트) DOM 변경이 필요한 시점 등에 실행된다.
여기서 중요한 것은 React는 Fiber를 이용한 작업들을 작업을 나눠서 수행하고, 중단/재개/취소할 수도 있다는 점인데 이것이 Fiber의 가장 큰 혁신이다. React 15까지 React는 Stack Reconciler라는 단일 스택 기반 구조로 렌더링을 했는데, 스택이라는 말에서 알 수 있듯이 이 작업은 싱글 스레드 언어인 자바스크립트에서 동기적(Synchronously)으로 진행되었다.
즉, 한번 렌더링이 시작되면 전체 컴포넌트를 끝까지 처리해야 했고 렌더링 중간엔 사용자 입력이나 애니메이션 프레임을 중단할 수 없었다. UI가 멈추고, 프레임이 드랍되고 스크롤이 끊기는 현상이 빈번하게 일어났다. 이런 렌더링의 비효율성을 해결하기 위해 React 팀은 렌더링을 작게 쪼개고, 우선순위 기반으로 조정할 수 있는 구조로 바꿨는데 그것이 Fiber다. 위의 createFiberFromElement에서 본 파라미터 중 lanes가 이 업데이트 우선순위를 설정하는 것이다.
또 하나의 중요한 점은 Fiber는 컴포넌트가 최초로 마운트되는 시점에 생성되어 이후엔 가능한 재사용(필드만 갱신)된다는 사실이다. 이는 React 요소가 렌더링이 발생할 때마다 새롭게 생성되는것과 대비된다.
3-2. 리액트 파이버 트리
React Fiber들로 이루어진 트리는 더블 버퍼링(Double Buffering)이라는 기술을 통해 자연스럽게 가상 DOM을 업데이트한다. Double Buffering이란, 컴퓨터 그래픽 분야에서 사용되는 용어로 한번에 모든 작업을 완료해 그림을 완성할 수는 없으니 보이지 않는 곳에서 그려야할 그림을 미리 그린 후, 완성되면 현재 상태를 새로운 상태 그림으로 바꾸는 기법이다.
이를 위해 Fiber Tree는 React 내부에 2개 존재한다. 현재 모습을 담은 Fiber Tree가 있다면, 또 하나의 트리는 작업 중인 상태를 나타내는 workInProgress 트리다. 현재 모습을 담은 current 트리를 두고, 업데이트가 발생하면 새로 받은 데이터로 새 workInProgress 트리를 빌드한다. 이 트리의 빌드가 끝나면 다음 렌더링에 이 트리를 사용한다. 그리고 이 workInProgress 트리가 UI에 최종적으로 렌더링되어 반영되면 current가 이 workInProgress로 변경되는데, React가 단순히 포인터만 변경하는 것일 뿐이다.
3-3. 파이버의 작업 순서
일반적인 Fiber Node의 생성 흐름은 이렇다:
- React는 
beginWork()라는 함수를 실행해 Fiber 작업을 수행하는데, 더 이상 작업이 없는 Fiber를 만날 때까지 트리 형식으로 시작된다. - 1의 작업이 끝나면 
completeWork()함수를 실행해 Fiber 작업을 완료한다. - 형제가 있다면 형제로 넘어간다.
 - 2, 3번이 모두 끝났다면 
return으로 돌아가 자신의 작업이 완료됐음을 알린다. 
여기서 setState 등으로 업데이트가 발생하면, 위에서 만든 current 트리는 두고 setState로 인한 업데이트 요청을 받아 workInProgress 트리를 빌드한다. 이 빌드는 위에서 만든 생성 흐름과 같은 방식이지만, Fiber는 이미 존재하므로 가급적 새로 생성하는게 아닌 기존 Fiber 내부에서 업데이트된 props를 받아 내부에서 처리한다.
트리를 비교해 업데이트하는 작업은 React에서 정말 흔한 작업이고, 계속해서 일어나는 이런 재조정 작업때마다 자바스크립트 객체를 만드는 것은 상당한 리소스 낭비다. 그렇기에 Fiber는 객체를 가급적 새로 만들지 않고 내부 속성값만 초기화하거나 바꾸는 형태로 트리를 업데이트한다. 앞서 언급한 예전의 Reconciler인 Stack Reconciler는 이 작업을 동기식으로 처리했기에 재귀적으로 트리를 순회하고 새로운 트리를 만드는 작업이 오래 걸리면 UI가 버벅였다.
4. 파이버와 가상 DOM
Fiber는 (tag 속성에서 봤듯이) React Element에 대한 정보를 1:1로 가지고 있으며 React Architecture 내부에서 비동기로 작업을 처리한다. 실제 브라우저 구조인 DOM에 반영하는 것은 동기적으로 일어나야 하고, 처리하는 작업이 많아 화면에 불완전하게 보여질 수 있으므로 이러한 작업을 가상, 즉 메모리상에서 수행한 후 최종적인 결과물만 실제 브라우저 DOM에 적용하는 것이 가상 DOM에서의 Fiber의 역할이다.
엄밀히 말하면, React Native에서도 React Fiber를 쓰기에 Fiber와 가상 DOM은 동일한 개념이 아니다. React Fiber는 브라우저가 아닌 환경에서도 사용할 수 있고, 따라서 다른 렌더러를 거치면 다른 환경에서도 사용될 수 있다.
5. 결론
가상 DOM과 React의 핵심은 브라우저의 DOM을 빠르게 반영한다는 것이 아니라, 화면에 표시되는 UI를 자바스크립트의 문자열, 배열 등과 같은 값으로 관리한다는 것이다.