본문 바로가기

Develop/Frontend 가이드

[FE] 브라우저 렌더링 Browser rendering - 4단계 페인팅 'paint'

반응형

브라우저 렌더링
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 렌더링 파이프라인

4단계 페인팅 'paint'

Paint
Paint

브라우저 렌더링 엔진 Blink 에 콘텐츠가 입력되고 HTML 텍스트 파일을 해석해 DOM 트리를 생성하고, CSS 텍스트 파일을 해석해서 스타일링을 하고, 화면이 어떻게 구성될지 레이아웃을 계산하고 나면, 남은 단계는 페인팅 (paint) 단계입니다. 페인팅 단계는 지금까지 만들어 온 데이터로 화면을 그리는 작업을 진행하게 됩니다.

위 그림 왼쪽 상단을 보면 지금까지 중점적으로 살펴보았던 Document 가 아니라 LocalFrameView 가 등장하고 있습니다. 페인팅 이전 단계들은 문서를 해석하고 데이터를 만드는 작업이 주된 작업이라 Document 즉 문서를 중심으로 진행이 되었지만, 페인팅 단계부터는 화면을 어떻게 그리고 언제 업데이트할지가 중요하게 다루어지는 단계라, LocalFrameView 즉 프레임을 중심으로 작업이 진행됩니다. 일단 페인팅 단계 작업의 시작은 LocalFrameViewPaintTree 메서드에서 출발합니다. 정확하게 말하자면 페인팅 라이플 사이클의 시작 메서드인 RunPaintLifecyclePhase 메서드가 PaintTree 메서드를 호출하면서 페인팅 단계가 시작됩니다.

실제로 화면을 그리기 전에 화면을 어떻게 그릴지를 먼저 계획합니다. 먼저 이전 레이아웃 단계에서 생성된 LayoutObject 를 순회하며 Paint 메서드를 호출해 DisplayItem 을 만듭니다. DisplayItemLayoutObject 가 화면에 표현되기 위한 페인팅 작업들을 보관하고 있는 객체입니다. 그리고 LayoutObjectPaint 메서드로 만들어진 DisplayItemPaintControllerPaintArtifact 에 저장합니다.

// third_party/blink/renderer/core/frame/local_frame_view.cc
bool LocalFrameView::PaintTree(PaintBenchmarkMode benchmark_mode) {

  // ...
    // 'PaintController' 은 페인팅 명령어의 집합인 'PaintArtifact' 을 관리하는 역할을 맡고 있습니다.
    EnsurePaintController();

    // ...
    // 'LayoutObject' 의 'Paint()' 메서드를 호출해서 'DisplayItem' 을 생성하는 코드가 위치하고 있는데,
    // 코드가 너무 복잡해서 어떻게 'LayoutObject' 의 'Paint()' 메서드까지 연결되는지 파악하지는 못했습니다.
    // ...

      // 지금까지 생성한 'DisplayItem' 들을 'PaintController' 의 'PaintArtifact' 에 커밋합니다.
      paint_controller_->CommitNewDisplayItems();
}

LayoutObjectPaint 메서드는 상속받는 클래스에서 오버라이딩하여 구체적으로 어떻게 DisplayItem 을 만들지 구현하도록 설계되어 있습니다.

// third_party/blink/renderer/core/layout/layout_object.cc
void LayoutObject::Paint(const PaintInfo&) const {
  NOT_DESTROYED();
}

LayoutObject 를 상속받는 대표적인 클래스인 LayoutBoxPaint 메서드는 다시 BoxPainter 에게 작업을 위임합니다.

// third_party/blink/renderer/core/layout/layout_box.cc
void LayoutBox::Paint(const PaintInfo& paint_info) const {
  NOT_DESTROYED();
  BoxPainter(*this).Paint(paint_info);
}
// third_party/blink/renderer/core/paint/box_painter.cc
void BoxPainter::Paint(const PaintInfo& paint_info) {
  // Default implementation. Just pass paint through to the children.
  // ...
  PaintChildren(paint_state.GetPaintInfo());
}

