RenderingNG 심층 분석: LayoutNG 블록 조각화

Morten Stenshorne
Morten Stenshorne

블록 단편화프래그먼테이너라고 하는 하나의 프래그먼트 컨테이너 내부에 전체가 맞지 않을 때 CSS 블록 수준 상자 (예: 섹션 또는 단락)를 여러 프래그먼트로 분할합니다. 프래그먼트는 요소가 아니지만, 다중 열 레이아웃의 열이나 페이징된 미디어의 페이지를 나타냅니다.

단편화가 발생하려면 콘텐츠가 프래그먼테이션 컨텍스트 내에 있어야 합니다. 단편화 컨텍스트는 일반적으로 다중 열 컨테이너 (콘텐츠가 열로 분할됨) 또는 인쇄 (콘텐츠가 페이지로 분할됨)에 의해 설정됩니다. 여러 줄이 있는 긴 단락은 첫 번째 줄은 첫 번째 조각에 배치하고 나머지 줄은 후속 조각에 배치하도록 여러 조각으로 분할해야 할 수 있습니다.

두 개의 열로 나누어진 텍스트 단락입니다.
이 예시에서는 다중 열 레이아웃을 사용하여 단락을 두 개의 열로 분할했습니다. 각 열은 프래그먼트화된 흐름의 프래그먼트를 나타내는 프래그먼트입니다.

블록 단편화는 잘 알려진 또 다른 유형의 단편화인 '줄 바꿈'이라고도 하는 줄 파편화와 유사합니다. 둘 이상의 단어 (텍스트 노드, 모든 <a> 요소 등)로 구성되고 줄바꿈을 허용하는 인라인 요소는 여러 프래그먼트로 분할될 수 있습니다. 각 프래그먼트는 다른 줄 상자에 배치됩니다. 라인 상자는 열과 페이지의 프래그먼트테이너와 동일한 인라인 파편화입니다.

LayoutNG 블록 조각화

LayoutNGBlockFragmentation은 Chrome 102에서 처음 출시된 LayoutNG용 조각화 엔진을 재작성한 것입니다. 데이터 구조 측면에서는 여러 개의 NG 이전 데이터 구조를 프래그먼트 트리에서 직접 표현되는 NG 프래그먼트로 대체했습니다.

예를 들어 이제 작성자가 헤더 바로 뒤에서 중단을 방지할 수 있도록 'break-before' 및 'break-after' CSS 속성의 'avoid' 값을 지원합니다. 페이지의 마지막 항목이 머리글인데 섹션의 콘텐츠가 다음 페이지에서 시작하면 어색해 보일 때가 많습니다. 헤더 에서 중단하는 것이 좋습니다.

제목 정렬 예
그림 1. 첫 번째 예시에서는 페이지 하단에 제목을 표시하고 두 번째 예시에서는 관련 콘텐츠와 함께 다음 페이지 상단에 제목을 표시합니다.

Chrome은 또한 파편화 오버플로를 지원하므로, 모놀리식 (깨질 수 없는 것으로 추정됨) 콘텐츠가 여러 열로 분할되지 않고 그림자 및 변환과 같은 페인트 효과가 올바르게 적용됩니다.

이제 LayoutNG의 블록 조각화가 완료되었습니다.

Chrome 102에서 제공되는 핵심 단편화 (라인 레이아웃, 부동 소수점 수, 흐름 이탈 포지셔닝을 비롯한 블록 컨테이너) Flex 및 그리드 단편화는 Chrome 103에서 제공되고 테이블 단편화는 Chrome 106에서 제공됩니다. 마지막으로, 인쇄는 Chrome 108에서 제공됩니다. 블록 단편화는 레이아웃을 수행하기 위해 기존 엔진에 종속된 마지막 기능이었습니다.

Chrome 108부터 더 이상 레이아웃을 실행하는 데 기존 엔진이 사용되지 않습니다.

또한 LayoutNG 데이터 구조는 페인팅과 히트 테스트를 지원하지만 offsetLeftoffsetTop와 같은 레이아웃 정보를 읽는 JavaScript API에는 일부 기존 데이터 구조를 사용합니다.

