브라우저의 렌더링 과정 😬 ⭐️

문규찬·2021년 11월 12일
2
post-thumbnail

⭐️ 주소창에 주소를 입력하면?

각 컴퓨터는 IP주소를 가지고 있습니다.
서버에 요청을 보내려면 해당 서버의 IP주소를 알아야 하는데 IP주소는 192.168.0.1 과 같은 숫자들로 구성되어 있어서 외우기 쉽지 않죠 그래서 https://velog:io/@something 과 같은 사람이 기억하기 쉬운 도메인 이름을 사용합니다.

웹 브라우저는 주소창에 입력된 웹 사이트 주소(URL)를 이해하고 해당 주소와 연결된 IP 주소를 찾아야 합니다. 이 과정을 "DNS (Domain Name System)" 조회라고 합니다.

대략적인 순서는 이렇습니다

  1. DNS는 웹 사이트 주소 (예: www.example.com)를 컴퓨터가 이해할 수 있는 IP 주소 (예: 192.168.1.1)로 변환하는 역할을 합니다. 이런 변환 작업을 통해 컴퓨터는 웹 서버에 연결할 수 있게 됩니다.
  2. 브라우저는 호스트명을 로컬 DNS 캐시(최근에 방문한 웹 사이트의 주소와 IP 주소를 저장한 임시 저장소)에서 확인합니다.
  3. 만약 DNS 캐시에서 주소를 찾지 못하면, 브라우저는 컴퓨터의 운영 체제에 등록된 DNS 서버에 호스트명을 요청합니다.
  4. DNS 서버는 호스트명을 IP 주소로 변환하고 그 정보를 브라우저에게 제공합니다.

컴퓨터의 운영 체제는 사용자가 명시적으로 DNS 서버 설정을 변경하지 않는다면, 운영 체제는 기본적으로 인터넷 서비스 제공자(ISP)에서 제공하는 DNS 서버를 사용하게 됩니다

이러한 과정에서 TCP/IP라거나 Three-Way Handshake와 같은 개념들이 들어가는데 이 게시글에서는 우선 스킵하겠습니다.


⭐️ 브라우저의 기본 구조

브라우저의 주요 기능은 사용자가 선택한 자원을 서버에 요청하고 브라우저에 표시하는 것이다.

  • 사용자 인터페이스-주소 표시줄, 이전/다음 버튼, 북마크 메뉴 등. 요청한 페이지를 보여주는 창을 제외한 나머지 모든 부분이다.
  • 브라우저 엔진 - 사용자 인터페이스와 렌더링 엔진 사이의 동작을 제어.
  • 렌더링 엔진 - 요청한 콘텐츠를 표시. 예를 들어 HTML을 요청하면 HTML과 CSS를 파싱하여 화면에 표시함.
  • 통신 - HTTP 요청과 같은 네트워크 호출에 사용됨. 이것은 플랫폼 독립적인 인터페이스이고 각 플랫폼 하부에서 실행됨.
  • UI 백엔드 - 콤보 박스와 창 같은 기본적인 장치를 그림. 플랫폼에서 명시하지 않은 일반적인 인터페이스로서, OS 사용자 인터페이스 체계를 사용.
  • 자바스크립트 해석기 - 자바스크립트 코드를 해석하고 실행.
  • 자료 저장소 - 이 부분은 자료를 저장하는 계층이다. 쿠키를 저장하는 것과 같이 모든 종류의 자원을 하드 디스크에 저장할 필요가 있다. HTML5 명세에는 브라우저가 지원하는 '웹 데이터 베이스'가 정의되어 있다.

⭐️ 렌더링 엔진

조금 자세히 살펴 보아야 할 부분은 렌더링 엔진의 역할입니다.

렌더링 엔진은 응답으로 받은 내용을 브라우저 화면에 표시해주는 역할을 합니다.

(브라우저마다 사용하는 렌더링 엔진이 각각 다르기 때문에 크로스 브라우징 이슈가 발생하곤 한다.)

1) 전체적인 흐름

  1. 서버에서 응답으로 받은 HTML 데이터를 파싱한다.
  2. HTML을 파싱한 결과로 DOM Tree를 만든다.
  3. 파싱하는 중 CSS 파일 링크를 만나면 CSS 파일을 요청해서 받아온다.
  4. CSS 파일을 읽어서 CSSOM(CSS Object Model)을 만든다.
  5. DOM Tree와 CSSOM이 모두 만들어지면 이 둘을 사용해 Render Tree를 만든다.
  6. Render Tree에 있는 각각의 노드들이 화면의 어디에 어떻게 위치할 지를 계산하는 Layout과정을 거쳐서,
  7. 화면에 실제 픽셀을 Paint한다.

2) DOM

위의 내용에서 1,2번 부분입니다.

