
구글의 V8 엔진으로 빌드된 자바스크립트 런타임 환경인 Node.js가 등장함으로써 웹의 영역에서 벗어나 서버 사이드 애플리케이션 영역까지 커버할 수 있는 범용 개발 언어가 되었다.
그래도 역시나 주력 분야는 웹 브라우저 환경에서 동작하는 웹 페이지/애플리케이션 의 클라이언트 사이드이다. (major 는 무시못하져)
대부분의 프로그래밍 언어는 운영체제(OS) 또는 가상머신(VM) 과 손잡고 실행되지만, 웹 애플리케이션 의 클라이언트 사이드인 자바스크립트는 HTML, CSS 의 손을 잡고 실행된다.
자, 그럼 프론트엔드라면 모를 수가 없는 HTML, CSS, JavaScript 로 작성된 문서를 어떻게 파싱하여 렌더링하는지 살펴보자!
✋🏻 잠깐! 파싱과 렌더링이 뭐야?
- 파싱(
parsing) : 구문 분석이라는 의미로 작성된 텍스트 문서를 프로그래밍 언어가 읽고 실행할 수 있도록 문자열을 토큰으로 분해한 후, 토큰에 문법적 의미와 구조를 반영하여 트리 구조의 자료구조를 생성하는 과정을 의미한다.- 렌더링(
rendering) : HTML, CSS, JavaScript 로 작성된 문서를파싱하여 브라우저에시각적으로 출력하는 것을 의미한다.
그렇다면 브라우저가 렌더링을 수행하는 과정은 어떻게 될까?
1️⃣ HTML, CSS, JavaScript, 이미지, 폰트 등 렌더링에 필요한 리소스를 요청하고 서버로부터 응답 받는다.
2️⃣ 렌더링 엔진은 응답된 HTML, CSS 를 파싱하여 DOM과 CSSOM 을 생성하고 결합하여 렌더 트리를 생성한다.
3️⃣ 자바스크립트 엔진은 응답된 자바스크립트를 파싱 하여 AST를 생성하고 바이트코드로 변환하여 실행한다.
이 때, 자바스크립트는 DOM API를 통해 DOM 이나 CSSOM 변경을 할 수 있으며, 변경된 DOM과 CSSOM은 다시 렌더 트리로 결합된다. (변경상태 업데이트 완료!)
4️⃣ 최종적으로 업데이트 된 렌더 트리를 기반으로 HTML 요소의 레이아웃을 계산하고 우리가 보는 브라우저 화면에 요소를 그린다.
이제 단계별로 하나씩 살펴보자.
브라우저의 핵심 기능이라고하면 필요한 리소스를 서버에 요청하고, 응답받아 브라우저에 시각적으로 렌더링 하는 것이다.
https://www.naver.com/
우리에게 너무나 익숙한 이 것, URL 이다.
이 곳에서 검색어를 입력하고 엔터키를 누르면 검색결과를 볼 수 있다.
이것이 우리가 사용하는 과정을 겉으로만 봤을 때이다.
우리가 엔터키를 누르고 검색결과를 보는 간단해보이는 과정이 브라우저에서의 처리과정으로 보면 어떨까?
서버에 리소스를 요청하기위해 브라우저는 주소창을 제공한다.
제공된 주소창에 우리는 요청할 내용을 URL에 담아서 입력하고 엔터키를 누르면 결과가 보인다.
https://search.naver.com/search.naver?where=nexearch&sm=top_hty&fbm=0&ie=utf8&query=%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8&ackey=9m6l1yfc
위 URL 은 네이버에서 '자바스크립트' 로 검색했을 때 주소창의 모습이다.
검색어를 입력하고 엔터키를 누르면 루트요청(/) 과 호스트(domain, 도메인) 으로 구성된 요청이 서버로 전송된다.
루트요청에는 리소스를 요청하는 내용이 명확하게 나타나있지 않지만, 암묵적으로 서버는 루트요청에 대해서는 index.html 을 응답하도록 설정되어 있다.
(이것이 우리가 루트디렉토리에 index.html을 구성하는 이유인 것이다.)
💡 요청과 응답은 개발자도구-Network에서 확인할 수 있다.
| 웹에서 브라우저와 서버가 통신하기 위한 규약(프로토콜)이다.
그렇다면 HTTP 1.1 과 HTTP 2.0의 차이점은 뭘까?
HTTP 1.1은 기본적으로 커넥션당 하나의 요청과 응답만 처리한다.
즉, 여러 개의 요청이 들어와도 한 번에 전송할 수 없으며, 응답도 마찬가지이다.
(1.1이라는 숫자에 연관지어서 생각해보면, 1:1 처리방식이라고 생각하면 좋겠다. "하나의 요청엔 하나의 응답만이!")
그렇기때문에 응답시간은 요청할 리소스의 개수에 비례한다는 단점이 있다.
이러한 단점을 보완하여 나온 것이 HTTP 2.0이다. (두-둥🎉)
HTTP 2.0은 커넥션당 여러 개의 요청와 응답이 가능하므로 HTTP 1.0보다 페이지 로드속도가 약 50%정도 빠르다고 한다.
순수한 텍스트로 이루어진 HTML문서를 브라우저에 시각적으로 표현하려면 브라우저가 이해할 수 있는 자료구조(DOM)로 변환하여 메모리에 저장해야 한다.
그 과정은 아래와 같다.