NG를 사용하여 모든 것을 배치하면 CSS 컨테이너 쿼리, 앵커 포지셔닝, MathML맞춤 레이아웃(Houdini)과 같은 LayoutNG 구현만 있고 기존 엔진에 대응하는 기능은 없는 새로운 기능을 구현하고 제공할 수 있습니다. 컨테이너 쿼리의 경우 조금 전에 제공하면서 인쇄가 아직 지원되지 않는다고 개발자에게 경고했습니다.

Google은 2019년에 일반 블록 컨테이너 레이아웃, 인라인 레이아웃, 부동 소수점 수 및 외부 흐름으로 구성된 LayoutNG의 첫 번째 부분을 공개했지만, Flex, 그리드 또는 테이블에 대한 지원은 제공하지 않았으며 블록 조각화는 전혀 지원하지 않았습니다. 플렉스, 그리드, 테이블 및 블록 단편화와 관련된 모든 것에 기존 레이아웃 엔진을 사용하는 것으로 대체됩니다. 이는 단편화된 콘텐츠 내의 블록, 인라인, 플로팅 및 흐름 외부 요소에 대해서도 사실이었습니다. 보시다시피 이처럼 복잡한 레이아웃 엔진을 그 자리에서 업그레이드하는 것은 매우 정교한 작업입니다.

또한 2019년 중반까지 LayoutNG 블록 조각화 레이아웃의 핵심 기능 대부분이 이미 플래그 뒤에 구현되었습니다. 그래서, 왜 배송이 오래 걸리는 걸까요? 간단히 말하자면 단편화는 시스템의 여러 기존 부분과 올바르게 공존해야 하며 모든 종속 항목이 업그레이드될 때까지 삭제하거나 업그레이드할 수 없습니다.

기존 엔진 상호작용

기존 데이터 구조는 여전히 레이아웃 정보를 읽는 JavaScript API를 담당하므로, 이해할 수 있는 방식으로 기존 엔진에 데이터를 다시 써야 합니다. 여기에는 LayoutMultiColumnFlowThread와 같은 기존 다중 열 데이터 구조를 올바르게 업데이트하는 작업이 포함됩니다.

기존 엔진 대체 감지 및 처리

LayoutNG 블록 조각화로 아직 처리할 수 없는 콘텐츠가 내부에 있을 때 기존 레이아웃 엔진으로 대체해야 했습니다. 핵심 LayoutNG 블록 조각화에는 플렉스, 그리드, 테이블 및 인쇄되는 모든 항목이 포함되었습니다. 이는 레이아웃 트리에서 객체를 만들기 전에 기존 대체의 필요성을 감지해야 했기 때문에 특히 까다로웠습니다. 예를 들어 다중 열 컨테이너 상위 항목이 있는지 알기 전에 그리고 어떤 DOM 노드가 형식 지정 컨텍스트가 될지 알기 전에 감지해야 했습니다. 완벽한 해결책은 없는 닭과 달걀 문제입니다. 하지만 유일한 오탐이 거짓양성 (실제로 필요하지 않을 때 기존 버그로 대체)이라면 괜찮습니다. 해당 레이아웃 동작의 버그는 새로운 버그가 아니라 Chromium에 이미 있는 버그이기 때문입니다.

페인트를 칠하기 전 나무 산책

사전 페인트는 레이아웃 후 페인트 전에 하는 작업입니다. 주요 과제는 여전히 레이아웃 객체 트리를 거쳐야 하지만 이제 NG 프래그먼트가 있다는 것입니다. 그렇다면 이를 어떻게 처리할 수 있을까요? 레이아웃 객체와 NG 프래그먼트 트리를 동시에 걷습니다. 이는 두 트리 간 매핑이 간단하지 않기 때문에 상당히 복잡합니다.

레이아웃 객체 트리 구조는 DOM 트리의 구조와 매우 비슷하지만 프래그먼트 트리는 레이아웃의 출력일 뿐이며, 프래그먼트 트리에 대한 입력이 아닙니다. 프래그먼트 트리는 인라인 단편화 (줄 프래그먼트) 및 블록 조각화 (열 또는 페이지 프래그먼트)를 비롯한 모든 단편화의 효과를 실제로 반영하는 것 외에도 포함 블록과 해당 프래그먼트를 포함 블록으로 사용하는 DOM 하위 요소 간에 직접적인 상위-하위 관계가 있습니다. 예를 들어 프래그먼트 트리에서 절대 위치로 배치된 요소에 의해 생성된 프래그먼트는 포함된 블록 프래그먼트의 직접적인 하위 요소가 됩니다. 이는 흐름이 벗어난 위치에 있는 하위 요소와 이를 포함하는 블록 사이에 상위 체인에 다른 노드가 있더라도 마찬가지입니다.

