블록 단편화는 CSS 블록 수준 상자(예: 섹션 또는 단락)가 하나의 프래그먼트 컨테이너(프래그먼테이너라고 함) 내에 전체적으로 들어맞지 않을 때 이를 여러 프래그먼트로 분할하는 것입니다. 프래그먼트는 요소가 아니지만, 다중 열 레이아웃의 열이나 페이징된 미디어의 페이지를 나타냅니다.
단편화가 발생하려면 콘텐츠가 단편화 컨텍스트 내에 있어야 합니다. 단편화 컨텍스트는 일반적으로 다중 열 컨테이너 (콘텐츠가 열로 분할됨) 또는 인쇄 (콘텐츠가 페이지로 분할됨)에 의해 설정됩니다. 줄이 많은 긴 단락은 첫 번째 줄이 첫 번째 프래그먼트에 배치되고 나머지 줄이 후속 프래그먼트에 배치되도록 여러 프래그먼트로 분할해야 할 수 있습니다.
블록 단편화는 잘 알려진 또 다른 유형의 단편화인 '줄 바꿈'이라고도 하는 줄 파편화와 유사합니다. 두 개 이상의 단어로 구성되고 줄바꿈을 허용하는 인라인 요소 (모든 텍스트 노드, 모든 <a>
요소 등)는 여러 프래그먼트로 분할될 수 있습니다. 각 프래그먼트는 서로 다른 선 상자에 배치됩니다. 줄 상자는 열 및 페이지의 fragmentainer와 동일한 인라인 단편화입니다.
LayoutNG 블록 조각화
LayoutNGBlockFragmentation은 LayoutNG의 단편화 엔진을 다시 작성한 것으로, 처음에는 Chrome 102로 제공되었습니다. 데이터 구조 측면에서, 여러 NG 이전 데이터 구조를 프래그먼트 트리에 직접 표시되는 NG 프래그먼트로 대체했습니다.
예를 들어 이제 'break-before' 및 'break-after' CSS 속성의 'avoid' 값이 지원되므로 작성자가 헤더 바로 뒤의 줄바꿈을 피할 수 있습니다. 페이지의 마지막 항목이 머리글이고 섹션의 콘텐츠가 다음 페이지에서 시작되는 경우 어색해 보일 수 있습니다. 헤더 앞에서 중단하는 것이 좋습니다.
Chrome은 또한 파편화 오버플로를 지원하므로, 모놀리식 (깨질 수 없는 것으로 추정됨) 콘텐츠가 여러 열로 분할되지 않고 그림자 및 변환과 같은 페인트 효과가 올바르게 적용됩니다.
이제 LayoutNG의 블록 단편화가 완료되었습니다.
핵심 단편화 (줄 레이아웃, 플로팅, 흐름 외부 배치를 포함한 블록 컨테이너)가 Chrome 102에서 출시되었습니다. Flex 및 그리드 단편화는 Chrome 103에서 제공되고 테이블 단편화는 Chrome 106에서 제공됩니다. 마지막으로 인쇄는 Chrome 108에서 제공됩니다. 블록 단편화는 레이아웃을 실행하기 위해 기존 엔진에 의존하는 마지막 기능이었습니다.
Chrome 108부터는 기존 엔진이 더 이상 레이아웃을 실행하는 데 사용되지 않습니다.
또한 LayoutNG 데이터 구조는 페인팅 및 히트 테스트를 지원하지만 offsetLeft
및 offsetTop
와 같이 레이아웃 정보를 읽는 JavaScript API의 경우 일부 기존 데이터 구조를 사용합니다.
NG로 모든 것을 배치하면 CSS 컨테이너 쿼리, 앵커 배치, MathML, 맞춤 레이아웃(Houdini)과 같이 LayoutNG 구현만 있고 기존 엔진에 상응하는 구현이 없는 새로운 기능을 구현하고 제공할 수 있습니다. 컨테이너 쿼리의 경우 조금 전에 제공하면서 인쇄가 아직 지원되지 않는다고 개발자에게 경고했습니다.
2019년에 LayoutNG의 첫 번째 부분을 출시했습니다. 이 부분은 일반 블록 컨테이너 레이아웃, 인라인 레이아웃, 플로팅, 흐름 외부 배치로 구성되었지만 플렉스, 그리드, 표는 지원하지 않았으며 블록 단편화는 전혀 지원하지 않았습니다. 플렉스, 그리드, 테이블 및 블록 단편화와 관련된 모든 것에 기존 레이아웃 엔진을 사용하는 것으로 대체됩니다. 이는 단편화된 콘텐츠 내의 블록, 인라인, 플로팅 및 흐름 외부 요소의 경우에도 사실이었습니다. 보시다시피 이처럼 복잡한 레이아웃 엔진을 그 자리에서 업그레이드하는 것은 매우 정교한 작업입니다.
또한 2019년 중반까지 LayoutNG 블록 조각화 레이아웃의 핵심 기능 대부분이 이미 플래그 뒤에 구현되었습니다. 그런데 배송이 오래 걸리는 이유는 무엇일까요? 간단히 말하면 단편화는 시스템의 여러 기존 부분과 올바르게 공존해야 하며 모든 종속 항목이 업그레이드될 때까지 삭제하거나 업그레이드할 수 없습니다.
기존 엔진 상호작용
기존 데이터 구조는 여전히 레이아웃 정보를 읽는 JavaScript API를 담당하므로 기존 엔진이 이해할 수 있는 방식으로 데이터를 기존 엔진에 다시 써야 합니다. 여기에는 LayoutMultiColumnFlowThread와 같은 기존의 다중 열 데이터 구조를 올바르게 업데이트하는 작업이 포함됩니다.
기존 엔진 대체 감지 및 처리
LayoutNG 블록 단편화로 아직 처리할 수 없는 콘텐츠가 내부에 있으면 기존 레이아웃 엔진으로 대체해야 했습니다. 핵심 LayoutNG 블록 단편화를 출시할 때는 플렉스, 그리드, 표, 인쇄되는 모든 항목이 포함되었습니다. 이는 레이아웃 트리에서 객체를 만들기 전에 기존 대체의 필요성을 감지해야 했기 때문에 특히 까다로웠습니다. 예를 들어 다중 열 컨테이너 조상이 있는지, 어떤 DOM 노드가 형식 지정 컨텍스트가 될지 알기 전에 감지해야 했습니다. 완벽한 해결책이 없는 닭과 달걀 문제이지만 유일한 오작동이 거짓양성 (실제로는 필요하지 않은 경우 기존으로 대체)인 한 괜찮습니다. 이러한 레이아웃 동작의 버그는 새로운 버그가 아니라 Chromium에 이미 있는 버그이기 때문입니다.
사전 페인트 나무 산책
사전 페인트는 레이아웃 후 페인트 전에 하는 작업입니다. 주요 문제는 여전히 레이아웃 객체 트리를 탐색해야 하지만 이제 NG 프래그먼트가 있으므로 이를 어떻게 처리해야 하는지입니다. 레이아웃 객체와 NG 프래그먼트 트리를 동시에 탐색합니다. 두 트리 간의 매핑이 간단하지 않으므로 이는 매우 복잡합니다.
레이아웃 객체 트리 구조는 DOM 트리의 구조와 매우 유사하지만 프래그먼트 트리는 레이아웃의 입력이 아닌 출력입니다. 인라인 단편화 (줄 단편화) 및 블록 단편화 (열 또는 페이지 단편화)를 비롯한 모든 단편화의 효과를 실제로 반영하는 것 외에도 프래그먼트 트리에는 포함 블록과 해당 프래그먼트를 포함 블록으로 사용하는 DOM 자손 간에 직접적인 상위-하위 관계가 있습니다. 예를 들어 프래그먼트 트리에서 절대 위치 지정된 요소에 의해 생성된 프래그먼트는 흐름 외부 위치 지정된 하위 요소와 이를 포함하는 블록 간에 조상 체인에 다른 노드가 있더라도 이를 포함하는 블록 프래그먼트의 직접적인 하위 요소입니다.
단편화 내부에 외부 흐름이 배치된 요소가 있으면 훨씬 더 복잡할 수 있습니다. 외부 프래그먼트가 프래그먼트가 포함 블록으로 생각하는 하위 요소가 아니라 프래그먼트의 직접적인 하위 요소가 되기 때문입니다. 이는 기존 엔진과 병용하기 위해 해결해야 하는 문제였습니다. LayoutNG는 모든 최신 레이아웃 모드를 유연하게 지원하도록 설계되었으므로 향후 이 코드를 간소화할 수 있습니다.
기존 단편화 엔진의 문제
웹 초기에 설계된 기존 엔진에는 인쇄를 지원하기 위해 기술적으로는 단편화가 존재했더라도 실제로 단편화라는 개념이 없습니다. 단편화 지원은 맨 위에 추가되거나 (인쇄) 후속으로 추가된 (다중 열) 기능이었습니다.
단편화 가능한 콘텐츠를 배치할 때 기존 엔진은 너비가 열 또는 페이지의 인라인 크기이고 높이는 콘텐츠를 포함하는 데 필요한 높이만큼 긴 스트립에 모든 것을 배치합니다. 이 긴 스트립은 페이지에 렌더링되지 않습니다. 가상 페이지에 렌더링된 후 최종 표시를 위해 재정렬된다고 생각하면 됩니다. 이는 신문 기사 전체를 하나의 열로 인쇄한 다음 가위를 사용하여 두 번째 단계로 여러 개로 자르는 것과 개념적으로 유사합니다. (예전에는 일부 신문이 실제로 이와 유사한 기술을 사용했습니다!)
기존 엔진은 스트립의 가상 페이지 또는 열 경계를 추적합니다. 이렇게 하면 경계를 넘어서 맞지 않는 콘텐츠를 다음 페이지나 열로 조금씩 이동할 수 있습니다. 예를 들어 엔진이 현재 페이지라고 생각하는 페이지에 줄의 상반만 들어맞는 경우 '페이지 표시 스트럿'을 삽입하여 엔진이 다음 페이지 상단이라고 가정하는 위치로 아래로 밀어 넣습니다. 그런 다음 대부분의 실제 단편화 작업('가위로 자르고 배치')은 레이아웃 후 전처리 및 페인팅 중에 긴 콘텐츠 스트립을 페이지 또는 열로 자르고 (부분을 클립하고 변환) 실행됩니다. 이로 인해 변환 및 상대적 위치 지정을 단편화 후 적용하는 것과 같은 몇 가지 작업이 기본적으로 불가능해졌습니다 (사양에서 요구하는 사항). 또한 기존 엔진에서는 테이블 단편화를 어느 정도 지원하지만 플렉스 또는 그리드 단편화는 전혀 지원되지 않습니다.
다음은 가위, 배치, 접착제를 사용하기 전에 기존 엔진에서 3열 레이아웃이 내부적으로 어떻게 표현되는지를 보여주는 그림입니다. 4줄만 들어맞도록 높이가 지정되어 있지만 하단에 여유 공간이 있습니다.
기존 레이아웃 엔진은 레이아웃 중에 실제로 콘텐츠를 분할하지 않으므로 상대적 위치 지정 및 변환이 잘못 적용되고 box-shadow가 열 가장자리에서 잘리는 등 이상한 아티팩트가 많이 발생합니다.
text-shadow를 사용한 예는 다음과 같습니다.
기존 엔진은 이를 제대로 처리하지 못합니다.
첫 번째 열의 줄에서 text-shadow가 잘린 후 두 번째 열 상단에 배치되는 것을 볼 수 있나요? 기존 레이아웃 엔진은 단편화를 이해하지 못하기 때문입니다.
다음과 같이 표시됩니다.
다음으로, 변환과 상자 그림자를 사용하여 좀 더 복잡하게 만들어 보겠습니다. 기존 엔진에서는 잘못된 클리핑과 열 블러드가 발생합니다. 이는 사양에 따라 변환이 레이아웃 후, 단편화 후 효과로 적용되어야 하기 때문입니다. LayoutNG를 사용하면 둘 다 올바르게 작동합니다. 이렇게 하면 Firefox와의 상호 운용성이 향상됩니다. Firefox는 오랫동안 이 영역의 대부분의 테스트가 통과할 정도로 우수한 단편화 지원을 제공해 왔습니다.
기존 엔진은 크기가 큰 모놀리식 콘텐츠에도 문제가 있습니다. 여러 프래그먼트로 분할할 수 없는 콘텐츠는 모놀리식입니다. 오버플로 스크롤이 있는 요소는 직사각형이 아닌 영역에서 스크롤하는 것이 사용자에게 적합하지 않으므로 모놀리식입니다. 모놀리식 콘텐츠의 또 다른 예로 라인 상자와 이미지가 있습니다. 예를 들면 다음과 같습니다.
모놀리식 콘텐츠가 너무 커서 열 안에 들어맞지 않으면 기존 엔진은 콘텐츠를 무자비하게 자릅니다. 스크롤 가능한 컨테이너를 스크롤하려고 하면 매우 '흥미로운' 동작이 발생합니다.
LayoutNG 블록 단편화에서와 같이 첫 번째 열을 오버플로시키지 않습니다.
기존 엔진은 강제 중단을 지원합니다. 예를 들어 <div style="break-before:page;">
는 DIV 앞에 페이지 나누기를 삽입합니다. 그러나 최적의 강제되지 않은 시점을 찾는 기능은 제한적으로 지원됩니다. break-inside:avoid
및 외로운 단락과 과도한 단락 끝은 지원하지만, 예를 들어 break-before:avoid
를 통해 요청된 경우 블록 간의 시점을 피하는 기능은 지원되지 않습니다. 다음 예를 살펴보세요.
여기서 #multicol
요소는 각 열에 5줄을 넣을 수 있습니다 (높이가 100px이고 line-height가 20px이므로). 따라서 모든 #firstchild
가 첫 번째 열에 들어갈 수 있습니다. 그러나 동위 요소인 #secondchild
에는 break-before:avoid가 있습니다. 즉, 콘텐츠에서 콘텐츠 사이에 광고 시간이 발생하지 않도록 합니다. widows
값이 2이므로 #firstchild
의 두 줄을 두 번째 열에 푸시하여 모든 중단 방지 요청을 적용해야 합니다. Chromium은 이러한 기능들을 완벽하게 지원하는 최초의 브라우저 엔진입니다.
NG 단편화 작동 방식
NG 레이아웃 엔진은 일반적으로 CSS 상자 트리 깊이를 먼저 순회하여 문서를 배치합니다. 노드의 모든 하위 요소가 배치되면 NGPhysicalFragment를 생성하고 상위 레이아웃 알고리즘으로 반환하여 해당 노드의 레이아웃을 완료할 수 있습니다. 이 알고리즘은 프래그먼트를 하위 프래그먼트 목록에 추가하고 모든 하위 요소가 완료되면 모든 하위 프래그먼트가 포함된 프래그먼트를 생성합니다. 이 메서드를 통해 전체 문서의 프래그먼트 트리를 만듭니다. 그러나 이는 지나치게 단순화된 설명입니다. 예를 들어 흐름 외부로 배치된 요소는 레이아웃되기 전에 DOM 트리에서 있는 위치에서 포함 블록으로 버블업되어야 합니다. 편의상 여기에서는 다루지 않습니다.
LayoutNG는 CSS 상자 자체와 함께 레이아웃 알고리즘에 제약 조건 공간을 제공합니다. 이렇게 하면 알고리즘에 레이아웃에 사용할 수 있는 공간, 새 서식 컨텍스트가 설정되었는지 여부, 이전 콘텐츠의 중간 여백 접힘 결과와 같은 정보가 제공됩니다. 제약 조건 공간은 또한 프래그먼테이너의 레이아웃된 블록 크기와 현재 블록 오프셋을 알고 있습니다. 줄바꿈 위치를 나타냅니다.
블록 조각화가 관련된 경우 하위 요소의 레이아웃은 중단에서 중지되어야 합니다. 줄바꿈의 이유는 페이지 또는 열의 공간이 부족하거나 강제 줄바꿈이 발생했기 때문일 수 있습니다. 그런 다음 방문한 노드의 프래그먼트를 생성하고 단편화 컨텍스트 루트 (멀티컬럼 컨테이너 또는 인쇄의 경우 문서 루트)까지 모두 반환합니다. 그런 다음 단편화 컨텍스트 루트에서 새 단편화기를 준비하고 트리로 다시 내려가 중단하기 전에 중단한 지점부터 다시 시작합니다.
중단 후 레이아웃을 재개하는 수단을 제공하는 데 중요한 데이터 구조를 NGBlockBreakToken이라고 합니다. 여기에는 다음 프래그먼트에서 레이아웃을 올바르게 재개하는 데 필요한 모든 정보가 포함되어 있습니다. NGBlockBreakToken은 노드와 연결되며, NGBlockBreakToken 트리를 형성하여 재개해야 하는 각 노드가 표현됩니다. NGBlockBreakToken은 내부에서 중단되는 노드에 대해 생성된 NGPhysicalBoxFragment에 연결됩니다. 중단 토큰은 상위 요소로 전파되어 중단 토큰의 트리를 형성합니다. 노드 내부가 아닌 노드 앞에서 중단해야 하는 경우 프래그먼트가 생성되지 않지만, 다음 프래그먼테이너의 노드 트리에서 동일한 위치에 도달할 때 레이아웃을 시작할 수 있도록 상위 노드는 여전히 노드의 'break-before' 중단 토큰을 만들어야 합니다.
중단은 프래그먼테이너 공간이 부족할 때 (강제되지 않은 중단) 또는 강제 중단이 요청될 때 삽입됩니다.
사양에는 최적의 비강제 시점에 관한 규칙이 있으며 공간이 부족한 위치에 시점을 삽입하는 것이 항상 올바른 것은 아닙니다. 예를 들어 시점 위치 선택에 영향을 미치는 다양한 CSS 속성(예: break-before
)이 있습니다.
레이아웃 중에 강제 중단 사양 섹션을 올바르게 구현하려면 정상 가능성이 있는 중단점을 추적해야 합니다. 이 레코드는 중단 회피 요청을 위반하는 지점(예: break-before:avoid
또는 orphans:7
)에서 공간이 부족한 경우 뒤로 돌아가서 찾은 최적의 마지막 중단점을 사용할 수 있음을 의미합니다. 각 가능한 중단점에는 '최후의 수단으로만 사용'에서 '중단하기에 완벽한 위치'에 이르기까지 점수(중간에 몇 가지 값 포함)가 부여됩니다. 시점 위치의 점수가 'perfect'인 경우 시점을 변경해도 시점 규칙이 위반되지 않는다는 의미입니다. 공간이 부족한 지점에서 정확히 이 점수를 얻는 경우 더 나은 점수를 찾기 위해 뒤로 돌아갈 필요가 없습니다. 점수가 '마지막 옵션'인 경우 중단점이 유효한 상태도 아닙니다. 하지만 더 나은 결과를 찾지 못한다면 단편화 오버플로를 피하기 위해 중단점이 중단될 수 있습니다.
유효한 브레이크포인트는 일반적으로 상위 요소와 첫 번째 하위 요소 간에 발생하는 것이 아니라, 형제 요소 (줄 상자 또는 블록) 사이에만 발생합니다 (C 클래스 브레이크포인트는 예외이지만 여기서는 다루지 않습니다). break-before:avoid가 있는 블록 형제 앞에 유효한 중단점이 있지만 '완벽'과 '최후의 수단' 사이 어딘가에 있습니다.
레이아웃 중에는 NGEarlyBreak라는 구조에서 지금까지 찾은 최상의 중단점을 추적합니다. 조기 중단은 블록 노드 앞이나 내부 또는 라인 (블록 컨테이너 라인 또는 가변 라인) 앞에 가능한 중단점입니다. 공간이 부족할 때 이전에 지나간 것의 깊은 곳에 최적의 중단점이 있는 경우 NGEarlyBreak 객체의 체인 또는 경로를 형성할 수 있습니다. 예를 들면 다음과 같습니다.
이 경우에는 #second
바로 앞에 공간이 부족하지만 'break-before:avoid'가 있어 '정책 위반 중단 방지'의 중단 위치 점수를 받습니다. 이 시점에는 '줄 3' 앞에 '#outer
내부 > #middle
내부 > #inner
내부 > '완벽'이라는 NGEarlyBreak 체인이 있으므로 중단하는 것이 좋습니다. 따라서 #inner의 '3번 줄' 전에 중단할 수 있도록 #outer의 시작 부분부터 레이아웃을 반환하고 다시 실행해야 합니다 (이번에는 찾은 NGEarlyBreak를 전달). (widows:4
를 준수하기 위해 나머지 4줄이 다음 프래그먼테이너에 포함되도록 '3번 줄' 앞에 중단됩니다.)
이 알고리즘은 모든 규칙을 충족할 수 없는 경우 올바른 순서로 규칙을 삭제하여 항상 사양에 정의된 최적의 중단점에서 중단되도록 설계되었습니다. 단편화 흐름당 최대 한 번만 레이아웃을 다시 설정하면 됩니다. 두 번째 레이아웃 패스에 도달할 때면 최적의 시점 위치가 이미 레이아웃 알고리즘에 전달됩니다. 이 위치는 첫 번째 레이아웃 패스에서 발견되었으며 해당 라운드의 레이아웃 출력의 일부로 제공된 시점 위치입니다. 두 번째 레이아웃 패스에서는 공간이 부족해질 때까지 레이아웃을 수행하지 않습니다. 사실 공간이 부족해질 것으로 예상되지는 않습니다(실제로는 오류가 발생함). 불필요하게 중단 규칙을 위반하지 않도록 조기에 중단을 삽입할 수 있는 매우 좋은 위치가 제공되었기 때문입니다. 그 지점까지 배치하고 중단합니다.
참고로, 프래그먼테이너 오버플로를 방지하는 데 도움이 되는 경우 일부 중단 방지 요청을 위반해야 할 수도 있습니다. 예를 들면 다음과 같습니다.
여기서는 #second
바로 앞에 공간이 부족하지만 'break-before:avoid'가 있습니다. 이는 마지막 예와 마찬가지로 'break avoid 위반'으로 번역됩니다. 또한 '고아 및 과부 행 위반' (#first
내부 > '2번 행' 앞)이 있는 NGEarlyBreak도 있습니다. 이 방법은 여전히 완벽하지는 않지만 '중단 회피 위반'보다는 낫습니다. 따라서 '2번 줄' 앞에서 줄바꿈하여 고아 / 과부 요청을 위반합니다. 사양은 4.4. Unforced Breaks: 프래그먼테이너 오버플로를 방지하기 위한 브레이크포인트가 충분하지 않은 경우 먼저 무시되는 시점 규칙을 정의합니다.
결론
LayoutNG 블록 단편화 프로젝트의 기능적 목표는 기존 엔진에서 지원하는 모든 것을 LayoutNG 아키텍처를 지원하는 구현으로 제공하고 버그 수정 외에는 최대한 적은 것을 제공하는 것이었습니다. 주요 예외는 더 나은 중단 방지 지원 (예: break-before:avoid
)입니다. 이는 단편화 엔진의 핵심 부분이므로 나중에 추가하면 다시 작성해야 하므로 처음부터 있어야 했습니다.
이제 LayoutNG 블록 단편화가 완료되었으므로 인쇄 시 혼합된 페이지 크기 지원, 인쇄 시 @page
여백 상자, box-decoration-break:clone
등 새로운 기능을 추가하는 작업을 시작할 수 있습니다. 또한 일반적으로 LayoutNG와 마찬가지로 새 시스템의 버그 발생률과 유지보수 부담은 시간이 지남에 따라 상당히 줄어들 것으로 예상됩니다.
감사의 말씀
- 멋진 '수제 스크린샷'을 제공해 주신 Una Kravets님, 감사합니다.
- 크리스 해럴슨님, 교정, 의견, 제안
- 필립 제겐스테트에게 의견을 보내 주세요.
- Rachel Andrew가 편집을 담당하고 첫 번째 다중 열 예 그림입니다.