void BoxPainter::PaintChildren(const PaintInfo& paint_info) {
  if (paint_info.DescendantPaintingBlocked())
    return;
  // 자식 'LayoutObject' 를 순회하며 페인팅 작업을 위한 'DisplayItem' 생성합니다.
  PaintInfo child_info(paint_info);
  for (LayoutObject* child = layout_box_.SlowFirstChild(); child;
       child = child->NextSibling()) {
    // ...
      child->Paint(child_info);
    // ...
  }
}

지금까지 화면에 어떤 것도 그리지 않았고, 어떻게 그릴지에 대해 정리한 PaintArtifact 을 만들었을 뿐이고 '래스터라이징' 작업을 통해 실제 화면에 보여줄 이미지를 만듭니다.

래스터라이징 raster

래스터라이징 raster
래스터라이징 raster

래스터라이징 (raster) 작업은 페인팅 단계에서 만든 PaintArtifact 을 실제 이미지로 만드는 단계입니다. DisplayItem 에 보관되어 있는 페인팅 작업을 하나씩 실행하면서 실제 화면에 그릴 수 있는 비트맵 이미지를 만듭니다. 이렇게 데이터에서 비트맵 이미지를 만드는 작업을 '래스터라이징' 이라고 하고 구수하게 표현하면 '이미지를 굽는다' 라고 표현하기도 합니다. 래스터라이징 작업은 그래픽 연산이기 때문에 그래픽처리장치(GPU) 의 도움을 받아 빠르게 이미지를 만들도록 되어 있습니다.

이미지를 해석하는 것도 '래스터라이징' 이라고 할 수 있는데 그렇게 표현하는 경우는 자주 없습니다.

위 그림의 오른쪽 상단을 보면 ResourcePool::InUsePoolResource 라는 것에 비트맵 이미지가 연결되어 있는걸 확인하실 수 있습니다. Bitmap 이 아니라 InUsePoolResource 인 이유는 중앙처리장치 (CPU) 와 그래픽처리장치 (GPU) 가 서로 데이터 전송으로 인한 지연(Latency) 을 최소화하기 위해 특정 메모리 영역을 공유하고 있는 구조로 되어 있고, 이를 표현한 InUsePoolResource 객체로 이미지 작업을 진행합니다. 이런 특이한 리소스들은 ResourcePool 에서 따로 관리하고 있습니다.

실제 이미지를 만드는 작업을 GPU 에게 지시하려면 그래픽 라이브러리를 활용해야 하는데 'OpenGL' 또는 'Vulkan' 같은 저수준 라이브러리를 호출하면서 이미지를 만들려면 너무 힘듭니다. 그래서 렌더링 엔진 Blink 는 Skia 그래픽 라이브러리를 사용하여 간접적으로 저수준 그래픽 라이브러리를 호출해서 이미지를 만듭니다.

Skia 라이브러리는 크로스 플랫폼이 사랑하는 라이브러리로 Chrome / Android / Xamarin / Flutter 등에서 사용되고 있습니다.

Skia 라이브러리
Skia 라이브러리

이렇게 힘겹게 만들어진 이미지는 브라우저 화면에서 볼 수 있게 되고 렌더링 파이프라인의 페인팅 단계가 완료됩니다.

파싱 단계부터 시작해서 페인팅 단계까지 진행하며 이미지를 만드는데, 모든 단계를 처음부터 진행하게 되면 성능이 매우 느립니다. 브라우저 화면이 위에서 부터 천천히 만들어지면서 내려오는걸 볼 수 있을지도 모릅니다. 다행히 브라우저로 인터넷을 하다가 답답해서 복창이 터지지 않도록, 많은 개발자가 노력해주셨고 어떤 방법으로 성능을 개선했는지는 다음 글에서 소개하도록 하겠습니다.

반응형