단편화 내부에 외부 흐름이 배치된 요소가 있으면 훨씬 더 복잡할 수 있습니다. 외부 프래그먼트가 프래그먼트가 포함 블록으로 생각하는 하위 요소가 아니라 프래그먼트의 직접적인 하위 요소가 되기 때문입니다. 이는 기존 엔진과 병용하기 위해 해결해야 하는 문제였습니다. 향후에는 이 코드를 단순화할 수 있을 것입니다. LayoutNG는 모든 최신 레이아웃 모드를 유연하게 지원하도록 설계되었기 때문입니다.

기존 단편화 엔진의 문제

웹 초기에 설계된 기존 엔진에는 인쇄를 지원하기 위해 기술적으로는 단편화가 존재했더라도 실제로 단편화라는 개념이 없습니다. 단편화 지원은 단순히 추가 (인쇄)하거나 개조 (다중 열)한 것이었습니다.

단편화 가능한 콘텐츠를 배치할 때 기존 엔진은 너비가 열 또는 페이지의 인라인 크기이고 높이는 콘텐츠를 포함하는 데 필요한 높이만큼 긴 스트립에 모든 것을 배치합니다. 이 긴 스트립은 페이지에 렌더링되지 않습니다. 가상 페이지를 렌더링한 후 최종 보기를 위해 다시 정렬되는 것이라고 생각하면 됩니다. 이는 신문 기사 전체를 하나의 열로 인쇄한 다음 가위를 사용하여 두 번째 단계로 여러 개로 자르는 것과 개념적으로 유사합니다. (예전에는 일부 신문이 실제로 이와 유사한 기술을 사용했습니다!)

기존 엔진은 스트립에서 가상의 페이지 또는 열 경계를 추적합니다. 이렇게 하면 경계를 넘어서 맞지 않는 콘텐츠를 다음 페이지나 열로 조금씩 이동할 수 있습니다. 예를 들어, 라인의 위쪽 절반만 엔진에서 인식하는 현재 페이지에 맞을 경우 (페이지 매김 스트럿)을 삽입하여 엔진이 다음 페이지의 상단이 있다고 가정하는 위치로 푸시다운합니다. 그런 다음 실제 단편화 작업('가위로 자르기 및 배치') 후 대부분을 미리 페인트 및 페인팅하는 동안 스트리핑 및 페인팅을 통해 스트리핑을 통해 레이아웃 부분 및 페인팅을 통해 스트립을 수행합니다. 이로 인해 단편화 (사양에서 요구하는 사항임) 이후에 변환 및 상대적 위치 지정과 같은 몇 가지 작업이 본질적으로 불가능해졌습니다. 또한 기존 엔진에서는 테이블 단편화를 일부 지원하지만 플렉스 또는 그리드 조각화는 전혀 지원되지 않습니다.

다음은 가위, 배치, 글루를 사용하기 전에 3열 레이아웃이 어떻게 기존 엔진에서 내부적으로 표현되는지를 보여주는 그림입니다 (높이가 지정되어 있으므로 4줄만 들어가지만 하단에는 여분의 공간이 있음).

콘텐츠가 끊기는 페이지 매김 스트럿이 포함된 열 1개, 열 3개로 화면 표시

기존 레이아웃 엔진은 레이아웃 중에 실제로 콘텐츠를 조각화하지 않기 때문에, 상대적 위치 지정 및 변환이 잘못 적용되고 상자 그림자가 열 가장자리에서 잘리는 등 이상한 아티팩트가 많이 있습니다.

다음은 텍스트 그림자를 사용한 예입니다.

기존 엔진은 이러한 문제를 제대로 처리하지 못합니다.

두 번째 열에 배치된 잘린 텍스트 그림자

