최종 수정 : 2024-06-01
DOM
가상 DOM을 이해하기 위해서는 DOM에 대한 이해가 필요하다. DOM(Document Object Model)은 객체로, 웹페이지에 대한 인터페이스로 브라우저가 웹페이지의 콘텐츠와 구조를 어떻게 보여줄지에 대한 정보를 담고 있다. 이 문서 구조는 XML이나 HTML로 작성돼 있다.
웹 브라우저는 DOM을 활용하여 객체에 JS와 CSS를 적용한다. DOM은 트리 형태라서 특정 노드를 찾거나 수정하거나 제거하거나 원하는 곳에 삽입할 수 있다. 그러나 DOM은 동적 UI에 최적화되어 있지 않다는 치명적인 단점이 있다. 예를 들어, 스크롤을 내릴 때마다 새로 생기는 웹 페이지나 버튼을 클릭했을 때 나타나는 이미지나 글자 등, 이처럼 규모가 큰 웹 애플리케이션에서 DOM에 직접 접근하여 변화를 주다 보면 느려지기 시작한다. 이를 두고 "요즘 자바스크립트 엔진은 매우 빠른 반면, DOM은 느리다"라고 하는데, 정확한 말은 아니다.
DOM 자체는 빠른 편이다. DOM 자체를 읽고 쓸 때의 성능은 자바스크립트 객체를 처리할 때의 성능과 비교하여 다르지 않다. 단, 웹 브라우저 단에서 DOM에 변화가 일어나면 웹 브라우저가 CSS를 다시 연산하고, 레이아웃을 구성하고, 페이지를 리페인팅한다. 이 과정에서 시간이 소요되는 것뿐이다.
HTML 마크업을 시각적인 형태로 변환하는 것은 웹 브라우저가 하는 주 역할이므로, 이를 처리할 때 컴퓨터 자원을 사용하는 것은 필연적이다. 그 대신 DOM을 최소한으로 조작하여 작업을 차리하는 방식으로 개선할 수 있는데, 리액트에서는 Virtual DOM 방식을 사용하여 DOM 업데이트를 추상화함으로써 DOM 처리 횟수를 최소화하고 효율적으로 진행한다.
브라우저 렌더링 과정
1. 브라우저가 사용자가 요청한 주소를 방문해 HTML 파일을 다운로드 한다.
2. 브라우저의 렌더링 엔진이 HTML을 파싱해 DOM Tree를 만든다.
3. 2번 과정에서 CSS파일을 만나면 그것을 다운로드 한다.
4. 브라우저의 렌더링 엔진이 이 CSS도 파싱해 CSSOM를 만든다.
5. 2번에서 만든 DOM 노드를 순회한다. display: none; 같이 사용자 화면에 보이지 않는 요소는 방문하지 않는다. 트리를 분석하는 과정을 조금이라도 빠르게 처리하기 위함이다.
6. 눈에 보이는 노드를 대상에 대한 CSSOM 정보를 찾고 발견한 CSS 스타일 정보를 적용한다.
1) 레이아웃(layout, reflow) : 각 노드가 브라우저 화면의 어느 좌표에 정확히 나타나야 하는지 계산하는 과정
2) 페인팅(painting) : 레이아웃 단계를 거친 노드에 색과 같은 실제 유효한 모습을 그리는 과정
브라우저는 이러한 과정을 거쳐 웹페이지를 렌더링 한다.
가상 DOM의 탄생 배경
요즘 앱은 렌더링된 이후 정보를 보여주는 데 그치지 않고, 사용자의 인터랙션을 통해 다양한 정보를 노출한다. 즉, 렌더링 이후에도 사용자의 인터랙션에 따라 웹페이지가 변경되는 상황 또한 고려해야 한다.
1) 특정한 요소의 색상 변경
페인팅만 일어나므로 비교적 빠르게 처리할 수 있다.
2) 어떤 특정한 요소의 노출 여부 및 사이즈 변경
요소의 위치와 크기를 재계산하는 레이아웃이 일어나고, 리페인팅 해야 하므로 많은 비용이 든다.
3) DOM 변경이 일어나는 요소가 많은 자식 요소를 갖고 있음
하위 자식 요소도 덩달아 변경돼야 하기 때문에 더 많은 비용을 브라우저와 사용자가 지불하게 된다.
이러한 렌더링 이후의 추가 렌더링 작업은 싱글 페이지 애플리케이션(Single Page Application)에서 더욱 많아진다. 페이지가 변경되는 경우, 하나의 페이지에서 계속해서 요소의 위치를 재계산하게 된다. 물론, 이러한 SPA의 특징 덕분에 사용자는 페이지의 깜빡임 없이 자연스러운 웹페이지 탐색이 가능하지만 그만큼 DOM을 관리하는 과정에서 부담해야 하는 비용이 커진다.
이러한 DOM의 모든 변경 사항을 추적하는 것은 개발자 입장에서 너무나 수고스러운 일이다. 무엇보다, 모든 DOM의 변경보다는 결과적으로 만들어진 DOM 결과물 하나만 알고 싶어진다. 이러한 문제를 해결하기 위해 탄생한 것이 바로 가상 DOM이다.
Virtual DOM
말 그대로 실제 브라우저의 DOM이 아닌 리액트가 관리하는 가상의 DOM을 의미한다. 웹페이지가 표시해야 할 DOM을 메모리에 저장하고 리액트가 실제 변경에 대한 준비가 완료됐을 때 실제 브라우저의 DOM에 반영한다. (여기에서 말하는 리액트는 package.json에 있는 react가 아닌 react-dom을 의미한다.)
# Virtual DOM의 장점
이처럼 메모리에서 계산하는 과정을 한 번 거치게 되면 실제로 여러 번 발생했을 렌더링 과정을 최소화할 수 있고 브라우저와 개발자의 부담을 덜 수 있다.
# Virtual DOM에 대한 오해와 정정
리액트의 이러한 방식이 일반적인 DOM을 관리하는 브라우저보 빠르다는 것은 오해이다. 리액트 개발자인 댄 아브라모프(dan_abramov)가 사실이 아니라고 분명히 얘기한 바 있다. 무조건 빠른 것이 아니라 대부분의 상황에서 웬만한 애플리케이션을 만들 수 있을 정도로 충분히 빠르다는 것이다.
리액트 매뉴얼에는 이렇게 명시되어 있다.
우리는 다음 문제를 해결하려고 리액트를 만들었습니다.
지속적으로 데이터가 변화하는 대규모 애플리케이션 구축하기
결국에는 적절한 곳에 사용해야 리액트가 지닌 진가를 발휘할 수 있다. 코드 최적화를 하다보면 DOM 작업이 느려지는 문제를 개선할 수도 있고, 또 작업이 간단할 때는 오히려 리액트를 사용하지 않는 편이 더 나은 성능을 보이기도 한다.
리액트와 Virtual DOM이 언제나 제공할 수 있는 것은 바로 업데이트 처리 간결성이다. UI를 업데이트하는 과정에서 생기는 복잡함을 해소하고, 더욱 쉽게 업데이트에 접근할 수 있다.
Q. 리액트는 이 가상 DOM을 어떻게 관리하는가?
A. 리액트 파이버(React Fiber)
리액트에서 관리하는 평범한 자바스크립트 객체이며, 가상 DOM과 렌더링 과정 최적화를 가능하게 해준다. 파이버는 파이버 재조정자(fiber reconciler)가 관리하는데, 가상 DOM과 실제 DOM을 비교해 변경 사항을 수집하며, 둘 사이에 차이가 있으면 변경에 관련된 정보를 가지고 있는 파이버를 기준으로 화면에 렌더링을 요청하는 역할을 수행한다. 재조정(reconciliation)이란 리액트에서 어떤 부분을 새롭게 렌더링해야 하는지 가상 DOM과 실제 DOM을 비교하는 작업(알고리즘)이라고 이해하면 된다.
리액트 파이버의 목표
반응성 문제를 해결한다.
1) 작업을 작은 단위로 분할하고 쪼갠 다음, 우선순위를 매긴다.
2) 이러한 작업을 일시 중지하고 나중에 재시작 할 수 있다.
3) 이전 작업을 다시 사용하거나 폐기할 수 있다.
리액트 파이버의 탄생 이유
한 가지 중요한 것은 이러한 모든 과정이 비동기로 일어난다는 점이다. 과거 리액트의 조정 알고리즘은 스택 알고리즘으로 이루어져 있어 동기적으로 작업이 이루어졌다. 결국 이는 리액트의 비효율성으로 이어졌다. 사용자가 입력할 때마다 스택에 쌓이는 작업이 많아질수록 리액트는 동기식으로 이를 처리하다보면, 많은 시간이 소요될 수도 있다. 최악의 경우, 글자 입력에 지연도 생길 수 있는 것이다. 이러한 기존 렌더링 스택의 비효울성을 타파하기 위해 리액트 팀은 이 스택 조정자 대신 '파이버'라는 개념을 탄생시켰다.
파이버는 어떻게 구현돼 있는가?
일단 하나의 작업 단위로 구성돼 있다. 이러한 작업 단위를 하나씩 처리하고 finishedWork()라는 작업으로 마무리한다. 그리고 이 작업을 커밋해 실제 브라우저 DOM에 가시적인 변경 사항을 만들어 낸다.
1) 렌더 단계에서 리액트는 사용자에게 노출되지 않는 모든 비동기 작업을 수행한다. 그리고 이 단계에서 앞서, 우선순위를 지정하거나 중지시키거나 버리는 등의 작업이 일어난다.
2) 커밋 단계에서는 앞서 언급한 것처럼 DOM에 실제 변경 사항을 반영하기 위한 작업, commitWork()가 실행되는데, 이 과정은 동기식으로 일어나고 중단될 수도 없다.
파이버의 특징
파이버는 리액트 요소와 유사하다고 느낄 수 있지만, 결정적인 차이점이 존재한다. 리액트 요소는 렌더링이 발생할 때마다 새롭게 생성되지만 파이버는 가급적이면 재사용된다. 컴포넌트가 최초로 마운트되는 시점에 생성되어 이후에는 가급적 재사용된다.
생성된 파이버는 state가 변경되거나 생명주기 메서드가 실행되거나 DOM의 변경이 필요한 시점 등에 실행된다. 그리고 리액트가 파이버를 처리할 때마다 이러한 작업을 직접 바로 처리하기도 하고 스케줄링하기도 한다는 점은 중요하다. 즉, 작은 단위로 나눠서 처리할 수도, 애니메이션과 같은 우선순위가 높은 작업은 가능한 한 빠르게 처리하거나, 낮은 작업을 연기시키는 등 좀 더 유연하게 처리된다.
파이버의 객체 값에서도 알 수 있듯이 리액트의 핵심 원칙은 UI를 문자열, 숫자, 배열과 같은 값으로 관리한다는 것이다. 변수에 이러한 UI 관련 값을 보관하고, 리액트의 자바스크립트 코드 흐름에 따라 이를 관리하고, 표현하는 것이 바로 리액트다.
리액트 파이버 트리
사실 리액트 내부에서 두 개가 존재한다. 하나는 현재 모습을 담은 파이버 트리이고, 다른 하나는 작업 중인 상태를 나타내는 workInProgress 트리다. 리액트 파이버의 끝나면 리액트는 단순히 포인터만 변경해 workInProgress 트리를 현재 트리로 바꿔버린다. 이러한 기술을 더블 버퍼링이라고 한다. 컴퓨터 그래픽 분야에서 사용하는 용어인데, 보이지 않는 곳에서 그다음으로 그려야 할 그림을 미리 그린 다음, 이것이 완성되면 현재 상태를 새로운 그림으로 바꾸는 기법이다. 이러한 더블 버퍼링을 위해 트리가 두 개 존재하며, 이 더블 버퍼링은 커밋 단계에서 수행된다.
최초 렌더링 시에는 모든 파이버를 새롭게 만들어야 했지만, 이제는 파이버가 이밎 존재하므로 되도록 새로 생성하지 않고 기존 파이버에서 업데이트된 props를 받아 파이버 내부에서 처리한다. "가급적 새로운 파이버를 생성하지 않는다"가 바로 이것이다. 일반적인 리액트 애플리케이션을 상상해 보면 이렇게 트리를 비교해서 업데이트하는 작업은 시도 때도 없이 일어난다. 이러한 반복적인 재조정 작업 때마다 새롭게 파이버 자바스크립트 객체를 만드는 것은 리소스 낭비라고 볼 수 있다. 따라서 가급적 객체를 새롭게 만들기보다는 기존에 있는 객체를 재솰용하기 위해 내부 속성값만 초기화하거나 바꾸는 형태로 트리를 업데이트 한다.
그리고 과거에는 이 작업을 동시식으로 처리했다. 때문에, 트리 업데이트 과정, 재귀적으로 하나의 트리를 순회해 새로운 트리를 만드는 작업은 중단될 수 없었다. 그러나 현재는 업데이트 작업을 일시 중단하거나 새롭게 만들거나, 폐기할 수도 있다. 또한 작업 단위를 나누어 우선순위를 할당하는 것 또한 가능하다. 리액트는 이러한 작업을 파이버 단위로 나눠서 수행한다. 즉 리액트 컴포넌트에 대한 정보를 1:1로 가지고 있는 것이 파이버이며, 이 파이버는 리액트 아키텍처 내부에서 비동기로 이뤄진다. 또한 엄밀히 말하자면 이는 오직 웹 애플리케이션에서만 통용되는 개념이다.
정리하자면, 가상 DOM과 파이버는 단순히 브라우저에 DOM을 변경하는 작업보다 빠르다는 이유로만 만들어진 게 아니다. 만약 이러한 도움 없이 개발자가 직접 DOM을 수동으로 하나하나 변경해야 한다면 어떤 값이 바뀌었는지, 또 그 값에 따라 어떤 값이 변경됐고 이와 관련된 것들이 무엇이었는지 파악하기가 매우 어려울 것이다. 이러한 어려움을 리액트 내부의 파이버와 재조정자가 내부적인 알고리즘을 통해 관리해 줌으로써 효울적인 유지보수 및 관리가 가능해진 것이다.
가상 DOM과 리액트의 핵심은 값으로 UI를 표현하는 것이다. 화면에 표시되는 UI를 자바스크립트의 값으로 관리하고 이러한 흐름을 효율적으로 관리하기 위한 메커니즘이 바로 리액트의 핵심이다.
참고 자료
리액트 모던 Deep Dive (p.128-143)
리액트를 다루는 기술 (p.39-41)
Virtual DOM 사진 출처https://www.scaler.com/topics/react/virtual-dom-in-react/
'프론트엔드 > React' 카테고리의 다른 글
제어 컴포넌트 / 비제어 컴포넌트 (0) | 2024.04.17 |
---|---|
리액트를 공부해야 하는 이유 + 장점과 단점 (0) | 2024.04.14 |
리액트 디자인 적용 (0) | 2024.04.11 |
State - Props - Childen (1) | 2024.04.09 |
SWR (0) | 2023.07.31 |
댓글