웹 서버는 특정 IP 주소 또는 도메인에 대한 요청이 들어오면 해당 요청에 맞는 HTML 문서 또는 웹 리소스를 응답하기 위해 동작합니다. 요청한 HTML 문서를 찾고, 이를 응답으로 클라이언트(브라우저)에게 전송하기 위한 과정은 다음과 같습니다

  1. 서버는 브라우저로부터 요청받은 HTML 파일을

    1. 읽고
    2. 메모리에 저장
    3. 그 메모리에 저장된 바이트(101101000100…)을 응답합니다.
  2. 응답으로는 위와 같은 형태의 HTML문서를 받아오게 되고, 이걸 하나하나 파싱parsing 합니다.

  3. 브라우저는 응답받은 바이트 형태의 문서를 meta태그의 charset 어트리뷰트에 지정된 인코딩방식(UTF-8)에 따라 문자열로 반환한다.

  4. 문자열로 변환된 HTML문서를 이번에는 문법적 의미를 갖는 코드의 최소 단위인 토큰(token)으로 분해한다.

  5. 토큰들의 내용에 따라 객체로 변환하여 각 노드들을 생성한다. (문서 노드, 요소 노드, 어트리뷰트 노드, 텍스트 노드)

  6. HTML은 요소 간의 부자 관계인 중첩 관계를 갖는데, 이를 반영하여 모든 노드들을 트리 구조로 구성하여 DOM을 만든다.

정리 : 서버로 부터 바이트 형태의 응답을, meta태그의 charset으로 지정된 인코딩방식으로 문자열로 변환하고, 변환된 문자열은 문법적 의미를 갖는 토큰으로 분해한다 토큰들에 의해 head와 같은 노드를 생성하고 트리 형식으로 DOM을 생성

3) CSSOM

HTML을 파싱하다가 link태그 혹은 style태그를 만나면, CSS파일을 요청해서 받아오게 됩니다.
받아온 CSS파일은 HTML을 파싱한 것과 유사한 과정을 거쳐서 역시 Tree형태의 CSSOM으로 만들어집니다. CSS 파싱은 CSS 특성상 자식 노드들이 부모 노드의 특성을 계속해서 이어받는 캐스캐이딩(cascading) 규칙이 추가된다는 것을 빼고는 HTML파싱과 동일하게 이루어집니다.

캐스케이딩 스타일 시트 원칙

  • Specificity (우선순위)
  • Inheritance (상속)
  • Importance (중요도)
  • Origin (출처)

4) 렌더 트리 (Render Tree)

DOM과 CSSOM를 합쳐서 Render Tree를 만듭니다. Render Tree는 DOM Tree에 있는 것들 중에서 화면에 실제로 '보이는' 친구들만으로 이루어집니다. 만약 CSS에서 display: none 으로 설정하였다면, 그 노드(와 그 자식 노드 전부)는 Render Tree에 추가되지 않는 것이죠.
마찬가지로 화면에 보이지 않는 head 태그 안의 내용들도 Render Tree에는 추가되지 않습니다.

span태그가 diplay:'none' 으로 그려지지 않았습니다, 다시 한번 렌더링 과정을 정리하면서 게시글을 써야겠다고 생각했던 것이 개인 프로젝트에서

const StyledInput = styled.input<{ show: Boolean }>(({ show, theme }) => {
  return {
    height: "30px",
    transition: "width 0.8s, opacity 0.8s",
    width: show ? "200px" : "0",
    visibility: "visible",
    opacity: show ? 1 : 0,
    marginRight: "10px",
    padding: "0 10px",
    fontSize: theme.fontSizeXs,
    border: `1.5px solid ${theme.grey2}`,
    borderRadius: "5px",
    ["&:focus, &:active"]: {
      outline: "none",
    },
  };
});

transition 을 적용해보고자 초기에 display:'none"으로 지정해버리는 실수가 있었습니다

4-1 Render Object Tree

Render Tree에는 사실 여러 가지가 포함되어 있습니다. Render Object Tree, Render Layer Tree 등등을 합쳐서 화면을 그리는 데에 필요한 모-든 정보를 가지고 있는 Render Tree가 완성됩니다.

Render Object Tree가 위에서 말했듯이 DOM Tree의 노드 중에서 화면에 보이는 것들만으로 이루어지는 트리입니다. block, inline, image, text, table같은 요소들이 Render Object가 됩니다. DOM Tree에서 div는 Render Object Tree에 Block element로, span은 Inline element로 옮겨지는 것이죠

Render Object의 속성에 따라 필요한 경우 Render Layer가 만들어집니다. 그리고 이 Render Layer중에서 GPU에서 처리되는 부분이 있으면 다시 Graphic Layer로 분리됩니다.
대표적으로는 다음과 같은 속성들이 쓰였을 때 Graphic Layer가 만들어지게 됩니다.

  • CSS 3D Transform(translate3d, preserve-3d 등)이나 perspective 속성이 적용된 경우
  • video 또는 canvas 요소
  • CSS3 애니메이션함수나 CSS 필터 함수를 사용하는 경우
  • 자식 요소가 레이어로 구성된 경우
  • z-index 값이 낮은 형제 요소가 레이어로 구성된 경우

만약 이런거 저런거 하나도 없이 div하나에 width정도 속성만 있다고 하면 레이어는 기본으로 만들어지는 하나만 사용하게 됩니다.

