📢 어렵고 정석적인 개념 설명보다는 저같은 초보자도 이해하기 쉽게 정리하는 것을 원칙으로 포스팅하고 있습니다. 😄
문제
코딩 공부를 위해 혹은 수정을 위해 여러 사이트를 뜯어보다 보면, 문득 드는 생각이 있다.
"어떤 건 script의 위치가 head에 있고, 어떤 건 body 안에 있고.. 이건 또 맨 밑에 있네? 도대체 script의 위치는 뭐가 맞는 거지...?" 이것은 웹 구조를 공부하지 않은 사람이라면 충분히 헷갈릴 만한 문제이다.
일단 정답은 HTML의 맨 밑에 위치하는 것이 일반적으로 좋다. 그 이유는 웹 브라우저가 HTML을 렌더링하는 과정을 보면 쉽게 이해할 것이다.
원리
<html>
<head>
<meta charset="UTF-8">
<!-- 1번 :: head 안에 위치 -->
<script src="./app.js"></script>
</head>
<body>
<div id="app">Hello!</div>
</body>
<!-- 2번 :: body 밑에 위치 -->
<script src="./app.js"></script>
</html>
DOM을 조작하는 코드가 있는 app.js를 불러온다고 하자. 1번의 경우 script는 실행되지 않는다.
하지만, 2번의 경우 script는 실행된다. 코드는 똑같지만, script를 어디서 불러오냐의 차이로 실행 여부가 결정되었다.
위 그림은 브라우저의 HTML 렌더링 과정이다. 브라우저는 우리가 코딩한 HTML을 위에서 아래로 쭉 읽으면서, DOM 트리를 생성한다. 그다음, DOM 트리와 CSS의 CSSOM 트리가 결합되면서 최종적으로 화면에 출력시킨다.
이때, 브라우저가 HTML을 읽는 과정에서 script를 만나게 되면 DOM 트리 생성을 잠시 중단하고, script를 읽게 된다.
(위의 그림처럼 HTML 파싱 과정에서 script를 만나면 script를 먼저 실행한 다음 파싱을 다시 재개한다.)
이 과정을 이해하면 위의 코드에서 1번 위치의 <script>가 왜 실행되지 않았는지 이해할 수 있을 것이다.
따라서, 일반적인 script를 불러오는 위치는 HTML의 파싱이 모두 끝난 body 밑이 가장 적합한 위치인 것이다.
하지만, 그렇다고 무조건 script를 <body> 아래에서 불러와야 한다는 법은 코딩에서 없다. '나는 무슨 일이 있어도 head에서 script를 불러오고야 말겠어!' 하는 사람들을 위해 (혹은 나를 위해) 3가지 방법을 준비했다.
방법
window.onload
window.onload = () => {
const wrap = document.getElementById('wrap');
wrap.style.color = 'red';
}
우리가 Jquery를 사용할 때, 항상 상단에 사용하는 코드가 있었다. 바로 $(document).ready()인데, 이게 자바스크립트에선 window.onload가 그 역할을 한다. 말 그대로 웹 브라우저 내의 모든 요소가 준비가 되었을 때 실행하라는 함수이다.
하지만, window.onload의 단점이 있다. 바로 중복 사용이 불가능하다는 점이다. 만약 script가 1개가 아닌 여러 개의 경우 첫 번째 스크립트에서 window.onload를 사용하고, 두 번째 스크립트에서도 사용한다면 최종적으로 가장 밑에 있는 두 번째 스크립트의 window.onload만 실행되면서 첫 번째 스크립트는 동작하지 않게 된다.
이를 방지하기 위해 window.addEventListener('load', function(){})를 사용하기도 한다. addEventListener()는 단순히 이벤트를 감지하는 함수이므로 중복으로 사용해도 상관없기 때문에, 위와 같은 불상사를 막을 수 있다.
만약, script가 여러 개이고 각각 window.onload를 사용 중이라면 1개만 남기고 나머지들을 addEventListener()로 바꾸거나 아니면 전부 addEventListener()로 바꿔주자.
* window.onload 혹은 window.addEventListener('load')가 단순히 스크립트를 <head>에서 불러오기 위해서만 쓰이는 용도는 아니다. 예를 들어 이미지와 같이 용량이 큰 경우, 이미지가 모두 로드되기 전에 스크립트가 먼저 실행될 수도 있다.
혹은 DOM 요소를 조작하는 스크립트가 있었을 때, 그 스크립트로 변형된 DOM 요소를 가져와야 하는 경우도 있을 수 있다. 이 때, load 이벤트를 사용한다면 이미지든 스크립트든 모두 로드가 끝난 이후에 스크립트를 실행하기 때문에 우리가 위에서 말한 로드 이전에 실행되거나 스크립트가 동시에 실행되는 등의 문제점을 해결할 수 있다.
따라서, load 이벤트는 'load'라는 이름처럼 나름 중요한 역할도 수행한다는 점을 잊지 말아야 한다.
async
<script async src="./app.js"></script>
async는 HTML과 script를 동시에 읽지만, script가 실행될 때는 HTML의 파싱을 잠시 중단시키고 script의 실행이 끝나게 돼서야 다시 HTML 파싱을 재개시킨다는 특징을 갖고 있다.
즉, 비동기 방식으로 script를 읽는다고 이해하면 쉽다. 일반적으로 script를 읽을 때는 아까 위의 그림에서도 봤듯이 HTML 파싱을 중단시킨 다음, script를 실행을 하는 동기적인 방식을 취하고 있다. 하지만, 이것은 상황에 따라 비효율적으로 보일 수 있다. 예를 들어, script를 외부에서 가져오는 경우 혹은 script의 코드가 많고 무거운 경우 브라우저가 script 자체를 다운로드받는 시간이 꽤 걸릴 수도 있다. 이럴 때 async를 사용하면 브라우저가 HTML 파싱과 script 다운로드를 동시에 하기 때문에 브라우저 입장에서 효율적으로 웹을 렌더링 할 수 있게 된다.
다만, async는 DOM 트리가 전부 생성된 이후에 script를 실행하는 것이 아닌 HTML 파싱 중간에서 냅다 실행해버리는 속성이기 때문에 비동기적으로 실행해야 하는상황이 아니라면 다음에 소개할 defer를 사용하도록 하자.
defer
<script defer src="./app.js"></script>
defer는 async와 마찬가지로 비동기적으로 script를 다운로드하지만, 실행은 HTML의 파싱이 모두 끝난 이후에 실행된다는 차이점을 가지고 있다. async와 달리 파싱이 모두 끝난 이후에서야 script가 실행된다는 특징이 있기 때문에, DOM을 조작해야 하는 직접적인 코드가 있다면 async보다는 defer를 사용하는 것을 권장한다.
async와 defer는 script를 HTML 파싱 과정과 동시에 읽는다는 점이 같다. 하지만, 실행되는 시점이 다르다.
async는 script를 읽음과 동시에 바로 실행되지만, defer는 HTML 파싱이 끝난 후 실행된다.
우선순위로 따지자면 async는 HTML 파싱 < script 실행이고, defer는 HTML 파싱 > script 실행이라고 보면 된다.
따라서, async는 직접적인 DOM 조작보다는 파일에 직접적인 영향이 없는 경우 (실행되는 시점이 중요하지 않는 경우) 사용하면 좋고 그 외의 경우에는 무난하게 defer를 사용하는 게 좋다.
1. <script> 위치는 일반적으로 <body> 밑이 좋다.
2. 굳이 위(head)에서 불러오고 싶다면, window.load 혹은 async/defer를 사용하자.
3. window.load는 중복 사용이 불가능하며, script가 여러 개의 경우 addEventListener()로 load를 감지하자.
4. async와 defer는 HTML 파싱과 동시에 script를 다운로드한다는 비동기적 공통점을 갖고 있다.
5. async는 script 다운로드가 끝나면, 잠시 HTML 파싱을 중단시키고 바로 스크립트를 실행시켜 버린다.
6. defer는 script 다운로드가 끝나도 HTML 파싱이 모두 끝나면 그때서야 스크립트가 실행된다.
7. 외부 스크립트 혹은 DOM에 직접적인 영향이 없는 script라면 async 사용, 그 외에는 defer 사용.
* 출처 - https://blog.asamaru.net/2017/05/04/script-async-defer/
'JavaScript' 카테고리의 다른 글
[JavaScript] 엘리먼트 요소의 상대 좌표와 절대 좌표 구하기 (0) | 2022.11.28 |
---|---|
[JavaScript] 스프레드 연산자(Spread Operator)를 쓰는 이유 (0) | 2022.11.23 |
[JavaScript] 중첩 if문을 깔끔하고 효율적으로 사용하는 방법 (0) | 2022.11.06 |
[JavaScript] undefined와 null은 무엇이 다른걸까? (0) | 2022.08.01 |
[JavaScript] Date()를 활용한 카운트다운 타이머 만들기 (0) | 2022.05.24 |