여기서 DOM은 HTML문서를 파싱한 결과물이라고 볼 수 있다.
렌더링 엔진은 HTML 을 한 줄씩 순차적으로 파싱하여 DOM을 생성한다.
그 과정중에서 CSS 로드를 담당하는 link, style 태그를 만나면 DOM 생성을 잠시 멈추고, CSS 로드 담당 태그내용을 먼저 처리한다.
파싱하여 CSSOM을 생성하는 과정은 DOM을 생성하는 과정과 동일하며, CSS파싱을 완료하고나면 일시중지해놨던 DOM 생성을 다시 시작한다.
렌더링 엔진은 서버로부터 응답된 HTML과 CSS 파싱과정을 거쳐 DOM와 CSSOM을 생성하고, DOM과 CSSOM은 최종 렌더링을 위해 렌더 트리(render tree)로 결합된다.
렌더 트리는 렌더링을 위한 트리 구조의 자료구조이기 때문에 브라우저 화면에 렌더링되지 않는 노드(meta, script태그 등)와 CSS에 의해 표시되지 않는(display:none) 노드들은 포함되지 않는다.
렌더 트리는 브라우저에 렌더링되는 노드만으로 구성되어 있다.
이렇게 완성된 렌더 트리는 각 HTML 요소의 레이아웃 계산에 사용되며, 브라우저 화면을 렌더링하는 페인팅처리에 입력값으로 사용된다.
브라우저 렌더링 과정은 구성값이 변경되면 반복적으로 실행된다.
이렇게 페인팅을 다시 실행하게하는 리렌더링은 비용이 많이들고, 성능에 악영향을 줄 수 있기 때문에 리렌더링이 자주 발생하지 않도록 주의해야 한다.🚨
자바스크립트 코드에서 DOM API 를 사용하면 이미 생성된 DOM의 동적 조작이 가능하다.
CSS 파싱과정에서 살펴본 것처럼 HTML을 한 줄씩 파싱하면서 script 태그를 만나게되면 DOM 생성을 일시중지하고 script 코드를 먼저 실행한다.
CSS때와의 차이점이라고하면 자바스크립트 파싱과 실행은 브라우저의 렌더링 엔진이 아닌 자바스크립트 엔진이 처리한다.
자바스크립트 엔진에는 다양한 종류가 있으며, 모든 자바스크립트 엔진은 ECMAScript 사양을 준수하게 되어있다.
렌더링 엔진이 HTML과 CSS를 파싱하여 DOM과 CSSOM을 생성한 것처럼,
자바스크립트 엔진은 자바스크립트를 파싱하여 AST(추상적 구문 트리) 를 생성한다. 그 과정을 그림으로 살펴보면 다음과 같다.

여기서 토크나이징과 파싱, 바이트코드 생성과 실행 단계에 대해서 간단하게 살펴보자.
토크나이징 : 단순 문자열인 자바스크립트 소스코드를 문법적 의미를 갖는 코드의 최소 단위인 토큰들로 분해하는 과정이다.파싱 : 이 장의 초반에 살펴본 개념이지만 한번 더 살펴보자면, 토큰들의 집합을 구문분석 하여 AST 를 생성한다.바이트코드 생성과 실행 : 파싱의 결과물인 AST는 인터프리터가 실행할 수 있는 중간 단계인 바이트코드로 변환되고, 인터프리터에 의해 실행된다.앞서 살펴보았던 DOM과 CSSOM이 변경되면, 다시 렌더 트리로 결합되고 그렇게 변경된 렌더 트리를 기반으로 페인트 과정을 지나 브라우저 화면에 렌더링되는 과정을 리플로우, 리페인트 이라고 한다.
리플로우
레이아웃에 영향을 주는 변경이 발생한 경우가 해당됨리페인트
변경이 일어났을 때 리플로우, 리페인트가 반드시 다 실행되는 것은 아니다.
레이아웃에 영향이 없는 변경은 리페인트만 실행이 되는 것이다.
렌더링 엔진과 자바스크립트 엔진의 파싱 과정을 살펴봤을 때, 병렬적으로 파싱을 실행한다기보단 직렬적 으로 파싱을 진행한 것이 보였을 것이다.
이처럼 브라우저는 동기적 으로 (위에서 아래, 순차적) HTML, CSS, 자바스크립트를 파싱하고 실행한다. 이 말은 script 태그 위치에 따라 HTML 파싱이 블로킹되어 DOM 생성이 지연될 수 있다는 말이다.
script 태그의 위치는 그만큼 중요하다!
❌ script 태그의 위치가 올바르지 못할 경우, DOM이 완성되지 않은 상태에서 자바스크립트가 DOM 조작을 하게되어 에러가 발생할 수 있다.
✅ 반면 script 태그를 올바르게 위치시키면, 자바스크립트 로딩/파싱/실행으로 인해 HTML 요소들의 렌더링에 지장받는 일이 발생하지 않아 페이지 로딩시간이 단축되는 효과가 있다.
자바스크립트 파싱에 의해 DOM 생성이 중단되는 문제를 근본적으로 해결하기 위해서는 어떻게 하는 것이 좋을까?
⇒ script 태그에 async, defer 어트리뷰트를 추가하는 것이다.
async 와 defer 어트리뷰트는 src 어트리뷰트를 통해 외부 자바스크립트 파일을 로드하는 경우에만 사용할 수 있다. (src 없이는 못살아~)
<script async src="example.js"></script>
<script defer src="example.js"></script>
async와defer어트리뷰트를 사용하면 HTML 파싱과 외부 자바스크립트 파일의 로드가 비동기적으로 진행된다.
❗️하지만 자바스크립트의 실행 시점에는 두 가지가 차이가 있다.
async
완료된 직후 진행되며, 이때 HTML 파싱은 중단된다.defer
defer 의 경우에는 IE6~9에서도 지원은 되나 정상작동하지 않을 수 있다.