블록 단편화는 CSS 블록 수준 상자(예: 섹션 또는 단락)가 하나의 프래그먼트 컨테이너(프래그먼테이너라고 함) 내에 전체적으로 들어맞지 않을 때 이를 여러 프래그먼트로 분할하는 것입니다. 프래그먼테이너는 요소가 아니지만 다중 열 레이아웃의 열 또는 페이징된 미디어의 페이지를 나타냅니다.
단편화가 발생하려면 콘텐츠가 단편화 컨텍스트 내에 있어야 합니다. 단편화 컨텍스트는 가장 일반적으로 다중 열 컨테이너 (콘텐츠가 열로 분할됨) 또는 인쇄 (콘텐츠가 페이지로 분할됨) 시 설정됩니다. 줄이 많은 긴 단락은 첫 번째 줄이 첫 번째 프래그먼트에 배치되고 나머지 줄이 후속 프래그먼트에 배치되도록 여러 프래그먼트로 분할해야 할 수 있습니다.

블록 단편화는 잘 알려진 다른 유형의 단편화인 줄 단편화('줄 바꿈'이라고도 함)와 유사합니다. 두 개 이상의 단어로 구성되고 줄바꿈을 허용하는 인라인 요소 (모든 텍스트 노드, 모든 <a>
요소 등)는 여러 프래그먼트로 분할될 수 있습니다. 각 프래그먼트는 서로 다른 선 상자에 배치됩니다. 줄 상자는 열 및 페이지의 프래그먼테이너와 동일한 인라인 프래그먼테이션입니다.
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 자손 간에 직접적인 상위-하위 관계가 있습니다. 예를 들어 프래그먼트 트리에서 절대 위치 지정된 요소에 의해 생성된 프래그먼트는 흐름 외부 위치 지정된 하위 요소와 이를 포함하는 블록 간에 조상 체인에 다른 노드가 있더라도 이를 포함하는 블록 프래그먼트의 직접적인 하위 요소입니다.
단편화 내에 흐름 외부로 배치된 요소가 있는 경우 더 복잡해질 수 있습니다. 이 경우 흐름 외부 프래그먼트가 CSS에서 포함 블록으로 간주하는 요소의 하위 요소가 아닌 프래그먼테이너의 직계 하위 요소가 되기 때문입니다. 이는 기존 엔진과 공존하기 위해 해결해야 하는 문제였습니다. LayoutNG는 모든 최신 레이아웃 모드를 유연하게 지원하도록 설계되었으므로 향후 이 코드를 간소화할 수 있습니다.
기존 단편화 엔진의 문제
웹의 초기 시대에 설계된 기존 엔진에는 (인쇄를 지원하기 위해) 기술적으로 당시에도 단편화가 존재했음에도 불구하고 단편화라는 개념이 없습니다. 단편화 지원은 위에 추가되거나 (인쇄) 후속으로 추가된 (다중 열) 기능이었습니다.
기존 엔진은 단편화 가능한 콘텐츠를 배치할 때 너비가 열 또는 페이지의 인라인 크기이고 높이가 콘텐츠를 포함하는 데 필요한 높이인 긴 스트립에 모든 항목을 배치합니다. 이 긴 스트립은 페이지에 렌더링되지 않습니다. 가상 페이지에 렌더링된 후 최종 표시를 위해 재정렬된다고 생각하면 됩니다. 이는 종이 신문 기사 전체를 한 열에 인쇄한 다음 가위를 사용하여 여러 개로 자르는 것과 개념적으로 유사합니다. (예전에는 일부 신문에서 실제로 이와 유사한 기법을 사용했습니다.)
기존 엔진은 스트립의 가상 페이지 또는 열 경계를 추적합니다. 이렇게 하면 경계를 벗어나는 콘텐츠가 다음 페이지 또는 열로 이동됩니다. 예를 들어 엔진이 현재 페이지라고 생각하는 페이지에 줄의 상반만 들어맞는 경우 '페이지 표시 스트럿'을 삽입하여 엔진이 다음 페이지 상단이라고 가정하는 위치로 아래로 밀어 넣습니다. 그런 다음 대부분의 실제 단편화 작업('가위로 자르고 배치')은 레이아웃 후 전처리 및 페인팅 중에 긴 콘텐츠 스트립을 페이지 또는 열로 자르고 (부분을 클립하고 변환하여) 이루어집니다. 이로 인해 변환 및 상대적 위치 지정을 단편화 후 적용하는 것과 같은 몇 가지 작업이 사실상 불가능해졌습니다 (사양에서 요구하는 사항). 또한 기존 엔진에서는 테이블 단편화를 어느 정도 지원하지만 플렉스 또는 그리드 단편화는 전혀 지원되지 않습니다.
다음은 가위, 배치, 접착제를 사용하기 전에 기존 엔진에서 3열 레이아웃이 내부적으로 어떻게 표현되는지를 보여주는 그림입니다. 4줄만 들어맞도록 높이가 지정되어 있지만 하단에 여유 공간이 있습니다.

