개요
웹 애플리케이션을 개발할 때 가장 중요한 것은 브라우저 환경을 제대로 이해하는 것이다. 특히 클라이언트 사이드 자바스크립트는 대부분 브라우저에서 HTML, CSS와 함께 동작하기 때문에, 브라우저의 렌더링 과정을 깊이 이해하면 더 효율적인 프로그래밍이 가능하다.
렌더링 과정을 이해하기 위해서는 먼저 두 가지 핵심 개념을 알아야 한다. 바로 '파싱'과 '렌더링'이다.
파싱(Parsing)의 상세 과정
파싱은 단순한 텍스트 분석 이상의 복잡한 과정이다. 프로그래밍 언어로 작성된 텍스트 문서를 읽어 실행 가능한 형태로 변환하는 과정을 말한다. 이는 크게 두 단계로 나눌 수 있다.
첫 번째는 어휘 분석(Lexical Analysis) 단계로, 문서의 문자열을 의미 있는 토큰으로 분해한다. 두 번째는 구문 분석(Syntactic Analysis) 단계로, 분해된 토큰들에 문법적 의미와 구조를 부여해 트리 구조의 자료구조인 파스 트리를 생성한다. 이렇게 생성된 파스 트리는 이후 중간 언어인 바이트코드로 변환되어 실제로 실행된다.
렌더링(Rendering)의 전체 흐름
렌더링은 파싱된 문서를 실제로 화면에 표시하는 과정이다. HTML, CSS, 자바스크립트로 작성된 문서를 파싱한 결과물을 바탕으로 브라우저 화면에 픽셀 단위로 시각화하는 모든 처리 과정을 포함한다. 이 과정은 여러 단계로 이루어져 있고, 각 단계는 서로 긴밀하게 연관되어 있다.
- 리소스 로딩 단계에서는 HTML, CSS, 자바스크립트뿐만 아니라 이미지, 폰트 파일 등 웹 페이지를 구성하는 모든 리소스를 서버에 요청하고 응답을 받는다.
- 파싱 단계에서는 브라우저의 렌더링 엔진이 HTML과 CSS를 파싱해 각각 DOM과 CSSOM을 생성한다. 이 두 구조는 이후 결합되어 렌더 트리를 형성한다.
- 자바스크립트 처리 단계에서는 브라우저의 자바스크립트 엔진이 자바스크립트 코드를 파싱해 AST(추상 구문 트리)를 생성하고, 이를 바이트코드로 변환해 실행한다. 실행 중에 DOM API를 통해 DOM이나 CSSOM을 변경할 수 있고, 변경된 내용은 다시 렌더 트리에 반영된다.
- 레이아웃과 페인팅 단계에서는 렌더 트리를 기반으로 각 요소의 정확한 위치와 크기를 계산하고, 이를 실제 화면의 픽셀로 변환하는 작업이 이루어진다.
브라우저의 렌더링 과정을 상세하게 살펴보면 위와 같다.
요청과 응답
브라우저의 핵심 기능은 필요한 리소스를 서버로부터 받아와 시각적으로 표현하는 것이다. 이 과정은 생각보다 복잡한 단계를 거친다.
사용자가 브라우저 주소창에 URL을 입력하고 엔터를 누르면, 브라우저는 먼저 URL의 호스트 이름을 DNS를 통해 IP 주소로 변환한다. 이후 이 IP 주소를 가진 서버에 접속해 필요한 리소스를 요청한다.
일반적으로 루트 경로(예: example.com)로 접속하면 서버는 기본 설정에 따라 index.html 파일을 응답한다. 이는 웹 서버의 기본 동작이지만, 다른 파일을 요청하고 싶다면 URL의 경로 부분에 해당 파일의 위치를 명시하면 된다.
브라우저는 HTML 문서를 파싱하는 도중 외부 리소스를 로드하는 태그(link, script, img 등)를 만나면 파싱을 일시 중단하고 해당 리소스를 서버에 추가로 요청한다. 이러한 과정은 모든 리소스가 로드될 때까지 계속된다.
HTTP 1.1 과 HTTP 2.0
HTTP는 웹의 기초를 이루는 프로토콜로, 1991년 최초로 문서화된 이후 지속적으로 발전해왔다. 특히 HTTP/1.1과 HTTP/2.0 사이에는 큰 성능 차이가 있는데, 이는 두 버전의 근본적인 동작 방식 차이에서 기인한다.
HTTP/1.1의 한계와 특징
HTTP/1.1은 1999년에 표준화되었다. 이 버전의 가장 큰 특징은 커넥션당 하나의 요청과 응답만을 처리할 수 있다는 점이다. 이는 웹 페이지가 복잡해질수록 심각한 성능 저하를 초래했다. 예를 들어, 하나의 웹 페이지에 여러 개의 이미지, CSS 파일, 자바스크립트 파일이 포함되어 있다면, 각각의 리소스를 순차적으로 요청하고 응답받아야 했다.
이러한 구조는 특히 현대의 복잡한 웹 애플리케이션에서 큰 문제가 된다. 페이지 로딩 시간이 리소스 개수에 비례해서 증가하기 때문에, 사용자 경험을 크게 저하시킬 수 있다.
HTTP/2.0의 혁신적 개선사항
2015년에 등장한 HTTP/2.0은 이전 버전의 한계를 획기적으로 개선했다. 가장 중요한 특징은 하나의 커넥션으로 여러 요청과 응답을 동시에 처리할 수 있다는 점이다. 이를 멀티플렉싱(Multiplexing)이라고 하는데, 이를 통해 리소스 로딩 시간을 크게 단축할 수 있었다.
HTTP/2.0에서는 여러 리소스를 동시에 전송할 수 있기 때문에, 같은 페이지를 로드할 때 HTTP/1.1에 비해 약 50% 정도 빠른 속도를 보인다. 이는 현대 웹의 복잡한 요구사항을 효과적으로 처리할 수 있게 해주었고, 결과적으로 더 나은 사용자 경험을 제공할 수 있게 되었다.
HTML 파싱과 DOM 생성
웹 브라우저가 HTML 문서를 파싱하는 과정은 매우 복잡하고 정교한 단계들로 이루어져 있다. 이는 단순한 텍스트 문서를 브라우저가 이해하고 처리할 수 있는 구조화된 객체 모델로 변환하는 과정이다. 브라우저에 표시되는 모든 콘텐츠의 기초가 되는 이 과정을 자세히 살펴보자.
먼저, 서버와 브라우저 간의 초기 통신 과정이 일어난다. 사용자가 웹 페이지를 요청하면, 서버는 해당 HTML 문서를 읽어들이고 이를 메모리에 저장한다. 이때 서버는 저장된 내용을 바이트 단위의 데이터로 변환하여 네트워크를 통해 브라우저로 전송한다. 이 과정에서 데이터는 이진 형식으로 전달되며, 이는 네트워크 전송을 최적화하기 위한 것이다.
브라우저가 이 바이트 데이터를 받으면, 이를 문자열로 변환하는 작업을 수행한다. 이 변환 과정에서 매우 중요한 역할을 하는 것이 문서의 인코딩 방식이다. HTML 문서의 meta 태그에 명시된 charset 속성(일반적으로 UTF-8)을 기준으로 변환이 이루어진다. 이 인코딩 정보는 HTTP 응답 헤더의 Content-Type 필드에도 포함되어 있어, 브라우저는 이를 참조하여 정확한 문자열 변환을 수행할 수 있다.
문자열로 변환된 HTML 문서는 이제 의미 있는 단위로 분석되어야 한다. 이 과정에서 브라우저는 문서를 토큰이라 불리는 최소 의미 단위로 분해한다. HTML 토큰은 시작 태그, 종료 태그, 속성 이름과 값, 텍스트 등을 포함한다. 예를 들어, <div class="container">Hello</div>라는 HTML 코드는 시작 태그 토큰(<div>), 속성 토큰(class="container"), 텍스트 토큰(Hello), 종료 태그 토큰(</div>)으로 분해된다.
이렇게 생성된 토큰들은 각각의 특성에 따라 다양한 종류의 노드로 변환된다. DOM 트리의 구성 요소가 될 이 노드들은 크게 네 가지 유형으로 나눌 수 있다.
- 문서 노드는 전체 HTML 문서를 대표하는 최상위 노드로, document 객체로 참조할 수 있다.
- 요소 노드는 HTML 태그에 해당하는 노드로, DOM 트리의 대부분을 차지한다.
- 어트리뷰트 노드는 HTML 요소의 속성을 나타내며, 해당 요소 노드와 연결되어 있다.
- 텍스트 노드는 HTML 요소 내의 텍스트 내용을 담고 있으며, DOM 트리의 말단 노드가 된다.
이러한 노드들은 HTML 문서의 계층 구조를 정확히 반영하여 트리 형태로 구성된다. DOM 트리에서는 HTML 요소들의 포함 관계가 부모-자식 관계로 표현되며, 이를 통해 문서의 구조를 프로그래밍적으로 탐색하고 조작할 수 있게 된다.
CSS 파싱과 CSSOM 생성
CSS의 파싱 과정은 HTML 파싱과 밀접하게 연관되어 있다. 브라우저가 HTML을 파싱하는 도중 CSS를 불러오는 link 태그나 style 태그를 만나면, 즉시 HTML 파싱을 일시 중단하고 CSS 처리를 시작한다. 이는 화면에 콘텐츠를 정확하게 스타일링하여 표시하기 위해 필수적인 과정이다.
CSS 파싱도 HTML 파싱과 유사한 단계를 거친다. 먼저 바이트 데이터를 문자열로 변환하고, 이를 다시 토큰으로 분해한다. CSS 토큰에는 선택자, 속성 이름, 속성 값 등이 포함된다. 예를 들어 p { color: blue; }라는 CSS 규칙은 선택자 토큰(p), 속성 이름 토큰(color), 속성 값 토큰(blue) 등으로 분해된다.
CSSOM의 가장 중요한 특징 중 하나는 CSS의 상속을 명확하게 반영한다는 점이다. 예를 들어 body 요소에 설정된 font-size나 color와 같은 속성은 별도로 지정하지 않는 한 모든 하위 요소에 자동으로 상속된다. CSSOM은 이러한 상속 관계를 트리 구조로 정확하게 표현하여, 각 요소의 최종적인 스타일을 효율적으로 계산할 수 있게 한다.
렌더 트리 생성
렌더 트리는 DOM과 CSSOM을 결합하여 만들어지는 또 다른 트리 구조다. 이는 실제로 화면에 그려질 요소들만을 포함하는 특별한 구조로, 시각적인 요소들의 레이아웃을 계산하고 화면에 표시하는 데 사용된다.
렌더 트리 생성 과정에서 중요한 점은 모든 DOM 노드가 렌더 트리에 포함되는 것은 아니라는 것이다. meta 태그나 script 태그와 같이 화면에 시각적으로 표현되지 않는 요소들은 렌더 트리에서 제외된다. 또한 CSS의 display: none이 적용된 요소도 렌더 트리에 포함되지 않는다. 반면 visibility: hidden이나 opacity: 0이 적용된 요소는 공간을 차지하므로 렌더 트리에 포함된다.
렌더 트리가 완성되면 브라우저는 각 노드의 정확한 위치와 크기를 계산한다. 이 과정을 레이아웃 또는 리플로우라고 부른다. 이때 각 요소의 박스 모델이 계산되며, 뷰포트 내에서의 정확한 위치가 결정된다. 예를 들어 width: 50%로 지정된 요소의 실제 픽셀 너비가 이 단계에서 계산된다.
레이아웃 계산이 완료되면 실제로 화면에 픽셀을 그리는 페인팅 작업이 수행된다. 이 과정에서 각 요소의 배경, 테두리, 텍스트, 이미지 등이 실제로 화면에 그려진다. 이는 렌더링 과정의 마지막 단계로, 사용자가 최종적으로 보게 되는 화면이 완성된다.
이러한 전체 과정은 DOM이나 CSSOM이 변경될 때마다 다시 실행될 수 있다. 예를 들어 JavaScript로 요소를 추가하거나 삭제할 때, 브라우저 창의 크기를 조절할 때, 또는 요소의 크기나 위치를 변경하는 스타일을 수정할 때 리렌더링이 발생한다. 이는 상당한 계산 비용이 드는 작업이므로, 성능 최적화를 위해서는 불필요한 리렌더링을 최소화하는 것이 중요하다.
자바스크립트 파싱과 실행
브라우저에서 자바스크립트의 처리는 매우 복잡한 과정을 거친다. 먼저, HTML 문서를 파싱한 결과물인 DOM은 단순한 문서 구조체가 아니라 프로그래밍 인터페이스로서의 역할도 수행한다. DOM API를 통해 자바스크립트는 이미 생성된 DOM을 동적으로 수정할 수 있으며, 이는 웹 페이지의 동적인 기능 구현의 핵심이 된다.
렌더링 엔진이 HTML 문서를 파싱하는 도중 script 태그를 만나면 흥미로운 과정이 시작된다. 렌더링 엔진은 즉시 DOM 생성 과정을 중단하고 자바스크립트 엔진에게 제어권을 넘긴다. 이때 자바스크립트 엔진은 크게 세 가지 주요 단계를 거쳐 코드를 처리한다.
첫 번째는 토크나이징 단계다. 이 과정에서 자바스크립트 엔진은 소스 코드를 읽어 들여 의미 있는 최소 단위인 토큰으로 분해한다. 예를 들어 function add(x, y) { return x + y; } 라는 코드는 'function'이라는 키워드 토큰, 'add'라는 식별자 토큰, '('와 ')'라는 괄호 토큰, 'x'와 'y'라는 매개변수 토큰 등으로 분해된다. 이 과정은 단순한 문자열 분할이 아니라, 각 토큰의 문법적 의미를 파악하는 과정을 포함한다.
두 번째로 파싱 단계가 진행된다. 분해된 토큰들은 문법적 의미와 구조를 가진 트리 형태의 자료구조인 AST(Abstract Syntax Tree)로 변환된다. AST는 프로그램의 구조를 계층적으로 표현하는 트리로, 각 노드는 프로그램을 구성하는 각각의 요소를 나타낸다. 예를 들어, 함수 선언, 변수 할당, 연산자 사용 등이 모두 트리의 각기 다른 노드로 표현된다.
마지막으로 바이트코드 생성과 실행 단계가 이루어진다. AST는 인터프리터가 실행할 수 있는 중간 코드인 바이트코드로 변환된다. 특히 V8 엔진의 경우 성능 최적화를 위해 추가적인 처리를 수행한다. 자주 실행되는 코드는 터보팬이라는 컴파일러를 통해 최적화된 기계어로 컴파일되어 실행된다. 이후 해당 코드의 실행 빈도가 낮아지면 다시 디옵티마이징 과정을 거쳐 원래의 상태로 되돌리기도 한다.
리플로우와 리페인트
자바스크립트를 통해 DOM이나 CSSOM을 변경하면, 브라우저는 이러한 변경사항을 화면에 반영하기 위해 추가적인 작업을 수행한다. 이 과정은 크게 리플로우와 리페인트라는 두 가지 단계로 나뉜다.
리플로우는 변경된 엘리먼트의 위치와 크기를 다시 계산하는 과정이다. 예를 들어, 특정 div 요소의 width를 변경하면, 그 요소뿐만 아니라 해당 요소의 자식 요소들과 경우에 따라 부모 요소들의 레이아웃도 함께 재계산해야 할 수 있다. 이는 상당히 비용이 큰 작업으로, 특히 복잡한 레이아웃에서는 성능에 큰 영향을 미칠 수 있다.
- 요소의 크기나 위치가 변경될 때
- 윈도우 크기가 변경될 때
- 폰트가 변경될 때
- 콘텐츠가 변경될 때
- 노드가 추가되거나 삭제될 때
리플로우는 이러한 경우일 때 발생한다.
리페인트는 계산된 레이아웃을 실제 화면에 그리는 작업이다. 모든 리플로우는 리페인트를 동반하지만, 모든 리페인트가 리플로우를 필요로 하는 것은 아니다. 예를 들어, 배경색을 변경하는 경우에는 레이아웃 재계산 없이 색상만 다시 그리면 된다.
자바스크립트 파싱에 의한 HTML 파싱 중단
브라우저는 HTML, CSS, 자바스크립트를 동기적으로, 즉 순차적으로 처리한다. 이는 script 태그의 위치가 전체 페이지의 로딩 성능에 큰 영향을 미친다는 것을 의미한다. script 태그를 만나면 HTML 파싱이 중단되기 때문에, script 태그의 위치는 매우 중요한 의미를 갖는다.
<!-- 잘못된 예시 -->
<head>
<script>
// DOM이 생성되기 전이므로 에러 발생
document.getElementById('target').style.color = 'red';
</script>
</head>
<body>
<div id="target">Hello</div>
</body>
이러한 문제를 해결하기 위해 body 요소의 가장 마지막에 스크립트를 위치시키는 것이 권장된다. 이렇게 하면 두 가지 주요한 이점이 있다. 첫째, HTML 파싱이 완료된 후에 자바스크립트가 실행되므로 DOM 조작과 관련된 에러를 방지할 수 있다. 둘째, 자바스크립트 다운로드와 실행이 HTML 렌더링을 차단하지 않으므로 사용자가 더 빠르게 페이지 콘텐츠를 볼 수 있다.
리플로우와 리페인트가 발생하는 주요 상황들을 이해하고 이를 최적화하는 것도 중요하다. 예를 들어, 여러 스타일 변경을 한꺼번에 처리하거나, 복잡한 애니메이션의 경우 transform을 사용하는 등의 최적화 기법을 적용할 수 있다.
script 태그의 async/defer 어트리뷰트
웹 애플리케이션이 복잡해지면서 자바스크립트 파일의 크기와 수가 크게 증가했다. 이로 인해 기존의 동기적 스크립트 로딩 방식은 심각한 성능 문제를 야기했다. 브라우저가 script 태그를 만날 때마다 HTML 파싱을 중단하고 자바스크립트 파일을 로드하고 실행해야 했기 때문이다. 이는 사용자가 웹 페이지의 콘텐츠를 보는 데까지 걸리는 시간을 크게 증가시켰다.
이러한 문제를 해결하기 위해 HTML5는 script 태그에 async와 defer라는 두 가지 새로운 어트리뷰트를 도입했다. 이 두 어트리뷰트는 자바스크립트 파일의 로딩이 HTML 파싱을 차단하지 않도록 하여, 페이지 로딩 성능을 크게 개선할 수 있게 했다.
비동기 로딩 어트리뷰트의 사용 조건
async와 defer 어트리뷰트는 외부 자바스크립트 파일을 로드하는 경우에만 사용할 수 있다는 중요한 제한이 있다. 다시 말해, script 태그의 src 어트리뷰트를 통해 외부 파일을 참조하는 경우에만 이 기능을 사용할 수 있으며, script 태그 내부에 직접 자바스크립트 코드를 작성하는 인라인 방식에서는 이 어트리뷰트들이 아무런 효과를 발휘하지 않는다.
async 어트리뷰트
async 어트리뷰트를 사용하면 브라우저는 script 태그를 만났을 때 특별한 방식으로 동작한다. HTML 파싱을 멈추지 않고 계속 진행하면서, 동시에 자바스크립트 파일을 별도의 스레드에서 다운로드한다. 이후 자바스크립트 파일의 다운로드가 완료되면, 그 시점에 HTML 파싱을 잠시 중단하고 지금까지 다운로드된 자바스크립트를 실행한다.
async의 가장 큰 특징은 실행 순서가 보장되지 않는다는 점이다. 예를 들어 세 개의 자바스크립트 파일이 있다고 가정해보자.
<script async src="analytics.js"></script> <!-- 0.5MB -->
<script async src="main.js"></script> <!-- 2MB -->
<script async src="utility.js"></script> <!-- 1MB -->
이 경우 파일 크기와 네트워크 상태에 따라 실행 순서가 매번 달라질 수 있다.
analytics.js가 가장 작으므로 먼저 다운로드되어 실행될 가능성이 높지만, 네트워크 상태에 따라 다른 파일이 먼저 실행될 수도 있다. 따라서 async는 다른 스크립트에 의존성이 없는 독립적인 기능을 구현한 스크립트에만 사용해야 한다.
defer 어트리뷰트
defer 어트리뷰트도 async처럼 HTML 파싱과 자바스크립트 파일 다운로드를 병렬적으로 처리한다. 하지만 실행 시점에서 중요한 차이를 보인다. defer는 모든 HTML 파싱이 완료될 때까지 자바스크립트 실행을 지연시킨다.
defer의 동작 과정을 상세히 살펴보면 다음과 같다.
- 브라우저가 script 태그를 발견하면 파일 다운로드를 시작하되, HTML 파싱을 중단하지 않는다.
- 다운로드는 백그라운드에서 진행되며, 이 시간 동안 HTML 파싱이 계속된다.
- HTML 문서 전체의 파싱이 완료되어 DOM 트리가 완성된다.
- 이후 defer 속성이 지정된 스크립트들이 HTML에 작성된 순서대로 차례차례 실행된다.
- 모든 defer 스크립트의 실행이 완료되면 DOMContentLoaded 이벤트가 발생한다.
이러한 특성은 다음과 같은 상황에서 매우 유용하다.
<script defer src="framework.js"></script>
<script defer src="plugin.js"></script>
<script defer src="app.js"></script>
이 경우 파일 크기나 네트워크 상태와 관계없이 반드시 framework.js가 먼저 실행되고, 그 다음 plugin.js, 마지막으로 app.js가 실행된다. 따라서 프레임워크나 라이브러리에 의존하는 코드가 있을 때 특히 유용하다.
실제 적용에서의 전략적 선택
두 어트리뷰트의 특성을 고려할 때, 다음과 같은 전략으로 사용하는 것이 효과적이다.
- 독립적으로 동작하는 기능성 스크립트(예: 방문자 분석, 광고 추적, 소셜 미디어 위젯 등)에는 async를 사용한다.
- 코어 애플리케이션 로직이나 프레임워크/라이브러리 의존성이 있는 스크립트에는 defer를 사용한다.
- DOM을 직접 조작하는 스크립트에는 반드시 defer를 사용하거나, 기존 방식대로 body 태그 최하단에 위치시킨다.
이러한 전략적 선택을 통해 웹 페이지의 초기 로딩 속도를 최적화하면서도 안정적인 스크립트 실행을 보장할 수 있다.
요약
렌더링 과정의 전체 개요
렌더링의 기본 개념과 과정
- 브라우저의 렌더링은 HTML, CSS, JavaScript로 작성된 문서를 파싱하고 화면에 표시하는 과정이다.
- 파싱은 프로그래밍 언어의 문법에 맞게 작성된 텍스트 문서를 읽어들여 실행하기 위해 문서의 문자열을 토큰으로 분해하고, 문법적 의미와 구조를 반영하여 트리 구조의 자료구조를 생성하는 과정이다.
- 렌더링은 이렇게 파싱된 문서를 기반으로 브라우저에 시각적으로 출력하는 과정이다.
브라우저의 렌더링 수행 단계
- 브라우저는 필요한 리소스를 서버에 요청하고 응답을 받는다.
- HTML, CSS, JavaScript, 이미지, 폰트 등 모든 리소스를 포함
- 각 리소스는 개별적인 요청과 응답 과정을 거친다.
- 렌더링 엔진은 HTML과 CSS를 파싱하여 DOM과 CSSOM을 생성한다.
- HTML은 DOM(Document Object Model)으로 변환
- CSS는 CSSOM(CSS Object Model)으로 변환
- 두 모델은 독립적인 자료구조로 구성
- DOM과 CSSOM을 결합하여 렌더 트리를 생성한다.
- 렌더 트리는 화면에 표시되는 요소만을 포함
- meta 태그나 script 태그 등은 제외
- 생성된 렌더 트리를 기반으로 레이아웃을 계산하고 화면에 페인팅한다.
브라우저 렌더링의 세부 과정 분석
1. 요청과 응답의 상세 과정
- 브라우저의 주소창에 URL을 입력하면:
- URL의 호스트 이름이 DNS를 통해 IP 주소로 변환된다.
- 해당 IP 주소를 가진 서버에 요청을 전송한다.
- 일반적으로 루트 요청의 경우 index.html이 응답된다.
- HTML 문서를 파싱하는 중에:
- CSS 파일을 로드하는 link 태그를 만나면 CSS 파일을 요청
- 이미지 파일을 로드하는 img 태그를 만나면 이미지 파일을 요청
- JavaScript 파일을 로드하는 script 태그를 만나면 JavaScript 파일을 요청
2. HTTP 프로토콜의 진화
- HTTP/1.1의 특징과 한계:
- 커넥션당 하나의 요청과 응답만 처리 가능
- 리소스의 동시 전송이 불가능한 구조
- 요청할 리소스가 많을수록 응답 시간이 증가
- 현대 웹 애플리케이션에서는 성능 저하의 원인이 됨
- HTTP/2.0의 개선사항:
- 커넥션당 여러 요청과 응답을 동시에 처리 가능
- 멀티플렉싱을 통한 리소스 동시 전송
- HTTP/1.1 대비 약 50% 향상된 페이지 로드 속도
- 현대 웹의 복잡한 리소스 요청을 효율적으로 처리
3. HTML 파싱과 DOM 생성 과정
- 바이트 처리 단계:
- 서버가 응답한 HTML 문서는 바이트(2진수) 형태
- 지정된 인코딩 방식(예: UTF-8)에 따라 문자열로 변환
- 토큰화 단계:
- 문자열을 의미 있는 최소 단위인 토큰으로 분해
- HTML 태그, 속성, 텍스트 등을 토큰으로 인식
- 노드 생성 단계:
- 각 토큰을 해당하는 객체(노드)로 변환
- 문서 노드, 요소 노드, 어트리뷰트 노드, 텍스트 노드 생성
- DOM 트리 구축 단계:
- 노드들의 관계를 트리 구조로 구성
- HTML 요소의 중첩 관계가 부모-자식 관계로 표현
4. CSS 파싱과 CSSOM 생성 과정
- CSS 파싱 시작:
- HTML 파싱 중 CSS 관련 태그 발견 시 DOM 생성 일시 중단
- link 태그나 style 태그를 통한 CSS 처리 시작
- CSSOM 생성:
- HTML과 유사한 파싱 과정을 거침 (바이트 → 문자 → 토큰 → 노드)
- CSS의 상속 관계를 반영한 트리 구조 생성
- 부모 요소의 스타일이 자식 요소에 상속되는 구조 반영
5. 렌더 트리 생성과 활용
- 렌더 트리의 특성:
- DOM과 CSSOM을 결합하여 생성
- 시각적으로 보이는 요소만 포함
- display: none이 적용된 요소는 제외
- visibility: hidden이나 opacity: 0이 적용된 요소는 포함
- 렌더 트리의 활용:
- 요소들의 레이아웃(위치와 크기) 계산
- 실제 픽셀로의 변환과 페인팅 처리
- 뷰포트 내에서의 정확한 위치 결정
6. 자바스크립트 처리 과정
- 자바스크립트 엔진의 역할:
- HTML 파싱 중 script 태그 만나면 자바스크립트 엔진으로 제어권 이동
- 자바스크립트 코드를 파싱하여 AST(추상 구문 트리) 생성
- AST를 바이트코드로 변환하여 실행
- 실행 최적화:
- V8 엔진의 경우 자주 사용되는 코드는 터보팬 컴파일러로 최적화
- 실행 빈도에 따라 최적화 수준 조절
- 필요한 경우 디옵티마이징 수행
7. 리플로우와 리페인트
- 리플로우(레이아웃 재계산)가 필요한 경우:
- 요소의 크기나 위치가 변경될 때
- 브라우저 창 크기가 변경될 때
- 노드의 추가/삭제가 발생할 때
- 폰트 변경이 있을 때
- 리페인트 특성:
- 모든 리플로우는 리페인트를 동반
- 색상 변경 등은 리플로우 없이 리페인트만 발생
- 가능한 한 리플로우를 최소화하는 것이 성능에 좋음
브라우저 렌더링의 동작 예시
1. DOM 생성 과정의 구체적 단계
<html>
<body>
<div class="example">Hello</div>
</body>
</html>
- 바이트 → 문자 → 토큰 → 노드 → DOM의 각 단계별 상세 처리 (위의 내용)
- 이러한 HTML이 다음과 같이 처리된다:
- 바이트 스트림 해석: 3C 68 74 6D 6C 3E ... → <html>
- 문자 토큰화: { type: "tag-start", content: "html" }, { type: "tag-start", content: "body" } 등
- 노드 생성: HTMLHtmlElement, HTMLBodyElement, HTMLDivElement 등의 노드
- DOM 트리 구축: 노드 간의 부모-자식 관계 형성
2. CSSOM 생성과 스타일 계산의 상세 과정
body { font-size: 16px; }
.example {
color: blue;
padding: 20px;
}
- CSS 규칙의 처리와 상속 관계 형성 (위의 내용)
- 이런 CSS는 다음과 같이 처리된다.
- 선택자 매칭: 각 규칙을 해당 DOM 요소와 매칭
- 스타일 상속: body의 font-size는 하위 모든 요소에 상속
- 계산된 스타일: 각 요소의 최종 스타일 값 계산
- CSSOM 트리 구축: 스타일 정보를 포함한 트리 구조 형성
3. 렌더 트리 구축의 세부 단계
- DOM과 CSSOM의 결합 과정:
- 보이는 노드 선별:
- display: none → 렌더 트리에서 제외
- visibility: hidden → 렌더 트리에 포함 (공간 차지)
- 메타데이터 태그(meta, script 등) → 제외
- 스타일 정보 첨부 (아래의 내용)
// 예시 렌더 트리 노드 구조
{
type: 'div',
computedStyle: {
color: 'blue',
padding: '20px',
display: 'block',
// ... 기타 계산된 스타일
},
children: []
}
4. 레이아웃(리플로우) 계산의 구체적 예시
.example {
width: 50%;
padding: 20px;
border: 1px solid black;
margin: 10px;
}
- 박스 모델 계산 과정 (위의 내용)
- 이런 스타일의 경우:
- 컨테이너 너비 계산 (부모 요소의 50%)
- padding 영역 계산 (각 방향 20px 추가)
- border 영역 계산 (각 방향 1px 추가)
- margin 영역 계산 (각 방향 10px의 여백)
- 최종 박스 크기와 위치 결정
5. 실제 자바스크립트 실행 과정
<!-- 시나리오 1: 일반적인 script 태그 -->
<script src="app.js"></script>
<!-- 시나리오 2: async 사용 -->
<script async src="analytics.js"></script>
<!-- 시나리오 3: defer 사용 -->
<script defer src="main.js"></script>
- script 태그 처리의 구체적 시나리오:
- 각 시나리오별 처리 과정:
- 일반 script:
- HTML 파싱 중단
- 스크립트 다운로드
- 실행 완료 후 파싱 재개
- async script:
- HTML 파싱 계속
- 스크립트 다운로드 완료 시점에 파싱 중단
- 실행 후 파싱 재개
- 실행 순서 불확실
- defer script:
- HTML 파싱 계속
- 파싱 완료 후 순서대로 실행
- DOM 조작 안전
- 일반 script:
6. 리렌더링 트리거 상황과 최적화
// 리플로우 발생
element.style.width = '100px';
element.style.height = '200px';
element.style.margin = '20px';
element.style.padding = '10px';
element.style.borderWidth = '1px';
element.style.position = 'absolute';
// 리페인트만 발생
element.style.color = 'red';
element.style.backgroundColor = 'blue';
element.style.visibility = 'hidden';
- 리플로우를 발생시키는 구체적인 속성들 (위의 내용)
// 잘못된 방법 (여러 번의 리플로우 발생)
const element = document.getElementById('example');
element.style.width = '100px';
element.style.height = '200px';
element.style.margin = '20px';
// 최적화된 방법 (한 번의 리플로우)
const element = document.getElementById('example');
element.classList.add('new-layout');
.new-layout {
width: 100px;
height: 200px;
margin: 20px;
}
- 리플로우를 발생시키는 구체적인 속성들을 최적화하는 방법 (위의 내용)
7. 브라우저별 렌더링 차이점
- 주요 브라우저 엔진별 특징:
- Blink (Chrome, Edge): V8 자바스크립트 엔진과 최적화된 통합
- WebKit (Safari): 레이어 기반 렌더링 최적화
- Gecko (Firefox): 독자적인 렌더링 아키텍처
핵심 최적화 포인트
1. 스크립트 로딩 전략
- script 태그 위치:
- body 종료 태그 직전에 위치 권장
- DOM 완성 후 스크립트 실행 보장
- 페이지 로딩 시간 단축 효과
- async와 defer 속성:
- async: 독립적인 스크립트에 사용
- defer: DOM 조작이 필요한 스크립트에 사용
- 적절한 속성 선택으로 로딩 최적화
2. 렌더링 성능 최적화
- 리플로우 최소화:
- 여러 스타일 변경을 한 번에 처리
- 불필요한 DOM 조작 피하기
- 애니메이션 요소는 position: fixed/absolute 사용 고려
- CSS 최적화:
- 복잡한 선택자 피하기
- 스타일 변경이 잦은 요소는 별도 레이어로 분리
- CSS 규칙의 깊이를 최소화
3. 리소스 로딩 최적화
- HTML 구조화:
- 중요 리소스는 우선적으로 로드되도록 배치
- CSS는 head에, JavaScript는 body 끝에 배치
- 불필요한 리소스 로드 지연 또는 제거
- 리소스 요청 최적화:
- 필요한 리소스만 선택적으로 로드
- 이미지 스프라이트, CSS/JavaScript 번들링 활용
- 적절한 캐싱 전략 수립
4. 브라우저 렌더링 고려사항
- DOM 조작:
- 가능한 한 한 번에 여러 변경 사항 적용
- DocumentFragment 활용하여 리플로우 최소화
- 복잡한 렌더링 작업은 requestAnimationFrame 사용
- 이벤트 처리:
- 이벤트 위임 패턴 활용
- 스크롤, 리사이즈 이벤트의 스로틀링/디바운싱
- 무거운 계산은 Web Worker 활용 고려
추가 고려사항
1. async/defer 스크립트의 정확한 이해
- async 특성:
- 스크립트 로드와 HTML 파싱이 병렬로 진행
- 로드가 완료되면 즉시 실행
- 실행 순서가 보장되지 않음
- 독립적인 스크립트(예: 분석 도구)에 적합
- defer 특성:
- 스크립트 로드는 병렬로 진행
- HTML 파싱 완료 후 순서대로 실행
- DOM 조작이 필요한 스크립트에 적합
- 실행 순서가 보장됨
2. 렌더링 성능 모니터링
- 성능 측정:
- 브라우저 개발자 도구의 Performance 패널 활용
- 렌더링 병목 지점 식별
- 불필요한 리렌더링 감지
- 최적화 포인트:
- 크리티컬 렌더링 패스 최적화
- 자바스크립트 실행 시간 관리
- 메모리 누수 방지
3. 모던 브라우저의 렌더링 특성
- 렌더링 엔진 동작:
- 점진적 렌더링 수행
- 가능한 빨리 사용자에게 콘텐츠 표시
- 비동기 로딩과 렌더링 최적화
- 최신 기술 활용:
- IntersectionObserver를 통한 지연 로딩
- CSS containment를 통한 렌더링 범위 제한
- CSS will-change 속성을 통한 최적화 힌트 제공
예상문제 [🔥]
https://github.com/junh0328/prepare_frontend_interview?tab=readme-ov-file
브라우저의 렌더링 과정에 대해 설명해보세요.
브라우저의 렌더링 과정은 크게 다음과 같은 단계들로 이루어집니다.
먼저 브라우저는 HTML 문서를 파싱하여 DOM 트리를 생성합니다. 이 과정에서 HTML 문서를 토큰화하고, 이를 노드로 변환하여 트리 구조를 만듭니다.
다음으로 CSS를 파싱하여 CSSOM을 생성합니다. CSS는 상속이라는 특성이 있기 때문에, 이를 반영한 트리 구조가 만들어집니다.
이렇게 생성된 DOM과 CSSOM을 결합하여 렌더 트리를 생성합니다. 렌더 트리는 실제로 화면에 표시될 요소들만을 포함합니다. 예를 들어, display: none이 적용된 요소나 meta 태그 같은 것들은 렌더 트리에서 제외됩니다.
렌더 트리가 생성되면 각 노드의 정확한 위치와 크기를 계산하는 레이아웃(리플로우) 과정을 거치고, 마지막으로 실제 화면에 픽셀을 그리는 페인팅 과정이 진행됩니다.
이러한 과정은 DOM이나 CSSOM이 변경될 때마다 다시 발생할 수 있으며, 이를 리렌더링이라고 합니다.
브라우저의 렌더링 과정에 자바스크립트는 어떻게 동작하나요?
브라우저가 HTML을 파싱하다가 script 태그를 만나면, HTML 파싱을 일시 중단하고 자바스크립트 코드를 처리합니다.
자바스크립트 처리는 브라우저의 자바스크립트 엔진이 담당하는데, 크게 세 단계로 진행됩니다. 먼저 코드를 토큰으로 분해하고, 이를 파싱하여 AST(추상 구문 트리)를 생성한 다음, 이를 바이트코드로 변환하여 실행합니다.
자바스크립트는 DOM API를 통해 DOM을 동적으로 변경할 수 있습니다. 이런 변경이 발생하면 브라우저는 변경된 DOM과 CSSOM을 다시 렌더 트리로 결합하고, 레이아웃과 페인팅 과정을 다시 수행합니다.
특히 중요한 점은 script 태그의 처리 방식인데, 기본적으로는 파싱을 중단시키지만 async나 defer 속성을 사용하면 비동기적으로 처리할 수 있습니다. async는 스크립트의 다운로드가 완료되는 즉시 실행되고, defer는 HTML 파싱이 완료된 후에 실행됩니다.
script 태그를 body 태그 밑에 둬야하는 이유가 있을까요?
script 태그를 body 태그 끝에 두는 데는 두 가지 중요한 이유가 있습니다.
첫 번째는 성능 측면입니다. 브라우저는 script 태그를 만나면 HTML 파싱을 중단하고 스크립트를 다운로드하고 실행합니다. 만약 script 태그가 head에 있다면, 스크립트 처리가 완료될 때까지 HTML 파싱이 중단되어 사용자가 페이지의 컨텐츠를 보는 데 지연이 발생합니다. body 끝에 두면 HTML 파싱이 완료된 후에 스크립트가 로드되므로, 사용자는 더 빠르게 페이지 컨텐츠를 볼 수 있습니다.
두 번째는 에러 방지입니다. 자바스크립트에서 DOM을 조작하는 코드가 있을 때, HTML이 완전히 파싱되기 전에 실행되면 존재하지 않는 DOM 요소에 접근하려 시도할 수 있어 에러가 발생할 수 있습니다. body 끝에 스크립트를 두면 DOM이 완성된 후에 스크립트가 실행되므로 이러한 문제를 방지할 수 있습니다.
물론 최근에는 async나 defer 속성을 사용하여 이러한 문제를 해결할 수도 있지만, 여전히 script 태그를 body 끝에 두는 것이 가장 안전하고 명확한 방법으로 여겨지고 있습니다.
'🧱 프론트엔드 주제 > JavaScript' 카테고리의 다른 글
[모던 자바스크립트 Deep Dive] 40장 - 이벤트 (0) | 2024.11.11 |
---|---|
[모던 자바스크립트 Deep Dive] 39장 - DOM (0) | 2024.11.11 |
[모던 자바스크립트 Deep Dive] 36장 - 배열 디스트럭처링 할당 (0) | 2024.10.25 |
[모던 자바스크립트 Deep Dive] 35장 - 스프레드 문법 (1) | 2024.10.22 |
[모던 자바스크립트 Deep Dive] 26장 - ES6 함수의 추가 기능 (8) | 2024.10.17 |