본문 바로가기

Develop/Frontend 가이드

[FE] 브라우저 렌더링 Browser rendering - 3단계 레이아웃 'layout'

반응형

브라우저 렌더링
https://docs.google.com/presentation/d/1boPxbgNrTU0ddsc144rcXayGA_WF53k96imRH8Mp34Y

브라우저 렌더링 Browser rendering

브라우저는 'content (콘텐츠)' 를 'rendering (렌더링)' 해서 'pixel image(픽셀 이미지)' 즉 화면을 만듭니다.

  1. 브라우저 렌더링 Browser rendering - 0단계 소개
  2. 브라우저 렌더링 Browser rendering - 1단계 파싱 'parse'
  3. 브라우저 렌더링 Browser rendering - 2단계 스타일링 'style'
  4. 브라우저 렌더링 Browser rendering - 3단계 레이아웃 'layout'
  5. 브라우저 렌더링 Browser rendering - 4단계 페인팅 'paint'
  6. 브라우저 렌더링 Browser rendering - 5단계 변경 'change'

렌더링 파이프라인 Rendering pipeline

Blink 렌더링 파이프라인
Blink 렌더링 파이프라인

3단계 레이아웃 'layout'

Layout
Layout

브라우저 렌더링 엔진 Blink 에 콘텐츠가 입력되고 HTML 텍스트 파일을 해석해 DOM 트리를 생성하고, CSS 텍스트 파일을 해석해서 StyleSheetContents 를 만들고 나면 다음 단계인 레이아웃 작업이 시작됩니다. 레이아웃 단계에서 진행하는 작업은 위 그림처럼 각 Element 가 화면의 어떤 위치에 배치할지를 결정하는 작업을 진행하게 됩니다.

레이아웃 트리 초기화

Document 가 초기화할 때 레이아웃을 관리할 트리를 아래 코드 순서로 생성합니다.

// third_party/blink/renderer/core/dom/document.cc
void Document::Initialize() {
  // ...

  // 모든 레이아웃의 기준이 되는 Document 의 레이아웃을 초기화합니다.
  layout_view_ = new LayoutView(this);
  SetLayoutObject(layout_view_);
  // StyleResolver 로 전체 레이아웃의 기준이 될 ViewPort 크기를 계산합니다.
  layout_view_->SetStyle(GetStyleResolver().StyleForViewport());

  // 이제 Document 에 포함되는 모든 Element 의 레이아웃을 관리할 레이아웃 트리를 구성을 시작합니다.
  AttachContext context;
  AttachLayoutTree(context);

  // ...
}
// third_party/blink/renderer/core/dom/container_node.cc
void ContainerNode::AttachLayoutTree(AttachContext& context) {
  // ...

  // 자식 노드를 순회하면서 레이아웃 트리를 완성합니다.
  for (Node* child = firstChild(); child; child = child->nextSibling())
    child->AttachLayoutTree(context);

  // ...
}

Element 타입 별로 AttachLayoutTree 함수를 override 해서 Element 특성에 맞게 레이아웃 트리에 추가합니다.

// third_party/blink/renderer/core/html/html_html_element.cc
// HTML 의 가장 기본이 되는 HTMLHtmlElement 의 AttachLayoutTree 함수입니다.
void HTMLHtmlElement::AttachLayoutTree(AttachContext& context) {
  scoped_refptr<const ComputedStyle> original_style = GetComputedStyle();
  if (original_style)
    // 레이아웃과 관련한 스타일을 재계산합니다.
    SetComputedStyle(LayoutStyleForElement(original_style));

  Element::AttachLayoutTree(context);

  // ...
}
// third_party/blink/renderer/core/dom/element.cc
void Element::AttachLayoutTree(AttachContext& context) {
  // ...

    // LayoutTreeBuilderForElement 로 레이아웃의 특징 (block, inline 등)을 표현할 LayoutObject 를 생성합니다.
    LayoutTreeBuilderForElement builder(*this, context, style, legacy);
    builder.CreateLayoutObject();

  // ...
}
// third_party/blink/renderer/core/dom/layout_tree_builder.cc
void LayoutTreeBuilderForElement::CreateLayoutObject() {
  // ...

  LayoutObject* new_layout_object = node_->CreateLayoutObject(*style_, legacy_);

  // ...
}
// third_party/blink/renderer/core/layout/layout_object.cc
LayoutObject* LayoutObject::CreateObject(Element* element,
                                         const ComputedStyle& style,
                                         LegacyLayout legacy) {
  // ...

  // 스타일 Display 속성에 맞는 LayoutObject 를 만들어서 반환합니다.
  switch (style.Display()) {
    case EDisplay::kNone:
    case EDisplay::kContents:
      return nullptr;
    case EDisplay::kInline:
      return new LayoutInline(element);
    case EDisplay::kBlock:
    case EDisplay::kFlowRoot:
    case EDisplay::kInlineBlock:
    case EDisplay::kListItem:
      return LayoutObjectFactory::CreateBlockFlow(*element, style, legacy);
    // ...
    case EDisplay::kFlex:
    case EDisplay::kInlineFlex:
      UseCounter::Count(element->GetDocument(), WebFeature::kCSSFlexibleBox);
      return LayoutObjectFactory::CreateFlexibleBox(*element, style, legacy);
    case EDisplay::kGrid:
    case EDisplay::kInlineGrid:
      UseCounter::Count(element->GetDocument(), WebFeature::kCSSGridLayout);
      return LayoutObjectFactory::CreateGrid(*element, style, legacy);
   // ...
  }

  // ...
}

