리액트의 가장 큰 특징중 하나는 실제 DOM이 아닌 가상 DOM을 운영한다는 것이다!
이러한 가상 DOM은 왜 만들어졌고 실제 DOM과는 어떤차이가 있는지 알아보자.
DOM(Document Object Model)
웹페이지에 대한 인터페이스로 브라우저가 웹 페이지의 콘텐츠와 구조를 어떠한 방식으로 보여줄지에 대한 정보가 담겨있다.
브라우저가 웹 사이트 접근 요청을 받고 화면은 그리는 일은 아래의 순서대로 일어난다.
1. 브라우저가 URL에서 HTML파일을 다운로드
2. HTML을 파싱하여 DOM 트리를 만든다.
3. (2)번과정 진행중 CSS파일을 만나면 CSS파일을 다운로드
4. CSS를 파싱하여 CSSOM을 구성
5. (2)번 DOM노드를 순회한다. (display : none 과 같은 사용자에게 안보이는 요소는 작업하지 않는다.)
6. 해당 노드에 대한 CSSOM 정보를 찾아 스타일을 적용한다. (레이아웃 -> 페인팅 과정)
가상 DOM의 탄생 배경
방금 알아봤듯이 브라우저게 웹페이지를 렌더링하는 과정은 복잡하고 비용이 많이 드는 과정이다. 또한 최근의 대부분 앱은 렌더링된 이후 정보를 그저 보여주는 것이 아닌, 사용자와의 인터랙션을 통해서 계속 변경되는 상황이 많다.
1. 특정 요소의 색상이 변경되는 경우
페인팅만 일어나기 때문에 비교적 빠르다.
2. 특정요소의 크기,위치가 변경되는 경우
레이아웃 단계가 일어난다 -> 리페인팅이 발생한다. -> 많은 비용이 발생한다.
특히 이런 추가렌더링 방식은 한 페이지에서 모든 작업이 일어나는 싱글페이지 애플리케이션 (SPA) 에서 매우 많아지게 된다. SPA를 사용하면 사용자는 깜박임 없이 웹페이지를 이용할 수 있다는 장점이 있지만 그만큼 DOM를 관리하는 비용이 증가하게 된다.
개발자 입장에서도 사용자의 인터랙션에 따라서 DOM의 모든 변경 사항을 추적하는 것은 번거롭다. 모든 DOM의 변경보다는 결과적으로 만들어지는 DOM 결과물 하나만 아는것이 개발자에게도 유리하다.
이러한 배경에서 가상 DOM 개념이나오게 되었다.
가상 DOM은 웹페이지가 표시할 DOM을 우선은 메모리에 저장하고 리액트가 실제 변경에 대한 준비가 완료된 이후 실제 브라우저의 DOM에 반영하게 된다.
이러한 DOM 계산을 브라우저가 아닌 메모리에서 계산함으로써 여러번 발생할 렌더링을 최소화할 수 있으며 브라우저와 개발자의 부담을 덜 수 있다.
리액트의 속도
그렇다면 리액트는 일반적인 DOM관리 방식보다 빠를까? 그러지는 않다. 가상 DOM 방식은 거의 모든 상황에서 애플리캐이션을 만들 수 있을 정도로 충분히 빠르다는 것이 장점이다. 즉, 개발자에게 도움이 많이 되는 가상 DOM 방식이 충분히 빠르기 떄문에 채용되었다고 보는것이 맞다.
리액트 파이버
가상 DOM과 렌더링 과정 최적화를 가능하게 해주는 것이 리액트 파이버(React Fiber)이다.
리액트 파이버는 리액트에서 관리하는 평범한 자바스크립트 객체이다.
가상 DOM과 실제 DOM을 비교하여 변경사항을 수집하며 만약 이 둘 사이에 차이가 있다면 변경에 관련된 정보를 가지고 있는 파이버를 기준으로 화면에 렌더링을 요청하는 역할을 하게 된다.
파이버는 애니메이션,레이아웃,사용자 인터랙션에 올바른 결과물을 만드는 반응성 문제를 해결하기 위해 있으며 아래와 같은 일을 할 수 있다.
1. 작업을 작은 단위로 쪼개고 우선순위를 매긴다.
2. 이 작업들은 일시정지가 가능하다.
3. 이전 작업을 재사용하거나 필요없는 경우 폐기가 가능하다.
중요한 점은 이런 과정들이 모두 비동기로 이루어진다는 점이다.
과거 리액트는 이 과정이 스택구조였기 때문에 비효율성이 존재했는데 이 스택구조의 문제점을 해결하기 위해서 파이버라는 개념을 리액트 팀에서는 채용했다고 한다.
파이버의 구현
파이버는 하나의 작업 단위로 구성되어 있다. 리액트는 이 작업단위를 하나씩 처리한 후 finishedWork()라는 작업으로 마무리한다. 이후 이 작업을 커밋하여 실제 브라우저 DOM에 변경사항을 적용한다.
1. 렌더 단계 : 사용자에게 노출되지 않는 모든 비동기 작업을 수행한다. (우선순위,중지 등의 일이 파이버에서 일어남)
2. 커밋 단계 : DOM에 실제작업을 반영하기 위한 commitWork()가 실행된다. (동기식 진행 , 중단X)
이 렌더단계와 커밋단계는 추후에 보다 자세히 알아보자.
파이버는 아래와 같이 단순한 자바스크립트 객체로 구성되어 있다.
function 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 = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// Effects
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
this.expirationTime = NoWork;
this.childExpirationTime = NoWork;
this.alternate = null;
}
리액트 요소와의 중요한 차이점중 하나는 리액트 요소는 렌더링이 발생할때마다 새로 생성되지만 파이버는 가급적이면 재사용된다는 점이다.
리액트에는 아래와 같이 파이버를 생성하는 다양한 함수가 존재한다.
function createFiber(tag, pendingProps, key, mode) {
return new FiberNode(tag, pendingProps, key, mode);
}
function createFiberFromElement(element, mode, expirationTime) {
let owner = null;
const type = element.type;
const key = element.key;
const pendingProps = element.props;
const fiber = createFiberFromTypeAndProps(
type,
key,
pendingProps,
owner,
mode,
expirationTime,
);
return fiber;
}
위에서 볼 수 있듯이 파이버는 하나의 Element에 하나씩 생성되는 1:1관계를 지니고 있음을 이해하자. 또한 파이버와 1:1로 연결되는 것은 리액트의 컴포넌트뿐 아니라 HTML의 DOM노드나 그외 다른것일 수도 있다.
FunctionComponent (0): 함수형 컴포넌트를 나타냅니다.
ClassComponent (1): 클래스형 컴포넌트를 나타냅니다.
IndeterminateComponent (2): 아직 함수형인지 클래스형인지 결정되지 않은 컴포넌트를 나타냅니다.
HostRoot (3): 루트 Fiber를 나타냅니다. 이는 React 애플리케이션의 최상위 레벨을 나타냅니다.
HostComponent (5): DOM 엘리먼트를 나타냅니다. 예를 들어, <div>, <span> 등의 HTML 태그에 해당합니다.
HostText (6): 텍스트 노드를 나타냅니다.
Fragment (7): React Fragment를 나타냅니다. Fragment는 DOM 트리에 별도의 노드를 추가하지 않고 여러 자식을 그룹화하는 데 사용됩니다.
Mode (8): React의 동작 모드를 나타냅니다. 예를 들어, Strict Mode, Concurrent Mode 등이 여기에 해당합니다.
ContextConsumer (9): Context의 소비자를 나타냅니다.
ContextProvider (10): Context의 제공자를 나타냅니다.
ForwardRef (11): Ref를 하위 컴포넌트로 전달하는 데 사용되는 ForwardRef 컴포넌트를 나타냅니다.
Profiler (15): React Profiler를 나타냅니다.
SuspenseComponent (16): Suspense 컴포넌트를 나타냅니다.
MemoComponent (14): Memo 컴포넌트를 나타냅니다.
SimpleMemoComponent (15): SimpleMemo 컴포넌트를 나타냅니다.
LazyComponent (17): Lazy 컴포넌트를 나타냅니다.
IntrinsicComponent (18): 호스트 컴포넌트를 나타냅니다.
조금 파이버 객체를 잘 들여다 보면 children이 없고 child만 존재함을 알 수 있다. 파이버는 여러개의 자식이 있는 경우 첫번째 자식을 child로 두고, 두번째 자식은 첫번째 자식의 형제(sibling)으로 구성하는 방식을 가지고 있다.
이렇게 생성된 파이버는 state가 변경되거나 DOM의 변경이 필요한 시점에 실행되며 작은단위로 처리되거나 우선순위에 따라 다르게 처리시키는 등 유연한 처리가 가능하다.
리액트 팀은 사실 리액트가 가상 DOM이 아닌 Value UI 즉 값를 가진 UI를 관리하는 라이브러리임을 알려준 바가 있다.
리액트의 핵심 원칙은 UI를 문자열,숫자,배열과 같은 값으로 관리를 한다는 것이며 변수에 이 UI값을 보관하고 표현하는 것이다.
리액트 파이버 트리
파이버 트리는 리액트 내부에 두개 존재한다.
1. 현재 모습을 담은 파이버 트리
2. 작업중인 상태를 나타내는 workInProgress 트리
리액트 파이버의 작업이 끝나면 리액트는 단순히 포인터만 변경하여 workInProgress트리를 현재 트리로 바꿔 버리는데, 이를 더블 버퍼링이라 한다.
더블 버퍼링?
리액트에서 나온 개념은 아니며 컴퓨터 그래픽 분야에서 사용하는 용어이다. 사용자에게 다 그려지지 않은 그래픽을 보여주지 않기위해 보이지 않는 곳에서 그림을 그리고, 상태를 바꾸는 기법을 의미한다.
아무튼 리액트에서도 더블 버퍼링 기법을 활용해서 렌더링을 진행한다.
현재 UI렌더링을 위해 존재하는 트리인 current를 기준으로 모든 작업이 시작된다. 여기서 업데이트가 발생하는 경우 파이버는 리액트에서 새로 받은 데이터로 새로운 workInProgress 트리를 빌드한다.
이 workInProgress트리의 빌드가 끝나면 current트리가 workInProgress트리로 변경되는 방식이 일어난다!
파이버의 작업순서
1. beginWork()함수로 파이버 작업을 수행하며 자식이 없는 파이버를 만날때까지 시작된다.
2. completeWork()함수로 파이버 작업을 완료한다.
3. 형제가 있다면 형제로 넘어간다.
4. (2)(3)번 작업이 끝난다면 자신의 작업이 끝났음을 알린다.
위 방식으로 만든 트리에서 setState등 생타 변화가 일어났다고 생각해보자. setState로 인해 업데이트가 일어난다면 workInProgress 트리를 활용해 다시 트리를 만들지만, 지금은 파이버가 존재하기 때문에 업데이트된 props를 받아 파이버 내부에서 처리하게 된다.
즉 가급적 새로운 파이버를 생성하지 않고 기존에 있는 객체를 재활용하기 위해 내부 속성값을 초기화하거나 바꾸는 형태로 트리를 업데이트하게 된다.
위에서 리액트가 초기에는 스택 구조로 렌더링을 했다고 했는데 바로 이 트리 업데이트 과정을 재귀적으로 만들었었다. 현재는 우선순위가 높은 다른 업데이트가 오면 현재 진행중인 작업을 일시 중단하거나 만들거나 폐기할 수 있다.
따라서 리액트는 애니메이션이나 사용자가 입력하는 작업 등을 우선순위가 높은 작업으로 두어 최적의 순위로 작업을 완료할 수 있게 만들었다.
정리
리액트 컴포넌트에 대한 정보를 1:1로 가지고 있는것이 파이버이며 비동기 작업으로 일어난다.
하지만 실제 브라우저구조인 DOM은 동기적으로 일어나야 하기에 이런 작업을 가상에서 먼저 실행한 후, 최종적인 결과물만 실제 브라우저 DOM에 적용하게 된다.
가상 DOM vs 파이버
엄밀히 가상 DOM은 웹 에플리케이션에만 쓸 수 있는 표현이다. 리액트 파이버는 리엑트네이티브와 같이 웹 이외환경에서도 사용되기 때문에 완전히 같은 개념이라고 볼 수 는 없다. 다만 파이버를 활용해서 조정되는 과정은 동일하다!
'FrontEnd > Deep Dive' 카테고리의 다른 글
[React] Deep Dive 모던 리액트(7) 렌더링 & 메모이제이션 (1) | 2023.12.20 |
---|---|
[React] Deep Dive 모던 리액트(6) 클래스형&함수형 컴포넌트 (0) | 2023.12.19 |
[React] Deep Dive 모던 리액트(4) JSX (1) | 2023.12.18 |
[React] Deep Dive 모던 리액트(3)타입스크립트 (0) | 2023.12.15 |
[React] DeepDive 모던리액트(2) 동등 비교 (0) | 2023.12.14 |