기존 레이아웃 엔진은 레이아웃 중에 실제로 콘텐츠를 분할하지 않으므로 상대적 위치 지정 및 변환이 잘못 적용되고 박스 그림자가 열 가장자리에서 잘리는 등 이상한 아티팩트가 많이 발생합니다.
text-shadow를 사용한 예는 다음과 같습니다.
기존 엔진은 이를 제대로 처리하지 못합니다.

첫 번째 열의 줄에서 text-shadow가 잘린 후 두 번째 열 상단에 배치되는 것을 볼 수 있나요? 기존 레이아웃 엔진은 단편화를 이해하지 못하기 때문입니다.
다음과 같이 표시됩니다.
다음으로 변환과 box-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
의 2줄을 두 번째 열에 푸시해야 합니다. Chromium은 이러한 기능 조합을 완전히 지원하는 최초의 브라우저 엔진입니다.
NG 단편화 작동 방식
NG 레이아웃 엔진은 일반적으로 CSS 상자 트리를 깊이 우선으로 탐색하여 문서를 배치합니다. 노드의 모든 하위 요소가 배치되면 NGPhysicalFragment를 생성하고 상위 레이아웃 알고리즘으로 돌아가서 해당 노드의 레이아웃을 완료할 수 있습니다. 이 알고리즘은 프래그먼트를 하위 프래그먼트 목록에 추가하고 모든 하위 요소가 완료되면 모든 하위 프래그먼트가 포함된 프래그먼트를 생성합니다. 이 메서드를 통해 전체 문서의 프래그먼트 트리를 만듭니다. 그러나 이는 지나치게 단순화된 설명입니다. 예를 들어 흐름 외부로 배치된 요소는 배치되기 전에 DOM 트리에서 있는 위치에서 포함 블록으로 버블업되어야 합니다. 편의상 여기서는 이 고급 세부정보를 무시합니다.
LayoutNG는 CSS 상자 자체와 함께 레이아웃 알고리즘에 제약 조건 공간을 제공합니다. 이렇게 하면 알고리즘에 레이아웃에 사용할 수 있는 공간, 새 형식 지정 컨텍스트가 설정되었는지 여부, 이전 콘텐츠의 중간 여백 접힘 결과와 같은 정보가 제공됩니다. 제약 조건 공간은 또한 프래그먼테이너의 레이아웃된 블록 크기와 현재 블록 오프셋을 알고 있습니다. 줄바꿈 위치를 나타냅니다.
블록 단편화가 발생하면 하위 요소의 레이아웃이 시점에서 중지되어야 합니다. 줄바꿈의 이유는 페이지 또는 열의 공간이 부족하거나 강제 줄바꿈이 발생했기 때문일 수 있습니다. 그런 다음 방문한 노드의 프래그먼트를 생성하고 단편화 컨텍스트 루트 (멀티컬럼 컨테이너 또는 인쇄의 경우 문서 루트)까지 모두 반환합니다. 그런 다음 단편화 컨텍스트 루트에서 새 단편화기를 준비하고 트리로 다시 내려가 중단하기 전에 중단한 지점부터 다시 시작합니다.
시점 후 레이아웃을 재개하는 수단을 제공하는 데 중요한 데이터 구조를 NGBlockBreakToken이라고 합니다. 여기에는 다음 프래그먼테이너에서 레이아웃을 올바르게 재개하는 데 필요한 모든 정보가 포함되어 있습니다. NGBlockBreakToken은 노드와 연결되며, NGBlockBreakToken 트리를 형성하여 재개해야 하는 각 노드가 표현됩니다. NGBlockBreakToken은 내부에서 중단되는 노드에 대해 생성된 NGPhysicalBoxFragment에 연결됩니다. 중단 토큰은 상위 요소로 전파되어 중단 토큰의 트리를 형성합니다. 노드 내부가 아닌 노드 앞에서 중단해야 하는 경우 프래그먼트가 생성되지 않지만, 다음 프래그먼테이너의 노드 트리에서 동일한 위치에 도달할 때 레이아웃을 시작할 수 있도록 상위 노드는 여전히 노드의 'break-before' 중단 토큰을 만들어야 합니다.
중단은 프래그먼테이너 공간이 부족할 때 (강제되지 않은 중단) 또는 강제 중단이 요청될 때 삽입됩니다.
사양에는 최적의 비강제 시점에 관한 규칙이 있으며 공간이 부족한 위치에 시점을 삽입하는 것이 항상 올바른 것은 아닙니다. 예를 들어 시점 위치 선택에 영향을 미치는 다양한 CSS 속성(예: break-before
)이 있습니다.
레이아웃 중에 강제되지 않은 시점 사양 섹션을 올바르게 구현하려면 적절한 시점을 추적해야 합니다. 이 레코드는 중단 회피 요청을 위반하는 지점(예: break-before:avoid
또는 orphans:7
)에서 공간이 부족해지면 뒤로 돌아가서 찾은 최적의 마지막 중단점을 사용할 수 있음을 의미합니다. 각 가능한 중단점에는 '최후의 수단으로만 사용'에서 '중단하기에 완벽한 위치'에 이르기까지의 점수(중간에 몇 가지 값 포함)가 부여됩니다. 시점 위치의 점수가 'perfect'인 경우 시점을 변경해도 시점 규칙이 위반되지 않는다는 의미입니다. 공간이 부족한 지점에서 정확히 이 점수를 얻는 경우 더 나은 점수를 찾기 위해 뒤로 돌아갈 필요가 없습니다. 점수가 'last-resort'인 경우 중단점은 유효하지도 않지만 더 나은 것을 찾을 수 없는 경우 fragmentainer 오버플로를 방지하기 위해 중단될 수 있습니다.
유효한 브레이크포인트는 일반적으로 상위 요소와 첫 번째 하위 요소 간에 발생하는 것이 아니라, 동료 요소 (줄 상자 또는 블록) 사이에만 발생합니다 (C 클래스 브레이크포인트는 예외이지만 여기서는 다루지 않습니다). break-before:avoid가 있는 블록 형제 앞에 유효한 중단점이 있지만 '완벽'과 '최후의 수단' 사이 어딘가에 있습니다.
레이아웃 중에 NGEarlyBreak라는 구조에서 지금까지 찾은 최적의 브레이크포인트를 계속 추적합니다. 조기 중단은 블록 노드 앞이나 내부 또는 줄 (블록 컨테이너 줄 또는 플렉스 줄) 앞에 있을 수 있는 중단점입니다. 공간이 부족할 때 이전에 지나간 것의 깊은 곳에 최적의 중단점이 있는 경우 NGEarlyBreak 객체의 체인 또는 경로를 형성할 수 있습니다. 예를 들면 다음과 같습니다.
이 경우 #second
바로 앞에서 공간이 부족하지만 'break-before:avoid'가 있으므로 'break avoid 위반'의 시점 점수가 됩니다. 이 시점에서 '#outer
내부 > #middle
내부 > #inner
내부 > '3번 줄' 앞'의 NGEarlyBreak 체인이 'perfect'와 함께 있으므로 여기서 중단하는 것이 좋습니다. 따라서 #inner의 '3번 줄' 전에 중단할 수 있도록 #outer의 시작 부분부터 레이아웃을 반환하고 다시 실행해야 합니다 (이번에는 찾은 NGEarlyBreak를 전달). (widows:4
를 준수하기 위해 나머지 4줄이 다음 프래그먼테이너에 끝나도록 '3번 줄' 앞에 중단됩니다.)
이 알고리즘은 모든 규칙을 충족할 수 없는 경우 올바른 순서로 규칙을 삭제하여 항상 사양에 정의된 최적의 중단점에서 중단되도록 설계되었습니다. 단편화 흐름당 최대 한 번만 레이아웃을 다시 설정하면 됩니다. 두 번째 레이아웃 패스가 시작될 때 최적의 시점 위치가 이미 레이아웃 알고리즘에 전달됩니다. 이 위치는 첫 번째 레이아웃 패스에서 발견되었으며 해당 라운드의 레이아웃 출력의 일부로 제공된 시점 위치입니다. 두 번째 레이아웃 패스에서는 공간이 부족해질 때까지 레이아웃을 수행하지 않습니다. 사실 공간이 부족해질 것으로 예상되지는 않습니다 (실제로는 오류가 발생함). 불필요하게 중단 규칙을 위반하지 않도록 초기 중단을 삽입할 수 있는 매우 좋은 위치 (사용 가능한 한 가장 좋은 위치)가 제공되었기 때문입니다. 그 지점까지 배치하고 중단합니다.
참고로, 프래그먼테이너 오버플로를 방지하는 데 도움이 되는 경우 일부 중단 방지 요청을 위반해야 할 수도 있습니다. 예를 들면 다음과 같습니다.
여기에서는 #second
바로 앞에 공간이 부족하지만 'break-before:avoid'가 있습니다. 이는 마지막 예와 마찬가지로 'break avoid 위반'으로 번역됩니다. 또한 '고아 및 과부 행 위반' (#first
내부 > '2번 행' 앞)이 있는 NGEarlyBreak도 있습니다. 이 역시 완벽하지는 않지만 'break avoid 위반'보다는 낫습니다. 따라서 '2번 줄' 앞에서 줄바꿈하여 고아 / 과부 요청을 위반합니다. 사양은 4.4. Unforced Breaks: 프래그먼테이너 오버플로를 방지하기 위한 브레이크포인트가 충분하지 않은 경우 먼저 무시되는 중단 규칙을 정의합니다.
결론
LayoutNG 블록 단편화 프로젝트의 기능적 목표는 기존 엔진에서 지원하는 모든 것을 LayoutNG 아키텍처를 지원하는 구현으로 제공하고 버그 수정 외에는 최대한 적은 것을 제공하는 것이었습니다. 주요 예외는 더 나은 중단 방지 지원 (예: break-before:avoid
)입니다. 이는 단편화 엔진의 핵심 부분이므로 나중에 추가하면 다시 작성해야 하므로 처음부터 있어야 했습니다.
이제 LayoutNG 블록 단편화가 완료되었으므로 인쇄 시 혼합된 페이지 크기 지원, 인쇄 시 @page
여백 상자, box-decoration-break:clone
등 새로운 기능을 추가하는 작업을 시작할 수 있습니다. 또한 일반적으로 LayoutNG와 마찬가지로 새 시스템의 버그 발생률과 유지보수 부담은 시간이 지남에 따라 상당히 줄어들 것으로 예상됩니다.
감사의 말씀
- 멋진 '수제 스크린샷'을 제공해 주신 Una Kravets님, 감사합니다.
- 크리스 해럴슨님, 교정, 의견, 제안
- Philip Jägenstedt에게 의견과 제안을 보내주세요.
- 레이첼 앤드류님, 수정 및 첫 번째 다중 열 예시 그림에 대해 감사드립니다.