우리는 html 에 script 태그를 사용하여 자바스크립트 파일을 로드하곤 한다.
이 때, script 태그의 위치와 어떤 어트리뷰트가 존재하느냐에 따라 로드되는 방식에 차이가 있다.
이 차이를 이해하고 있어야 상황에 맞게 script태그를 위치 시킬 수 있다.
이 글에서는 script 태그를 불러오는 방식에 대해 정리를 해보려고 한다.
브라우저 렌더링 과정에서 살펴본 바와 같이, 렌더링 엔진과 자바스크립트 엔진은 병렬적으로 파싱을 실행하지 않고 직렬적으로 파싱을 수행한다.
이처럼 브라우저는 동기적으로 위에서 아래 방향으로 순차적으로 HTML, CSS, 자바스크립트를 파싱하고 실행한다.
이것은 script 태그의 위치에 따라 HTML 파싱이 블로킹되어 DOM 생성에 영향을 미칠 수 있다는 것을 의미한다.
따라서 script 태그의 위치는 중요하다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="app.js"></script>
</head>
<body>
<div id="hello">hello</div>
</body>
</html>
위의 코드에서 렌더링 엔진이 app.js 파일을 로드하는 script 태그를 만나면 DOM 파싱을 중단하고, app.js 파일을 로드한 후, 해당 자바스크립트 파일을 실행한다.
이때, 자바스크립트 코드(app.js)에서 DOM이나 CSSOM을 변경하는 DOM API 를 사용할 경우 DOM이나 CSSOM이 생성되어 있지 않기 때문에 에러가 발생한다.
아래의 코드를 보자.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
document.querySelector('#hello').innerHTML = 'world';
</script>
</head>
<body>
<div id="hello">hello</div>
</body>
</html>
script 태그 내에서 DOM API인 document.querySelector('#hello')를 호출하였다.
해당 API를 호출하는 시점에는, 렌더링 엔진이 id가 'hello'인 HTML 요소를 파싱하지 않았기 때문에, DOM에는 해당 요소가 포함되어 있지 않다. 따라서 아래의 사진과 같이 에러가 발생하는 것을 확인할 수 있다.
이러한 문제를 해결하기 위해 body 태그의 가장 아래 쪽에 자바스크립트 파일을 로드하는 script 태그를 위치시킬 수 있다.
아래 코드를 보자.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<div id="hello">hello</div>
<script>
document.querySelector('#hello').innerHTML = 'world';
</script>
</body>
</html>
렌더링 엔진이 script 태그를 파싱할 때는 이미 렌더링 엔진이 HTML 요소를 모두 파싱하여 DOM 생성이 완료된 이후다.
따라서 DOM 이 완성되지 않은 상태에서 DOM을 조작하는 에러가 발생할 우려가 없다.
또한 자바스크립트 파일이 실행되기 이전에 DOM 생성이 완료되어 렌더링되므로 페이지 로딩 시간이 단축된다는 이점 또한 있다.
앞에서 살펴본 자바스크립트 파싱에 의한 DOM 생성이 중단되는 문제를 근본적으로 해결하기 위해 HTML5부터 script 태그에 async와 defer 어트리뷰트가 추가되었다.
<script async src="script.js"></script>
<script defer src="script.js"></script>
async와 defer 어트리뷰트를 사용하면 HTML파싱과 외부 자바스크립트 파일의 로드가 비동기적으로 동시에 실행된다.
하지만 자바스크립트 실행의 시점에 차이가 있다.
HTML 파싱과 외부 자바스크립트 파일의 로드가 비동기적으로 동시에 진행된다.
단, 자바스크립트의 파싱과 실행은 자바스크립트 파일의 로드가 완료된 직후 진행되며, 이 때 HTML 파싱이 중단된다.
여러 개의 script 태그에 async 어트리뷰트를 지정하면 script 태그의 순서와는 상관없이 로드가 완료된 자바스크립트부터 먼저 실행되므로 순서가 보장되지 않는다.
따라서 순서 보장이 필요한 script 태그에는 async 어트리뷰트를 지정하지 않아야 한다.
async 어트리뷰트는 IE10 이상에서 지원된다.
async 어트리뷰트와 마찬가지로 HTML 파싱과 외부 자바스크립트 파일의 로드가 비동기적으로 진행된다.
단, 자바스크립트의 파싱과 실행은 HTML 파싱이 완료된 직후, 즉, DOM 생성이 완료된 직후 DOMContentLoaded 이벤트가 발생
진행된다.
따라서 DOM 생성이 완료된 이후, 실행되어야 할 자바스크립트에 유용하다.
defer 어트리뷰트는 IE10 이상에서 지원된다. IE6 ~ 9 에서도 지원되기는 하지만, 정상적으로 작동하지 않을 수 있다.
아무래도 DOM 파싱에 영향을 미치지 않고, 비동기적으로 실행이 되는 async, defer 어트리뷰트를 사용하는 것이 좋을 것같다.
하지만, DOM API 를 호출하거나 CSSOM을 조작하는 일이 있다면, defer 어트리뷰트를 사용하는 것이 좋겠다.
async 어트리뷰트의 경우에는, 로드된 직후 실행되기 때문에, 해당 자바스크립트 파일에서 조작하는 노드가 DOM 에 존재한다고 보장할 수 없기 때문이다. DOM과 CSSOM을 조작하지 않는 자바스크립트 파일에 대해서만 async 어트리뷰트를 사용하면 괜찮을 것 같다.