첫 번째 열에 있는 줄의 텍스트 그림자가 어떻게 잘리고 대신 두 번째 열의 맨 위에 배치되는지 보이나요? 기존 레이아웃 엔진이 단편화를 이해하지 못하기 때문입니다.

다음과 같이 표시됩니다.

그림자가 올바르게 표시된 텍스트 열 2개

다음으로, 변환과 상자 그림자를 사용하여 좀 더 복잡하게 만들어 보겠습니다. 기존 엔진에서는 잘못된 클리핑 및 컬럼 블리드가 있습니다. 그 이유는 변환이 사양에 따라 레이아웃 후, 조각화 후 효과로 적용되어야 하기 때문입니다. LayoutNG 단편화를 사용하면 둘 다 올바르게 작동합니다. 이렇게 하면 한동안 단편화를 잘 지원한 Firefox와의 상호 운용성이 높아져 이 영역에서 대부분의 테스트도 통과합니다.

상자가 두 열로 잘못 나누어져 있습니다.

기존 엔진에는 세로로 긴 모놀리식 콘텐츠도 문제가 있습니다. 여러 프래그먼트로 분할할 수 없는 콘텐츠는 모놀리식입니다. 오버플로 스크롤이 있는 요소는 사용자가 직사각형이 아닌 영역에서 스크롤하는 것은 적합하지 않기 때문에 모놀리식입니다. 모놀리식 콘텐츠의 또 다른 예로 라인 상자와 이미지가 있습니다. 예를 들면 다음과 같습니다.

모놀리식 콘텐츠의 높이가 너무 높아 열 안에 들어가지 못하면 기존 엔진이 해당 콘텐츠를 잔인하게 잘라냅니다. 스크롤 가능한 컨테이너를 스크롤하려고 할 때 매우 '재미있는' 동작이 발생합니다.

LayoutNG 블록 조각화를 사용할 때와 마찬가지로 첫 번째 열을 오버플로하는 대신 다음을 실행합니다.

ALT_TEXT_HERE

기존 엔진은 강제 종료를 지원합니다. 예를 들어 <div style="break-before:page;">는 DIV 앞에 페이지 나누기를 삽입하지만 최적의 강제 해제 시점을 찾는 기능은 제한적으로만 사용할 수 있습니다. break-inside:avoid분리된 객체를 지원하지만, 예를 들어 break-before:avoid를 통해 요청된 경우 블록 간 중단을 방지하는 기능은 지원되지 않습니다. 다음 예를 살펴보세요.

텍스트가 두 개의 열로 나뉘어 있습니다.

여기서 #multicol 요소는 각 열에 5줄의 공간을 가지고 있습니다 (높이가 100픽셀이고 선 높이가 20픽셀이기 때문). 따라서 #firstchild는 모두 첫 번째 열에 맞을 수 있습니다. 그러나 동위 요소인 #secondchild에는 break-before:avoid가 있습니다. 즉, 콘텐츠에서 콘텐츠 사이에 광고 시간이 발생하지 않도록 합니다. widows 값이 2이므로 #firstchild의 두 줄을 두 번째 열에 푸시하여 모든 중단 방지 요청을 적용해야 합니다. Chromium은 이러한 기능들을 완벽하게 지원하는 최초의 브라우저 엔진입니다.

NG 단편화 작동 방식

NG 레이아웃 엔진은 일반적으로 CSS 상자 트리 깊이를 먼저 순회하여 문서를 배치합니다. 노드의 모든 하위 요소가 배치되면 NGPhysicalFragment를 생성하고 상위 레이아웃 알고리즘으로 반환하여 해당 노드의 레이아웃을 완료할 수 있습니다. 이 알고리즘은 프래그먼트를 하위 프래그먼트 목록에 추가하고, 모든 하위 프래그먼트가 완료되면 그 안에 모든 하위 프래그먼트를 포함하여 자체적으로 프래그먼트를 생성합니다. 이 메서드를 사용하면 전체 문서의 프래그먼트 트리가 생성됩니다. 하지만 이는 지나치게 단순화된 것입니다. 예를 들어 유출된 배치된 요소는 배치되기 전에 DOM 트리의 위치에서 포함 블록으로 버블링되어야 합니다. 편의상 여기에서는 고급 세부정보를 무시하겠습니다.

