프로그래밍을 완전 처음 배울 때, 정확히는 생활코딩 JavaScript
강의를 보면서 실습해 나갈 때
지인이 이런 말을 한 적이 있다.
<scrpit>
태그는 맨 뒤에 위치시키는 게 좋아~
이는 브라우저의 렌더링 원리와 과정에서 에러를 발생시킬 우려도 없앨 뿐더러 페이지 로딩 시간이 단축된다는 장점이 있다.
기본적으로 위의 이미지와 같이 프로그래밍은 요청과 응답의 반복 작업이다.
브라우저도 마찬가지이다.
브라우저에 렌더링 할 리소스를 서버에 요청하고 서버는 해당 요청에 응답한다.
브라우저에서 서버에 리소스를 요청할 때 주소창을 이용한다.
주소창에 URL을 입력하면 URL은 IP주소로 변환되고 이 IP주소를 이용하여 브라우저는 웹 서버에 해당 웹 사이트에 맞는 HTML
문서를 요청한다.
HTTP는 브라우저와 서버가 통신하기 위한 프로토콜이다.
2015년 HTTP 2.0이 발표되면서 기존 HTTP 1.1과 큰 차이가 있다.
HTTP 1.1은 기본적으로 하나의 요청에 하나의 응답만이 가능하다.
뒤에 언급할 예정이지만 HTML
내에 CSS
파일, JavaSciprt
파일을 요청하는 리소스를 개별적으로 전송된다는 것이다.
하지만 HTTP 2.0은 이러한 단점을 완전히 보완하여 발표되었기 때문에 HTTP 1.1 보다 페이지 로드 속도가 50% 정도 빠르다고 알려져 있다.
브라우저 요청에 의해 서버는 해당 요청에 맞는 HTML
문서를 브라우저에 응답한다.
이때 HTML
문서는 서버에 저장되어 있는 형태, 즉 2바이트 형태로 응답된다.
브라우저는 응답받은 2바이트 형태의 HTML
문서를 이해할 수 있도록 Document Object Model인 DOM을 생성한다.
브라우저의 렌더링 엔진은 다음과 같은 과정을 통해 DOM을 생성한다.
- 바이트 > 2. 문자 > 3. 토큰 > 4. 노드 > 5. DOM
조금 자세하게 풀어서 설명을 해본다면,
브라우저는 2바이트 형태로 응답받은 HTML
문서 내에 meta
태그의 인코딩 방식을 기준으로 문자열로 변환한다.
이렇게 문자열로 변환된 HTML 문서를 읽어 문법적 의미를 갖는 토큰으로 분해한다.
그리고 각 토큰들을 객체로 변환한 것이 노드가 된다.
노드는 DOM을 구성하는 기본 요소가 되고 이러한 노드는
HTML
요소 같의 부자 관계 등을 반영하여 트리 자료구조가 되는데
이러한 트리 자료구조가 DOM이라고 한다.
즉 DOM은 HTML
문서를 파싱한 결과물이 된다.
브라우저의 렌더링 엔진은 요청받은 HTML
문서를 파싱하며 DOM을 생성한다.
하지만 파싱하는 과정에서 CSS 파일을 불러오는 link
태그 등을 만나면
그 순간 HTML
문서 파싱을 중단하고 CSS
파일을 파싱한다.
파싱 과정은 HTML
문서를 파싱하는 과정과 같다.
파싱 결과물은 CSS Object Model인 CSSOM을 생성한다.
- 바이트 > 2. 문자 > 3. 토큰 > 4. 노드 > 5. CSSOM
CSSOM은 DOM과 함께 브라우저에서 페이지를 렌더링 하는 데에 사용되고
웹 성능 최적화의 중요한 요소로 작용된다.
이렇게 CSSOM을 생성한다면 HTML
문서 파싱을 중단한 시점으로 다시 돌아가 파싱을 재개한다.
위의 과정을 통해 HTML 문서를 파싱한 DOM, CSS 파일을 파싱한 CSSOM을 생성했다.
그리고 렌더링 엔진은 DOM과 CSSOM을 결합한 렌더 트리를 생성한다.
이때 렌더 트리는 페이지에 렌더링할 노드만을 포함한다.
즉, display: none;
과 같이 페이지 렌더링과 관련없는 노드들은 구성 요소에서 빠진다.
이렇게 페이지에 렌더링 하기 위한 렌더 트리까지 완성되었다면
렌더링 엔진은 당연히 페이지에 렌더링을 한다.
이 과정을 페인팅 처리라고 한다.
렌더링 엔진이 HTML
문서를 파싱해가면서 CSS
파일을 불러오는 link
태그를 만나면
그 상태로 HTML
파싱을 중단하고 CSS
파일을 파싱한다.
마찬가지로 렌더링 엔진이 HTML
문서를 파싱해가면서 JavaScript
파일을 불러오는 script
태그를 만다면 그 상태로 HTML
파싱을 중단하고JavaScript
파일을 파싱한다.
이때 JavaScript
파일을 파싱하기 위해 브라우저는 제어권을 렌더링 엔진에서 자바스크립트 엔진으로 넘긴다.
그리고 자바스크립트 엔진은 JavaScript
해석하여 Abstract Syntax Tree 인 AST를 생성한다.
자바스크립트 엔진이 JavaScript
를 해석하는 과정에서 다음과 같이 DOM을 조작하는 코드가 있을 수 있다.
const getEmailValue = document.querySelector(".inp-login-email")
그럼 DOM이나 CSSOM이 변경되고 다시 렌더 트리를 결합해
브라우저는 렌더링을 다시 진행한다.
이를 리플로우, 리페인트라고 한다.
하지만 리플로우는 요소의 크기나 배치 등 레이아웃에 변경되는 것들에만 한하여 진행된다.
다음과 같은 HTML
문서를 파싱한다고 가정해보자.
<!DOCTYPE>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
<script>
const getEmailValue = document.querySelector(".inp-login-email")
</script>
</head>
<body>
<input type="text" class="inp-login-email">
</body>
</html>
브라우저의 렌더링 엔진이 HTML
문서를 파싱하다가 script
태그를 만나면 파싱을 중단하고 자바스크립트 엔진에게 제어권을 넘긴다.
그리고 자바스크립트 엔진은 위와 같이 DOM을 조작하는 코드를 만난다면?
위와 같은 상황은 DOM 생성이 완료되지 않은 상황에서 자바스크립트 엔진이 DOM API를 사용한 경우이다.
이처럼 DOM이 생성되지 않은 시점에서 DOM을 조작하는 API의 코드는 정상적으로 동작하지 않는다.
또한 어찌어찌하게 된다고 한들, 레이아웃에 작용을 주는 DOM API를 만난다면 리플로우와 리페인팅 작업을 하게 될 수 있다.
이는 가장 위에서 언급했던 말의 근거이다.
"브라우저의 렌더링 원리와 과정에서 에러를 발생시킬 우려도 없앨 뿐더러 페이지 로딩 시간이 단축된다는 장점이 있다."
위의 예시는 다음과 같이 body
태그 아래에 위치시키면 된다.
<!DOCTYPE>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<input type="text" class="inp-login-email">
</body>
<script>
const getEmailValue = document.querySelector(".inp-login-email")
</script>
</html>
이처럼 DOM 생성이 완료되고 DOM API가 동작하기 때문에 에러를 방지시킬 수 있다.