브라우저는 어떻게 동작하는가?
사용자가 연결된 주소의 서버에 데이터 요청을 하게 되면 서버로부터 데이터를 다운로드 받은 것을 가지고 웹브라우저가 그것을 해석해서 사용자가 보는 UI 를 완성해 주게 됩니다. 그래서 웹은 네트워크가 상당히 중요합니다.
여기서 서버에서 다운로드 받은 HTML, CSS, JS를 브라우저가 해석할 때 원리를 알 필요가 있는데 이 내용에 대해 다뤄보도록 하겠습니다.
웹 브라우저 동작과정 간단히 알아보기
브라우저의 주요 기능은 사용자가 선택한 자원을 서버에 요청하고 브라우저에 표시하는 것입니다. 자원은 보통 HTML 문서지만 PDF나 이미지 또는 다른 형태일 수 있습니다. 자원의 주소는 URI(Uniform Resource Identifier)에 의해 정해집니다.
브라우저는 HTML과 CSS 명세에 따라 HTML 파일을 해석해서 표시하는데 이 명세는 웹 표준화 기구인 W3C(World Wide Web Consortium)에서 정합니다. 과거에는 브라우저들이 일부만 이 명세에 따라 구현하고 독자적인 방법으로 확장함으로써 웹 제작자가 심각한 호환성 문제를 겪었지만 현대에는 대부분의 브라우저가 표준 명세를 따르고 있습니다.
브라우저의 기본 구조
브라우저의 주요 구성 요소는 다음과 같습니다.
- 사용자 인터페이스 - 주소 표시줄, 이전/다음 버튼, 북마크 메뉴 등. 요청한 페이지를 보여주는 창을 제외한 나머지 모든 부분이다.
- 브라우저 엔진 - 사용자 인터페이스와 렌더링 엔진 사이의 동작을 제어.
- 렌더링 엔진 - 요청한 콘텐츠를 표시. 예를 들어 HTML을 요청하면 HTML과 CSS를 파싱하여 화면에 표시함.
- 통신 - HTTP 요청과 같은 네트워크 호출에 사용됨. 이것은 플랫폼 독립적인 인터페이스이고 각 플랫폼 하부에서 실행됨.
- UI 백엔드 - 콤보 박스와 창 같은 기본적인 장치를 그림. 플랫폼에서 명시하지 않은 일반적인 인터페이스로서, OS 사용자 인터페이스 체계를 사용.
- 자바스크립트 해석기 - 자바스크립트 코드를 해석하고 실행.
- 자료 저장소 - 이 부분은 자료를 저장하는 계층이다. 쿠키를 저장하는 것과 같이 모든 종류의 자원을 하드 디스크에 저장할 필요가 있다. HTML5 명세에는 브라우저가 지원하는 '웹 데이터 베이스'가 정의되어 있다.
[ 브라우저의 주요 구성 요소 ]
렌더링 엔진
렌더링 엔진의 역할은 요청 받은 내용을 브라우저 화면에 표시하는 일을 수행합니다.
렌더링 엔진은 HTML 및 XML 문서와 이미지를 표시할 수 있다. 물론 플러그인이나 브라우저 확장 기능을 이용해 PDF와 같은 다른 유형도 표시할 수 있습니다.
대표적 렌더링 엔진으로 파이어폭스와 웹킷 엔진이 있는데 파이어폭스는 모질라에서 직접 만든 게코(Gecko) 엔진을 사용하고 사파리와 크롬은 웹킷(Webkit) 엔진을 사용합니다.
웹킷은 최초 리눅스 플랫폼에서 동작하기 위해 제작된 오픈소스 엔진인데 애플이 맥과 윈도우즈에서 사파리 브라우저를 지원하기 위해 수정을 가했습니다. 더 자세한 내용은 webkit.org를 참조하기 바랍니다.
파싱(parse or parsing)이란?
문서를 파싱한다는 것은 브라우저가 코드를 이해하고 사용할 수 있는 구조로 변환하는 것을 의미합니다.
파싱 결과는 보통 문서 구조를 나타내는 노드 트리인데 파싱 트리(parse tree) 또는 문법 트리(syntax tree)라고 부릅니다.
브라우저가 문서(HTML)를 해석하면서 하는일
브라우저는 기본적으로 다음과 같은 작업을 수행합니다.
- 불러오기(Loading) - 불러오기는 HTTP 모듈 또는 파일시스템으로 전달 받은 리소스 스트림(resource stream)을 읽는 과정으로
로더(Loader)
가 이 역할을 맡고 있음. 로더는 단순히 읽는 것이 아니라, 이미 데이터를 읽었는지도 확인하고, 팝업창을 열지 말지, 또는 파일을 다운로드 받을 지를 결정한다. - 파싱(Parsing) - 파싱은
DOM(Document Object Model)
트리를 만드는 과정으로 일반적으로 HTML, XML 파서를 각각 가지고 있음. HTML 파서는 말 그대로 HTML 문서를 해석하는데 사용되고, XML 파서는 XML 형식을 따르는 SVG, MathML 등을 처리하는데 사용함. - 렌더링 트리(Rendering Tree) 만들기 - 파싱으로 생성된 DOM 트리는 HTML/XML 문서의 내용을 트리 형태로 자료 구조화 한 것을 말한다. 다시 말해, DOM 트리는 내용 자체를 저장하고 있고, 화면에 표시하기 위한 위치와 크기 정보, 그리는 순서 등을 저장하기 위한 별도의 트리 구조가 필요한데 이를 일반적으로 렌더링 트리라고 부른다.
- CSS 스타일 결정 - CSS 는 HTML 문서 내용과 별도로 표현을 나타내기 위해 만들어 졌음
- 레이아웃(Layout) - 렌더링 트리가 생성될 때, 각 렌더(Render) 객체가 위치와 크기를 갖게 되는 과정을 레이아웃이라고 한다.
- 그리기(Painting) - 그리기 단계는 렌더링 트리를 탐색하면서 특정 메모리 공간에 RGB 값을 채우는 과정이다.
스크립트와 스타일 시트의 진행 순서
다음은 일반적으로 많이 사용되는 코드입니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" type="text/css" href="default.css">
<link rel="stylesheet" type="text/css" href="pages.css">
<script src="jquery.js"></script>
<script src="user.ui.js"></script>
</head>
<body>
<img src="/images/sample.png" alt="">
<ul>
<li>HTML을 파싱</li>
<li>외부 CSS파일 및 외부 자바스크립트 파일을 로드</li>
<li>자바스크립트가 전달된 시점에 실행</li>
<li>DOM 트리의 구축 완료</li>
<li>이미지 파일 및 플래시 등의 외부 리소스를 로드</li>
<li>모두 완료</li>
</ul>
</body>
</html>
일반적으로 브라우저가 위와 같은 문서를 만나게 되면 HTML을 파싱하고 외부 자원인 CSS, JS 파일을 로드하게 됩니다.
자바스크립트는 전달된 시점에 실행하게 되고 DOM 트리의 구축을 완료한 이후에 이미지 파일 및 플래시 등의 외부 리소스를 로드하면서 모든 작업이 완료됩니다.
여기서 중요한 점이 "스크립트를 만나게 되면 어떻게 되는 것인가"
입니다.
JS인 <script> 태그를 만나면 스크립트가 해석 및 실행되는 동안 문서의 파싱은 중단되게 됩니다.
스크립트가 외부에 있는 경우 우선 네트워크로부터 자원을 가져와야 하는데 이 또한 실시간으로 처리되고 자원을 받을 때까지 파싱은 중단됩니다.
이 모델은 수 년간 지속됐고 HTML4와 HTML5의 명세에도 정의되어 있습니다.
한편 스타일 시트는 이론적으로 DOM 트리를 변경하지 않기 때문에 문서 파싱을 기다리거나 중단하지 않습니다.
위의 코드에서는 스타일을 먼저 불러오고 다음에 스크립트를 로드하지만 만약에 스크립트르 파일을 먼저 로드하게 되는 경우 즉, 스크립트가 문서를 파싱하는 동안 스타일 정보를 요청하는 경우라면 문제가 됩니다.
이 경우에 스크립트가 문서를 파싱하는 동안 브라우저는 다른 작업을 수행하지 않기 때문에 스타일이 파싱되지 않은 상태가 되고 이렇게 되었을 때 화면 레이아웃이 제대로 구성되지 않은 상태로 사용자에게 뷰를 제공하게 될 확률이 높기 때문에 사용자 경험(UX)을 떨어뜨리는 결과를 초래하게 될 것입니다. 이런 문제는 흔치 않은 것처럼 보이지만 매우 빈번하게 발생합니다.
이러한 문제를 야기시키지 않고 사용자 경험을 떨어뜨리지 않기 위해 다음과 같이 스크립트 소스를 body 태그 끝에 두는 것을 권장하고 있습니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" type="text/css" href="default.css">
<link rel="stylesheet" type="text/css" href="pages.css">
</head>
<body>
<img src="/images/sample.png" alt="">
<ul>
<li>HTML을 파싱</li>
<li>외부 CSS파일 및 외부 자바스크립트 파일을 로드</li>
<li>자바스크립트가 전달된 시점에 실행</li>
<li>DOM 트리의 구축 완료</li>
<li>이미지 파일 및 플래시 등의 외부 리소스를 로드</li>
<li>모두 완료</li>
</ul>
<script src="jquery.js"></script>
<script src="user.ui.js"></script>
</body>
</html>
위와 같이 스크립트 소스를 하단에 두게 되면 HTML 문서를 화면에 표시하는 속도가 빨라지게 되고 사용자가 뷰를 보는데 필요한 왠만한 문서를 해석한 상태이기 때문에 사용자의 불편을 초래하지 않을 수 있습니다.
스크립트의 로드 시점 - async, defer
지금까지 알아본 바에 의하면 스크립트를 문서의 마지막(</body>) 이전에 삽입하는 방식이 오래된 브라우저에서도 동일한 효과를 얻을 수 있으면서 사용자 경험 개선은 물론이고 이벤트를 이용한 프로그래밍을 처리할 필요도 줄어들게 합니다.
다만, 문서의 <head>
영역에 스크립트가 삽입되거나 외부의 파일에 정의되어 있다면 이벤트 연결은 문서의 로드시점에 맞게 처리해야 한다는 것입니다.
이렇게 head 에 삽입하는 경우에 모던 브라우저에서는 defer, async 속성을 사용할 수 있는데 이에 대해 알아보고자 합니다.
크롬 개발자 도구를 열고 네트워크 패널을 보게 되면 웹 패이지를 구성하는데 필요한 자원들을 서버에서 다운로드 받은 결과를 시각적으로 확인할 수 있는데 이 네트워크 패널의 하단에서 모든 자원을 다운받는데 걸린 총 시간과 서버에 요청한 갯수, 전송된 파일 크기 등을 확인하실 수 있습니다.
이 네트워크 패널에서 DOMContentLoaded
와 Load
를 확인할 수 있는데,
먼저 DOMContentLoaded
는 DOM 트리를 완성되는 시점을 말합니다.
쉽게 말해, images 와 같은 외부 자원(iframe, image)을 제외(ex. embedded type)한 HTML Element 를 해석, 구성해 주는 것을 말합니다.
그리고 Load
는 문서의 모든 콘텐츠(images, script, css, etc)가 로드된 상태를 말합니다.
그렇기 때문에 대개 DOMContentLoaded 가 끝나는 시점과 Load 시점 사이 또는 Load 이후에도 image 파일들을 로드하게 됩니다.
스크립트 이벤트를 다루는 시간은 아니기 때문에 자세한 내용은 문서의 로드시점을 참고하기 바랍니다.
스크립트의 일반적인 실행
async
와 defer
의 동작에 대해 알아보기 전에 기본적인 <script>
의 실행 과정에 대해 알아봅니다.
기본적으로 <script>
는 인라인 코드의 경우 즉시 해석되고 실행될 수 있지만 위에서 언급했듯이 그렇지 않은 경우는 해당 파일을 가져올 때까지 HTML 문서의 구문 분석을 중단한다.
위 그림에서 보여주듯이 스크립트를 가져 와서 실행하기 위해 HTML 구문 분석이 일시 중지되므로 HTML이 화면에 출력되는 시간이 길어집니다.
async 속성이 추가된 경우의 실행
async
속성은 브라우저에 스크립트 파일이 비동기적(동기 VS 비동기)으로 실행될 수 있음을 나타내기 위해 사용할 수 있습니다.
즉, HTML 구문 해석기는 스크립트 태그에 도달한 지점에서 스크립트를 가져오고 실행하기 위해 일시 중지 할 필요가 없습니다.
따라서 HTML 구문 분석과 병행하여 스크립트를 가져온 후 스크립트가 준비 될 때마다 즉시 실행이 가능해 집니다.
그러므로 실행의 순서가 다운로드 완료 시점의 결정되므로 실행 순서가 중요한 스크립트들에 async 를 사용할 때는 유의해야 합니다.
예를 들어 제작자가 jQuery 를 사용한다고 가정해 봅니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" type="text/css" href="default.css">
<link rel="stylesheet" type="text/css" href="pages.css">
<script async src="jquery.js"></script>
<script async src="user.ui.js"></script>
</head>
<body>
<img src="/images/sample.png" alt="">
<ul>
<li>HTML을 파싱</li>
<li>외부 CSS파일 및 외부 자바스크립트 파일을 로드</li>
<li>자바스크립트가 전달된 시점에 실행</li>
<li>DOM 트리의 구축 완료</li>
<li>이미지 파일 및 플래시 등의 외부 리소스를 로드</li>
<li>모두 완료</li>
</ul>
</body>
</html>
위 코드에는 async 속성을 사용했으며, jQuery 라이브러리와 사용자 정의인 user.ui.js 가 있습니다.
제작자가 jQuery 로 어떤 요소를 참조하고 있을 경우에 jquery 라이브러리를 먼저 선언했다고 해서 먼저 실행된다는 보장이 없다는 점에 유의해야 합니다.
보통 제이쿼리를 사용하는 사용자가 있다면 먼저 제이쿼리를 불러온 다음에 제이쿼리 문법을 사용할 것입니다.
이렇듯 내가 사용하는 스크립트가 제이쿼리에 의존하고 있다면 제이쿼리에 의존성을 가지고 있다고 할 수 있습니다.
이를 의존성(dependency) 이라고 말합니다.
이렇게 사용자가 작성한 스크립트가 jQuery 에 의존하고 있는데 사용자 스크립트보다 jQuery 의 다운로드가 완료되는 시점이 더 느리다면 어떻게 될까요?
async
는 위에서 언급했듯이 실행 순서가 다운로드 완료 시점에 결정되기 때문에
jQuery 가 다운로드 완료되는 시점이 더 느리다면 사용자가 jQuery 로 참조한 요소는 undefined
로 요소를 찾을 수 없을 것입니다.
다시말해, jQuery 에 의존하고 있는 사용자 스크립트가 먼저 실행된다면 jQuery 라이브러리를 사용할 수 없는 상태에 놓이게 된 것입니다.
하지만 HTML5 스펙이 async="false"
속성 지정시 호출 순서대로 실행되도록 추가되었습니다.(default : true)).
defer 속성이 추가된 경우의 실행
async(기본값 true)인 경우와 다르게 defer
속성은 HTML 구문 분석이 완전히 완료되면 스크립트 파일을 실행하도록 브라우저에 지시하게 됩니다.
<script defer src="jquery.js"></script>
<script defer src="user.ui.js"></script>
비동기적으로 로드된 스크립트와 마찬가지로, HTML 구문 분석이 실행되는 동안(HTML 해석을 방해하지 않고) 스크립트 파일을 다운로드 할 수 있습니다.
여기서 async 속성과 차이점이 있습니다.
HTML 구문 분석이 완료되기 전에 스크립트 다운로드가 완료 되더라도 구문 해석이 완료 될 때까지 스크립트는 실행되지 않는다는 점입니다.
또한, async
와는 다르게 호출된 순서대로 실행됩니다.
그렇기 때문에 먼저 선언된 스크립트 다운로드가 완료시점이 늦더라도 호출된 순서대로 실행되기 때문에 위에서 의존성을 잃어버릴 가능성이 있는 defer="true"
일 경우와는 다릅니다.
그렇다면 언제 사용해야 하는 것인가?
일반적인 스크립트 실행과 aync
, defer
실행을 결정하기 위해서는 몇가지 확인해야 할 사항이 있습니다.
<script>
요소는 어디에 있는가?
<script>
요소가 문서 맨 끝에 있지 않으면 스크립트의 비동기 및 지연 실행이 더 중요합니다.
HTML 문서는 첫 번째 여는 <html> 요소부터 닫히는 순서로 파싱됩니다.
외부 소스 JavaScript 파일이 닫는 </body> 요소 바로 앞에 있으면, async 또는 defer 속성을 사용하는 것이 큰 효과가 없을 것입니다.(HTML 파서가 그 시점까지 문서의 대다수를 완성 했기 때문에 지연에 대한 의미가 크게 없음).
스크립트 파일의 의존성(종속성) 여부에 따라
다른 파일들에 종속적이지 않거나 종속성(의존성) 자체가 없는 스크립트 파일의 경우 async 속성이 특히 유용합니다.
파일이 어느 지점에서 실행되는지 정확히 알 필요가 없기 때문에 비동기 로드가 가장 적합할 수 있습니다..
그리고 의존성을 가지고 있는 스크립트 파일이거나 어떤 이유로 든 문제의 파일을 다른 위치에 배치해야 하는 상황에서는 defer 를 사용하면 됩니다.
async와 defer를 지원하는 브라우저
async 및 defer 속성은 최신 브라우저를 기준으로 보자면 매우 광범위하게 지원되지만 이러한 속성의 동작은 JavaScript 엔진마다 약간 다를 수 있습니다.
지원하는 브라우저를 알아보기 전에 일부만 지원되는 경우에 대한 동작 과정을 알아봅니다. defer 속성만 있다면 스크립트는 페이지의 파싱이 완료된 후에 실행됩니다.
단, async와 defer 속성이 모두 지정된 경우 async 속성을 지원하는 모던 브라우저는 기본적으로 async 속성을 따른다.
하지만 async 속성을 지원하지 않는 구형 브라우저는 defer 속성의 지원 여부에 따라 결과가 다릅니다. defer 속성을 지원하는 경우 defer 속성에 의해 비동기적으로 스크립트를 실행하고 defer 조차도 지원하지 않는 구형 브라우저는 동기적으로 스크립트를 실행합니다.
IE의 경우가 예외적이긴 하지만 여전히 국내 환경이 뒷받침되고 있지 않지만 IE 10 미만의 점유율이 극히 낮아진 이 시점에서
이제는 고려하지 않아도 될 만한 수준이 되어야 된다고 봅니다.
만약에 굳이 지원해야 한다면 aync, defer 속성을 사용하기 보다 </body>
바로 앞에 스크립트를 선언하는 방법이 가장 권장됩니다.