5) 레이아웃 (Layout)

화면에 보이는 노드들만을 가지고 있는 Render Tree가 다 만들어지면, 이제 Render Tree에 있는 각각의 노드들이 화면의 어디에 위치할 지를 계산하는 Layout과정을 거칩니다. CSSOM에서 가져온 스타일 정보들로 이미 얘가 어떻게 생겨야 한다는 것은 모두 알고 있지만, 현재 보이는 뷰포트를 기준으로 실제로 놓으려면 얘가 어디에 가야하는 지는 계산을 또 해야하는 거죠. 여기에서 CSS box model이 쓰이며, position(relative, absolute, fixed..), width, height 등등 틀과 위치에 관련된 부분들이 계산됩니다.

width: 50% 로 되어있는데 브라우저를 리사이즈한다고 하면, 보이는 요소들은 변함이 없으니 Render Tree는 그대로인 상태에서, layout단계를 다시 거쳐 위치를 계산해서 그리게 됩니다.

이렇게 화면에 보이는 요소 각각이 어디에 어떻게 위치할 지를 정해주는 과정을 Webkit에서는 layout으로, Gecko에서는 reflow로 부르고 있습니다.

6) 페인트 (Paint)

그리고 드디어 Render Tree의 각 노드들을 실제로 화면에 그리게 됩니다
visibility, outline, background-color같이 정말로 눈에 보이는 픽셀들이 여기에서 그려집니다.

만약 Render Layer가 2개 이상이라면 각각의 Layer를 paint한 뒤 하나의 이미지로 Composite하는 과정을 추가로 거친 뒤에 실제로 화면에 그려지게 됩니다.

https://youtu.be/ZTnIxIA5KGw

7) 다시 위치와 , 그리기

Reflow

위에서 말한 것처럼 렌더 트리와 각 요소들의 크기와 위치를 다시 계산해주는 과정을 Reflow라고 합니다.

Reflow가 일어나는 대표적인 경우는 다음과 같다.

  • DOM 노드의 추가, 제거
  • DOM 노드의 위치 변경
  • DOM 노드의 크기 변경(margin, padding, border, width, height 등..)
  • CSS3 애니메이션과 트랜지션
  • 폰트 변경, 텍스트 내용 변경
  • 이미지 크기 변경
  • offset, scrollTop, scrollLeft과 같은 계산된 스타일 정보 요청
  • 페이지 초기 렌더링
  • 윈도우 리사이징

이외에도 화면의 구조가 변경되었다면 Reflow가 발생한다고 이해하면 된다.

Repaint

화면의 구조가 변경되었을 때에는 다시 Reflow 과정을 거쳐 화면 구조를 다시 계산한 후 화면을 Repaint 합니다.
즉 화면의 구조가 변경되었을 때에는 Reflow와 Repaint 모두 발생합니다.

하지만 화면의 구조가 변경되지 않는 화면 변화의 경우 Repaint만 발생합니다.
예를 들면 opacity, background-color, visibility, outline 등의 스타일 변경 시에는 Repaint만 동작합니다.

⭐️ 자바스크립트 파싱과 실행

HTML 문서를 파싱한 결과물로서 생선된 DOM은 HTML 문서의 구조와 정보뿐만 아니라 HTML 요소와 스타일 등을 변경할 수 있는 프로그래밍 인터페이스로서 DOM API를 제공합니다.

즉! 자바스크립트 코드에서 DOM API를 사용하면 이미 생성된 DOM을 동적으로 조작할 수 있습니다.

렌더링 엔진은 HTML을 한 줄씩 순차적으로 파싱하여 DOM을 생성해 나가다가 자바스크립트 파일을 로드하는 script 태그를 만나면 DOM생성을 일시 중단합니다.

그리고 자바스크립트 파일을 서버에 요청하여 자바스크립트 코드를 파싱하기 위해 자바스크립트 엔진에 제어권을 넘깁니다. 이후 자바스크립트 파싱과 실행이 종료되면 렌더링 엔진으로 다시 제어권을 넘겨 HTML 파싱이 중단된 지점부터 다시 HTML 파싱을 시작하여 DOM 생성을 재개합니다.

script 태그에

  • async 속성이 명시된 경우 : 브라우저가 페이지를 파싱되는 동안에도 스크립트가 실행됨.
  • async 속성은 명시되어 있지 않고 defer 속성만 명시된 경우 : 브라우저가 페이지의 파싱을 모두 끝내면 스크립트가 실행됨.
  • async 속성과 defer 속성이 모두 명시되어 있지 않은 경우 : 브라우저가 페이지를 파싱하기 전에 스크립트를 가져와 바로 실행시킴.

자바스크립트 파싱과 실행은 브라우저의 렌더링 엔진이 아닌 자바스크립트 엔진이 처리합니다!

✅ 출처:
1)이웅모(님) '모던 자바스크립트 Deep Dive' 를 기록합니다
2)https://velog.io/@thyoondev
3)https://m.post.naver.com/viewer/postView.nhn?volumeNo=8431285&memberNo=34176766
4)https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction?hl=ko

0개의 댓글