LayoutObjectCreateObject 함수 내 switch 문을 보면 다양한 레이아웃이 가능하다는걸 확인하실 수 있습니다. 대표적인 레이아웃인 'block' 과 'inline' 은 다음 그림과 같이 배치되는 특성을 가지는 LayoutObject 입니다.

block 레이아웃
block 레이아웃

inline 레이아웃
inline 레이아웃

요약하자면 Document 를 초기화할 때 DOM 트리를 순회하며 레이아웃 관련 스타일 속성을 StyleResolver 를 활용해서 얻은 뒤, 스타일의 'Display' 속성에 따라 일치하는 LayoutObject 을 생성하여 레이아웃 트리에 추가하는 과정을 반복해서 전체 레이아웃 트리를 완성합니다. 이렇게 완성된 레이아웃 트리는 화면 위에 Element 을 어떻게 배치할지를 결정하는 작업에 활용됩니다.

레이아웃 재구성

레이아웃 재구성 순서

레이아웃을 재구성해야 하는 이벤트가 발생하면 아래 코드 순서로 레이아웃을 재구성합니다.

// third_party/blink/renderer/core/css/style_engine.cc
void StyleEngine::UpdateStyleAndLayoutTree() {
   // ...
      RecalcStyle();
      // 스타일부터 재계산한 후에 레이아웃 트리를 재구성합니다.
      RebuildLayoutTree();
  // ...
}