LayoutNG는 CSS 상자 자체와 함께 레이아웃 알고리즘에 제약 조건 공간을 제공합니다. 이를 통해 레이아웃에 사용할 수 있는 공간, 새로운 서식 지정 컨텍스트가 설정되었는지 여부, 이전 콘텐츠의 중간 여백 축소 결과와 같은 정보를 알고리즘에 제공합니다. 제약 조건 공간은 프래그먼트의 배치된 블록 크기와 그 안의 현재 블록 오프셋도 알고 있습니다. 중단 위치를 나타냅니다.

블록 조각화가 관련된 경우 하위 요소의 레이아웃은 중단에서 중지되어야 합니다. 중단의 이유로는 페이지 또는 열의 공간 부족이나 강제 줄바꿈 등이 있습니다. 그런 다음 방문한 노드의 프래그먼트를 생성하고 단편화 컨텍스트 루트 (멀티콜 컨테이너 또는 인쇄의 경우 문서 루트)까지 돌아갑니다. 그런 다음 단편화 컨텍스트 루트에서 새 프래그먼트테이너를 준비하고 트리로 다시 내려가 중단 전에 중단했던 부분부터 다시 시작합니다.

중단 후 레이아웃을 재개하는 수단을 제공하는 데 중요한 데이터 구조를 NGBlockBreakToken이라고 합니다. 여기에는 다음 프래그먼트에서 레이아웃을 올바르게 재개하는 데 필요한 모든 정보가 포함되어 있습니다. NGBlockBreakToken은 노드와 연결되고 NGBlockBreakToken 트리를 형성하므로 재개해야 하는 각 노드가 표시됩니다. NGBlockBreakToken은 내부에서 중단되는 노드에 대해 생성된 NGPhysicalBoxFragment에 연결됩니다. 분할 토큰은 상위 요소로 전파되어 분할 토큰 트리를 형성합니다. 노드 내부가 아닌 노드 전에 중단해야 하는 경우 프래그먼트가 생성되지 않지만 상위 노드는 여전히 노드에 대해 'break-before' 중단 토큰을 생성해야 합니다. 그래야 다음 프래그먼트의 노드 트리에서 동일한 위치에 도달할 때 이를 배치할 수 있습니다.

분할은 프래그먼트 공간 (강제 종료)이 부족하거나 강제 중단이 요청될 때 삽입됩니다.

사양에는 최적의 강제 중단을 위한 규칙이 있으며 공간이 부족한 곳에 중단을 삽입하는 것이 항상 올바른 것은 아닙니다. 예를 들어 중단 위치 선택에 영향을 주는 break-before와 같은 다양한 CSS 속성이 있습니다.

레이아웃 중에 강제 중단 사양 섹션을 올바르게 구현하려면 정상 가능성이 있는 중단점을 추적해야 합니다. 이 기록은 중단 방지 요청을 위반할 소지가 있는 지점 (예: break-before:avoid 또는 orphans:7)에 공간이 부족할 경우 뒤로 돌아가서 가능한 한 마지막 중단점을 사용할 수 있음을 의미합니다. 가능한 각 중단점에는 '최후의 수단으로만 수행'부터 '완벽한 휴식처'와 그 사이에 일부 값을 포함하는 점수가 부여됩니다. 휴식 시간이 '완벽'으로 평가되면 중단해도 규칙이 위반되지 않는다는 의미입니다. 또한 공간이 부족한 시점에 해당 점수를 받았다면 더 좋은 방법을 찾을 필요가 없습니다. 점수가 '마지막 옵션'인 경우 중단점이 유효한 상태조차 아니더라도 더 나은 결과를 찾지 못한다면 단편화 오버플로를 피하기 위해 중단점이 발생할 수 있습니다.

일반적으로 유효한 중단점은 동위 요소 (줄 상자 또는 블록) 사이에만 발생하며 상위 요소와 첫 번째 하위 요소 사이에는 발생하지 않습니다 (클래스 C 중단점은 예외이지만 여기서는 설명하지 않아도 됨). 예를 들어 break-before:avoid가 포함된 블록 동위 요소 앞에는 유효한 중단점이 있지만 '완벽'과 '마지막 수단' 사이의 어딘가에 있습니다.

