-
4. HTML 프론트엔드 개발자라면 알고 있어야 할 브라우저의 동작 과정-4기초정보 2021. 4. 1. 16:46
3) 레이아웃 또는 리플로우
렌더 트리 구성이 끝나면 레이아웃 단계가 이어집니다. 모질라에서는 이 과정을 리플로우(reflow)라고 부르기도 합니다.
레이아웃 단계에서는 렌더 트리에서 계산되지 않았던 노드들의 크기와 위치, 레이어 간 순서와 같은 정보를 계산하여 좌표에 나타냅니다. 이 과정은 HTML의 루트 오브젝트로부터 재귀적으로 실행이 됩니다.
이 과정을 시각적으로 잘 나타낸 영상이 있어 소개해드리고자 합니다.
한편, 레이아웃은 계산의 범위에 따라 전역적 레이아웃(Global Layout)과 증분적 레이아웃(Incremental Layout)으로 구분할 수 있습니다.
전역적 레이아웃은 말 그대로 화면 전체의 레이아웃을 계산하는 것입니다. 가령 새로운 폰트를 적용하거나, 폰트 사이즈가 바뀌거나, 뷰포트의 사이즈 변경 같은 경우가 있을 때 전체 레이아웃을 다시 계산합니다. offsetHeight 같은 일부 DOM 관련 JavaScript API에 접근을 하는 경우에도 전역적 레이아웃이 다시 계산되기도 합니다.
이러한 전역적 레이아웃 단계는 모든 렌더 트리 노드에 대해 기하학적인 계산을 수행하기 때문에, 노드가 많아지게 된다면 그 속도가 느려지게 됩니다. 따라서 브라우저에서는 자체적인 최적화 로직을 탑재하고 있습니다.
그 중 하나가 바로 더티 비트 시스템(Dirty bit system)입니다. 더티 비트 시스템은 특정 엘리먼트의 레이아웃이 변경이 되었을 때, 렌더 트리를 처음부터 탐색하면서 레이아웃을 계산하지 않고 특정한 부분만 다시 계산하여 리소스의 낭비를 줄이는 최적화 방법입니다.
증분적 레이아웃은 이러한 더티 비트 시스템을 활용합니다. 레이아웃 과정에서 렌더 트리를 재귀적으로 탐색을 하다가 더티한 엘리먼트들, 즉 레이아웃의 변경이 발생해야 하는 엘리먼트들을 만나게 되면, 그 계산을 즉시 수행하는 것이 아니라 스케쥴러를 통해 비동기로 일괄 작업(batch)을 진행합니다. 이를 통해 연산의 횟수와 범위를 줄일 수 있습니다.
하지만 아주 복잡한 레이아웃의 경우에는 브라우저 단에서의 최적화만으로는 충분하지 않기 때문에, 프론트엔드 개발자 역시 레이아웃 과정의 연산을 최소화하도록 신경을 써야 합니다. 때문에 브라우저처럼 행동하는 것이 필요합니다. DOM의 레이아웃과 관련된 값을 직접 읽어오거나 변화를 주는 JavaScript 코드를 작성해야 한다면, 그러한 구문들을 최대한 묶어야 합니다.
const divWidth = div1.clientWidth; div2.style.width = `${diwWidth}px`; const divHeight = div1.clientHeight; div2.style.height = `${divHeight}px`;
위의 코드는 div1 이라는 태그의 너비와 높이 값을 읽어와 div2 의 인라인 스타일에 적용하는 방식으로 DOM을 직접 수정하는 코드입니다.
위에서 언급한 바와 같이, 브라우저는 더티한 레이아웃이 발생할 때마다 증분적 레이아웃을 수행하기 위해 레이아웃 계산을 스케쥴러에 등록합니다. 위의 코드는 div2 의 너비를 변경한 후 다시 div1 의 높이를 불러오기 때문에, 두 과정 사이에서 발생했을 수도 있는 레이아웃의 변경 때문에 불필요한 계산이 추가가 됩니다. 즉, 레이아웃 관련 값을 읽어오는 부분과 레이아웃을 수정하는 코드가 혼용되었기 때문에 최적화 관점에서 문제가 됩니다.
따라서 위 코드는 아래와 같이 수정함으로써 계산 과정을 줄일 수 있습니다.
const divWidth = div1.clientWidth; const divHeight = div1.clientHeight; div2.style.width = `${diwWidth}px`; div2.style.height = `${divHeight}px`;
4) 페인트
마지막 단계는 페인트 단계입니다. 페인트 단계는 말 그대로 레이아웃 단계를 통해 화면에 배치된 엘리먼트들에게 색을 입히고 레이어의 위치를 결정하는 단계입니다. 이 단계 역시 루트 오브젝트로부터 재귀적으로 실행이 됩니다. 또한 레이아웃과 마찬가지로 페인팅에도 전역적 페인팅과 증분적 페인팅이 있습니다.
즉, 문서가 클수록 브라우저가 수행해야 하는 작업도 더 많아지며, 스타일이 복잡할수록 페인팅에 걸리는 시간도 늘어납니다. 예를 들어, 단색은 페인트하는 데 시간과 작업이 적게 필요한 반면, 그림자 효과는 계산하고 페인트 하는데 시간과 작업이 더 필요합니다.
페인팅에는 그 순서가 있는데, 이는 z-index 축을 이용한 쌓임 맥락(Stacking context)과도 일맥상통합니다. 때문에 z-index가 낮은 순서대로 먼저 페인팅이 됩니다.
한편 블록 단위에서의 페인팅 순서는 CSS 페인팅 명세에 따르면 다음과 같습니다.
- background-color
- background-image
- border
- children
- outline
따라서 만약 background-color와 background-image 가 함께 세팅되어 있고, background-image로 설정한 외부 리소스의 크기가 크다면 background-color 를 먼저 보게 될 것이고, 나중에 이미지가 완전히 로드된 후 background-image로 교체가 될 것입니다.
가상 DOM
한편, 글을 마무리하기 전에 오늘 정리한 내용을 바탕으로 가상 DOM(Virtual DOM)의 출현 배경에 대해서도 이야기를 꺼내볼 수 있을 것 같습니다. React와 Vue를 써 보신 분이라면 반드시 들어봤을 용어일텐데, 이 가상 DOM이라는 단어를 들어봤어도 등장 배경에 대해 제대로 이해하지 못하고 쓰는 경우도 있을 수 있다고 생각합니다.
일반적으로 중요 렌더링 경로는 초당 60회 정도의 주기로 계산을 수행합니다. 이때 가장 비용이 많이 드는 단계가 바로 레이아웃 단계와 페인트 단계입니다. 때문에 성능 최적화를 위해서는 두 단계에서의 연산을 최소화하는 것이 중요합니다.
JavaScript를 이용해 DOM을 직접 조작하면, 변경 사항이 있을 때마다 잠재적인 레이아웃 단계와 페인트 단계를 초래하게 됩니다. 만약 10개의 DOM 노드를 for 문으로 일일이 수정하게 되면, 하나의 노드에 수정 사항이 생길 때마다 화면을 다시 그리는 과정을 거쳐야 할 수 있습니다. 즉 10개를 한 번에 수정하는 것이 아니라, 하나씩 수정된 노드가 10번에 걸쳐서 다시 화면에 그려질 수 있다는 이야기입니다. 때문에 일반적으로 DOM을 직접 조작하는 것은 비용이 크다고 이야기하죠.
한편 가상 DOM은 실제로 렌더링되지는 않았지만, 실제 DOM 구조를 반영한 상태로 메모리에 있는 가상의 DOM입니다. 메모리 상에 있고, 실제 화면에 그려야할 필요는 없기 때문에 실제 DOM보다는 연산 비용이 적습니다. 가상 DOM은 이러한 특징을 바탕으로 위에서 말한 변경 사항들을 한 번에 묶어서 실제 DOM에 반영을 합니다. 물론 레이아웃 단계와 페인트 단계에서 한 번에 변경되어야 하는 사항은 많아집니다. 대신 단 한 번의 계산만으로도 바뀐 DOM을 적용할 수 있기 때문에 연산의 횟수는 최소한이 됩니다.
사실 이 과정은 DOM에 변경사항을 유발하는 스크립트를 묶어서 실행하는 방법으로도 가능합니다. 하지만 가상 DOM을 사용함으로서 이 과정을 모두 자동화할 수 있고, 개발자는 관리할 포인트를 줄일 수 있습니다. 이러한 가상 DOM을 사용하는 대표적인 라이브러리, 프레임워크가 바로 React와 Vue입니다.
'기초정보' 카테고리의 다른 글
3. HTML 프론트엔드 개발자라면 알고 있어야 할 브라우저의 동작 과정-3 (0) 2021.04.01 2. HTML 프론트엔드 개발자라면 알고 있어야 할 브라우저의 동작 과정-2 (0) 2021.04.01 1. HTML 프론트엔드 개발자라면 알고 있어야 할 브라우저의 동작 과정-1 (0) 2021.04.01 1.http 상태코드(에러코드값) (0) 2020.03.03