void StyleEngine::RebuildLayoutTree() {
  // ...

  Element& root_element = layout_tree_rebuild_root_.RootElement();
  {
    WhitespaceAttacher whitespace_attacher;
    // rootElement 부터 시작해서 레이아웃을 재구성합니다.
    root_element.RebuildLayoutTree(whitespace_attacher);
  }
}
// third_party/blink/renderer/core/dom/element.cc
void Element::RebuildLayoutTree(WhitespaceAttacher& whitespace_attacher) {
  // ...
  if (NeedsReattachLayoutTree()) {
    // ...

    // 레이아웃 트리의 각 노드를 붙혔다 떼었다 하면서 레이아웃 노드 간의 관계를 다시 정리합니다.
    ReattachLayoutTree(reattach_context);

    // ...
}

레이아웃 계산

Layout 계산
Layout 계산

레이아웃을 재구성하면 레이아웃 트리가 이전과는 달라지게 되고 달라진 레이아웃 트리 구성에 맞도록 레이아웃 위치를 다시 계산해야 페인팅 단계 때 정확한 위치에 Element 를 그릴 수 있습니다.

렌더링 엔진 Blink 는 위 그림처럼 LayoutObjectLayoutNGMixin 적용하여 레이아웃을 계산합니다. 'NG' 는 'Next Generation' 의 약어로 이전 레이아웃 계산 구조와 달라졌음을 나타내고 있습니다.

LayoutNGMixin 이전에는 LayoutObject 별로 레이아웃을 계산하는 함수가 있었는데, LayoutNGMixin 이후부터 모든 레이아웃 연산 계산을 모듈로 모아서 관리하고 있습니다. 예시로 'block' 레이아웃을 기준으로 아래 코드와 함께 어떻게 동작하는지 설명드리도록 하겠습니다.

'block' 레이아웃은 CreateObject 함수에서 LayoutObjectFactoryCreateBlockFlow 함수를 통해 LayoutNGBlockFlow 로 만들어집니다.

// third_party/blink/renderer/core/layout/layout_object.cc
LayoutObject* LayoutObject::CreateObject(Element* element, const ComputedStyle& style, LegacyLayout legacy) {
  // ...

  switch (style.Display()) {
    // ...
    case EDisplay::kBlock:
    case EDisplay::kFlowRoot:
    case EDisplay::kInlineBlock:
    case EDisplay::kListItem:
      return LayoutObjectFactory::CreateBlockFlow(*element, style, legacy);
   // ...
  }

  // ...
}
// third_party/blink/renderer/core/layout/layout_object_factory.cc
LayoutBlockFlow* LayoutObjectFactory::CreateBlockFlow(
  // ...

  // Create a plain LayoutBlockFlow
  return CreateObject<LayoutBlockFlow, LayoutNGBlockFlow>(node, style, legacy);
}

template <typename BaseType, typename NGType, typename LegacyType = BaseType>
inline BaseType* CreateObject(Node& node,
                              const ComputedStyle& style,
                              LegacyLayout legacy,
                              bool disable_ng_for_type = false) {
  Element* element = GetElementForLayoutObject(node);
  bool force_legacy = false;

    // ...

    if (!force_legacy)
      // 'LayoutNGBlockFlow' 을 생성해 반환합니다.
      return new NGType(element);
}

LayoutNGBlockFlowLayoutNGBlockFlowMixin<LayoutBlockFlow> 를 상속받고, LayoutNGBlockFlowMixin<LayoutBlockFlow> 은 다시 LayoutNgMixin<Base> 를 상속받는 구조로 되어 있고 레이아웃 계산은 LayoutNGBlockFlowUpdateBlockLayout 함수를 호출하면서 시작됩니다. 상속 구조와 UpdateBlockLayout 함수의 호출 구조는 아래 제공된 코드로 살펴보실 수 있습니다.

// third_party/blink/renderer/core/layout/ng/layout_ng_block_flow.h
// This overrides the default layout block algorithm to use Layout NG.
class CORE_EXPORT LayoutNGBlockFlow
    : public LayoutNGBlockFlowMixin<LayoutBlockFlow> {
    // ...
      // 레이아웃을 재계산하는 함수입니다.
      void UpdateBlockLayout(bool relayout_children) override;
    // ...
}

// third_party/blink/renderer/core/layout/ng/layout_ng_block_flow.cc
void LayoutNGBlockFlow::UpdateBlockLayout(bool relayout_children) {
  UpdateNGBlockLayout();
}
// third_party/blink/renderer/core/layout/ng/layout_ng_block_flow_mixin.h
// This mixin holds code shared between LayoutNG subclasses of LayoutBlockFlow.
template <typename Base>
class LayoutNGBlockFlowMixin : public LayoutNGMixin<Base> {
  // ...

  // Intended to be called from UpdateLayout() for subclasses that want the same
  // behavior as LayoutNGBlockFlow.
  void UpdateNGBlockLayout();

}

// third_party/blink/renderer/core/layout/ng/layout_ng_block_flow_mixin.cc
template <typename Base>
void LayoutNGBlockFlowMixin<Base>::UpdateNGBlockLayout() {
  // ...
  LayoutNGMixin<Base>::UpdateInFlowBlockLayout();
  // ...
}

LayoutNGBlockFlowUpdateBlockLayoutLayoutNGMixin<Base>UpdateInFlowBlockLayout() 함수 호출에 도달하게 됩니다. LayoutNGMixin<Base>UpdateInFlowBlockLayout() 함수가 실행되면서 실제 레이아웃 계산을 NGLayoutAlgorithm 에 전달하여 계산하게 되며, 계산 결과는 NGLayoutResult 로 만들어져 반환됩니다. 반환된 NGLayoutResult 은 다음 단계인 페인팅 단계를 위해 활용됩니다.

// third_party/blink/renderer/core/layout/ng/layout_ng_mixin.h
// This mixin holds code shared between LayoutNG subclasses of
// LayoutBlock.
template <typename Base>
class LayoutNGMixin : public Base {
  // ...
  public:
    const NGPhysicalBoxFragment* CurrentFragment() const final;
  // ...
  protected:
    // 'NGLayoutResult' 만듭니다. 
    scoped_refptr<const NGLayoutResult> UpdateInFlowBlockLayout();
}

// third_party/blink/renderer/core/layout/ng/layout_ng_mixin.cc
template <typename Base>
scoped_refptr<const NGLayoutResult>
LayoutNGMixin<Base>::UpdateInFlowBlockLayout() {
  // 캐싱된 레이아웃이 있는지 검사합니다.
  scoped_refptr<const NGLayoutResult> previous_result =
      Base::GetCachedLayoutResult();
  // ...

  // 레이아웃 계산을 실행합니다.
  scoped_refptr<const NGLayoutResult> result =
      NGBlockNode(this).Layout(constraint_space);

  // 레이아웃 계산 결과를 반환합니다.
  return result;
}
// third_party/blink/renderer/core/layout/ng/ng_block_node.cc
scoped_refptr<const NGLayoutResult> NGBlockNode::Layout(
    const NGConstraintSpace& constraint_space,
    const NGBlockBreakToken* break_token,
    const NGEarlyBreak* early_break) const {

  // ...
    base::Optional<NGFragmentGeometry> fragment_geometry;
    // 레이아웃 계산 결과를 저장할 'NGLayoutResult' 을 준비합니다.
    scoped_refptr<const NGLayoutResult> layout_result =
      box_->CachedLayoutResult(constraint_space, break_token, early_break,
                               &fragment_geometry, &cache_status);
  // ...

  // NGFragmentGeometry 를 초기화합니다.
  if (!fragment_geometry) {
    fragment_geometry =
        CalculateInitialFragmentGeometry(constraint_space, *this);
  }

  // ... 

  PrepareForLayout();

  // 레이아웃 계산 알고리즘에 전달할 데이터를 준비합니다.
  NGLayoutAlgorithmParams params(*this, *fragment_geometry, constraint_space,
                                 break_token, early_break);

  // ...

  if (!layout_result)
    // 알고리즘으로 레이아웃을 계산합니다.
    layout_result = LayoutWithAlgorithm(params);

  FinishLayout(block_flow, constraint_space, break_token, layout_result);

  // 레이아웃 계산 결과를 반환합니다.
  return layout_result;
}

// 어떤 알고리즘으로 레이아웃을 계산할지를 다음 함수인 'DetermineAlgorithmAndRun' 로 넘깁니다.
inline scoped_refptr<const NGLayoutResult> LayoutWithAlgorithm(
    const NGLayoutAlgorithmParams& params) {
  scoped_refptr<const NGLayoutResult> result;
  DetermineAlgorithmAndRun(params,
                           [&result](NGLayoutAlgorithmOperations* algorithm) {
                             result = algorithm->Layout();
                           });
  return result;
}

// 어떤 NGLayoutAlgorithm 을 사용하여 레이아웃을 계산할 건지 결정하는 함수입니다.
template <typename Callback>
NOINLINE void DetermineAlgorithmAndRun(const NGLayoutAlgorithmParams& params,
                                       const Callback& callback) {
  // 레이아웃 계산 대상인 노드의 스타일과 LayoutObject 을 상속한 LayoutBox 타입에 따라,
  // 레이아웃 계산 알고리즘을 선택합니다.
  const ComputedStyle& style = params.node.Style();
  const LayoutBox& box = *params.node.GetLayoutBox();
  // 실질적인 레이아웃 연산을 수행합니다.
  if (box.IsLayoutNGFlexibleBox()) {
    CreateAlgorithmAndRun<NGFlexLayoutAlgorithm>(params, callback);
  // ...
  } else if (box.IsLayoutNGGrid() &&
             RuntimeEnabledFeatures::LayoutNGGridEnabled()) {
    CreateAlgorithmAndRun<NGGridLayoutAlgorithm>(params, callback);
  }
  // ...
}

레이아웃 계산 알고리즘 NGLayoutAlgorithm 의 결과인 NGLayoutResult 안에 NGPhysicalFragment 아래와 같은 트리 형태로 저장되어 있고 이 데이터를 활용해서 페인팅 단계에 어느 위치에 어떤 크기로 Element 를 그릴지가 결정됩니다.

NGLayoutAlgorithm 계산 결과인 NGLayoutResult 안에 NGPhysicalFragment 트리
NGLayoutAlgorithm 계산 결과인 NGLayoutResult 안에 NGPhysicalFragment 트리

결국 레이아웃 단계는 레이아웃 연산 알고리즘으로 NGLayoutResult, 즉 NGPhysicalFragment로 이루어진 트리를 준비하는 걸로 끝이 나게 되며, 다음 단계인 페인팅 단계로 넘어가게 됩니다.

참고

Life of a pixel (Chrome University 2019)
Life of a pixel
How Blink works

반응형