레이아웃 중에는 NGEarlyBreak라는 구조에서 지금까지 찾은 최상의 중단점을 추적합니다. 조기 중단은 블록 노드 앞이나 내부 또는 라인 (블록 컨테이너 라인 또는 가변 라인) 앞에 가능한 중단점입니다. 공간이 부족할 때 앞서 지나온 무언가의 깊숙한 어딘가에 최상의 중단점이 있는 경우, NGEarlyBreak 객체의 체인 또는 경로를 형성할 수도 있습니다. 예를 들면 다음과 같습니다.

이 경우에는 #second 바로 앞에 공간이 부족하지만 'break-before:avoid'가 있어 '위반 중단 방지'의 중단 위치 점수를 받습니다. 이때 '줄 3' 앞에 '#outer 내부 > #middle 내부 > #inner 내부 > '완벽'이라는 NGEarlyBreak 체인이 있으므로 중단하는 것이 좋습니다. 따라서 #outer의 시작 부분에서 레이아웃을 반환하고 다시 실행해야 합니다 (이번에는 찾은 NGEarlyBreak를 전달함). 그러면 #inner의 '줄 3' 앞에서 중단할 수 있습니다. '줄 3' 앞에서 중단합니다. 이렇게 하면 나머지 4줄이 다음 프래그먼트에 남게 되고 widows:4를 따르기 위함입니다.

알고리즘은 규칙을 모두 충족할 수 없는 경우 올바른 순서로 삭제하여 사양에 정의된 대로 가능한 한 최상의 중단점에서 항상 중단하도록 설계되었습니다. 조각화 흐름당 최대 한 번만 레이아웃을 변경하면 됩니다. 두 번째 레이아웃 패스에 진입할 때쯤에는 최적의 중단 위치가 이미 레이아웃 알고리즘에 전달되었습니다. 이것이 첫 번째 레이아웃 패스에서 발견되었고 해당 라운드에서 레이아웃 출력의 일부로 제공되는 중단 위치입니다. 두 번째 레이아웃 패스에서는 공간이 부족할 때까지 레이아웃을 배치하지 않습니다. 실제로 공간이 부족할 것으로 예상되지는 않습니다(실제로 오류일 수 있음). 이는 불필요하게 깨지는 규칙을 위반하지 않도록 조기 중단을 삽입할 수 있는 매우 달콤한 위치가 제공되었기 때문입니다. 그래서 그 지점까지 배치하고 중단합니다.

단편화 오버플로를 피하는 데 도움이 된다면 중단 회피 요청의 일부를 위반해야 할 때도 있습니다. 예를 들면 다음과 같습니다.

여기서는 #second 바로 앞에 공간이 부족하지만 'break-before:avoid'가 있습니다. 이는 마지막 예와 같이 '광고 휴식 시간 회피'로 해석됩니다. 또한 'violating orphans and widows' (#first 내부 > 'line 2' 앞)가 포함된 NGEarlyBreak도 있습니다. 여전히 완벽하지는 않지만 '위반 중단 방지'보다는 낫습니다. 따라서 '두 번째 줄' 앞에 중단하여 고아 / 자녀 요청을 위반하겠습니다. 이 내용은 사양에서 4.4. 비강제 중단: 프래그먼트 오버플로를 방지하기 위한 중단점이 충분하지 않은 경우 먼저 무시할 규칙을 정의합니다.

결론

LayoutNG 블록 조각화 프로젝트의 기능적 목표는 버그 수정을 제외하고 기존 엔진이 지원하는 모든 것의 LayoutNG 아키텍처 지원 구현을 제공하는 것이었습니다. 주요 예외는 더 나은 중단 방지 지원 (예: break-before:avoid)입니다. 이는 조각화 엔진의 핵심 부분이므로 나중에 다시 작성해야 하므로 처음부터 이를 포함해야 했습니다.

이제 LayoutNG 블록 조각화가 완료되었으므로 인쇄 시 혼합 페이지 크기 지원, 인쇄 시 여백 상자 @page 지원, box-decoration-break:clone 등 새로운 기능을 추가하는 작업을 시작할 수 있습니다. 또한 일반적으로 LayoutNG와 마찬가지로, 새로운 시스템의 버그 발생률과 유지 관리 부담은 시간이 지날수록 크게 감소할 것으로 예상됩니다.

감사의 말씀