HTML 이벤트를 공부하다가 `document.addEventListener("DOMContentLoaded", ...)`과 `window.addEventListener('load',...)`에 대한 분별을 하지 못해 정확한 이해가 부족하다고 생각하여 기존 글 내용의 중요한 부분을 표시하고 이해가 쉽도록 첨언하여 흐름에 따라서 정리한 내용입니다.

DOMContentLoaded, load, beforeunload, unload 이벤트
HTML 문서의 생명주기엔 다음과 같은 3가지 주요 이벤트가 관여합니다.
- `DOMContentLoaded` - 브라우저가 HTML을 전부 읽고 DOM 트리를 완성(=DOM 구성 완료)하는 즉시 발생합니다. 이미지 파일(`<img> `태그의 src 속성 값 대상)이나 스타일시트(`<link>`태그의 href 속성 값 대상 혹은 `<style>`태그의 @import 대상) 등의 기타 자원(resource)은 기다리지 않습니다.
- `load` - HTML로 DOM 트리를 만드는 게 완성되었을 뿐만 아니라 이미지, 스타일 시트 같은 외부 자원도 모두 불러오는 것이 끝났을 때 발생합니다.
- `beforeunload/unload` - 사용자가 페이지를 떠날 때 발생합니다.
세 이벤트는 다음과 같은 상황에서 활용할 수 있습니다.
- `DOMContentLoaded` - DOM이 준비된 것을 확인한 후 원하는 DOM 노드를 찾아 핸들러를 등록해 인터페이스를 초기화 할 때
- `load` - 이미지 사이즈를 확인할 때 등, 외부 자원이 로드된 후이기 때문에 스타일이 적용된 상태이므로 화면에 뿌려지는 요소의 실제 크기를 확인할 수 있음
- `beforeunload` - 사용자가 사이트를 떠나려 할 때, 변경되지 않은 사항들을 저장했는지 확인시켜줄 때
- `unload` - 사용자가 진짜 떠나기 전에 사용자 분석 정보를 담은 통계자료를 전송하고자 할 때
DOMContentLoaded
이 이벤트는 `window`객체가 아닌 `window` 객체 하위의 `document`객체에서 발생합니다.
따라서 이 이벤트를 다루려면 `addEventListener`를 사용해야 합니다.
document.addEventListener("DOMContentLoaded", ready);
// "document.onDOMContentLoaded = ..."는 동작하지 않습니다.
예시:
<script>
function ready() {
alert('DOM이 준비되었습니다!');
// 이미지가 로드되지 않은 상태이기 때문에 사이즈는 0x0입니다.
alert(`이미지 사이즈: ${img.offsetWidth}x${img.offsetHeight}`);
}
document.addEventListener("DOMContentLoaded", ready);
</script>
<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">
위 예시에서 `DOMContentLoaded` 핸들러는 문서가 로드 되었을 때 실행됩니다. 따라서 핸들러 아래쪽에 위치한 `<img>` 뿐만 아니라 모든 요소에 접근할 수 있습니다.
그렇지만 이미지가 로드되는 것은 기다리지 않기 때문에 `alert` 창에는 이미지 사이즈가 0이라고 뜹니다.
▶태그 그 자체는 기다리지만 불러오는 리소스(`src` 속성값)는 기다리지 않는다는 것을 알 수 있습니다.(= 기다리지 않을 뿐 그 리소스가 용량이 작아 `DOMContentLoaded` 이벤트 발생 전에 이미 전부 불러와져 있을 수도 있음)
처음 `DOMContentLoaded` 이벤트를 접하면 그다지 복잡하지 않은 이벤트라고 생각할 수 있습니다. "DOM 트리가 완성되면 `DOMContentLoaded`이벤트가 발생한다."라고 생각하기 때문이죠. 하지만 `DOMContentLoaded`에는 몇 가지 특이사항이 있습니다.
DOMContentLoaded와 scripts
브라우저는 HTML 문서를 처리하는 도중에 `<script>` 태그를 만나면 DOM 트리 구성을 멈추고 `<script>`를 실행합니다.
스크립트 실행이 끝난 후에야 나머지 HTML 문서를 처리하죠. `<script>`에 있는 스크립트가 DOM 조작 관련 로직을 담고 있을 수 있기 때문에 이런 방지책이 만들어졌습니다. 따라서 `DOMContentLoaded` 이벤트 역시 `<script>` 안에 있는 스크립트가 처리되고 난 후에 발생합니다.
예시를 통해 이를 살펴봅시다.
<script>
document.addEventListener("DOMContentLoaded", () => {
alert("DOM이 준비되었습니다!");
});
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></script>
<script>
alert("라이브러리 로딩이 끝나고 인라인 스크립트가 실행되었습니다.");
</script>
예시를 실행하면 "라이브러리 로딩이 끝나고…"가 먼저 보인 후 "DOM이 준비되었습니다!"가 출력되는 것을 확인할 수 있습니다. 스크립트가 모두 실행되고 난 후에야 DOMContentLoaded 이벤트가 발생한 것이죠.
DOMContentLoaded를 막지 않는 스크립트
위와 같은 규칙엔 두 가지 예외사항이 있습니다.
1. `async` 속성이 있는 스크립트는 `DOMContentLoaded`를 막지 않습니다. `async` 속성에 대해서 밑에서 다룹니다.
2. `document.createElement('script')`와 같이 자바스크립트 코드로 동적으로 생성되고 웹페이지에 추가된 스크립트는 `DOMContentLoaded`를 막지 않습니다. 이에 대해서도 밑에서 자세하게 다룹니다.
DOMContentLoaded와 styles
외부 스타일시트는(만으로는) DOM에 영향을 주지 않기 때문에 기본적으로 DOMContentLoaded는 외부 스타일시트가 로드되기를 기다리지 않습니다. css 파일 참조는 DOM parser와 DOM 빌드를 막지 않습니다.
그런데 한 가지 예외가 있습니다. 스타일시트를 불러오는 태그보다 아래에 스크립트(스타일시트 이후의 모든 스크립트를 의미)가 위치하면 이 스크립트는 스타일시트가 로드되기 전까지 실행되지 않습니다.
<link type="text/css" rel="stylesheet" href="style.css">
<script>
// 이 스크립트는 위 스타일시트가 로드될 때까지 실행되지 않습니다.
alert(getComputedStyle(document.body).marginTop);
</script>
이런 예외는 스크립트에서 스타일에 영향을 받는 요소의 프로퍼티를 사용할 가능성이 있기 때문에 만들어졌습니다. 위 예시에선 스크립트에서 요소의 좌표 정보를 사용하고 있네요. 스타일이 로드되고, 적용되고 난 다음에야 좌표 정보가 확정되기 때문에 자연스레 이런 제약이 생겼습니다. 좀 더 정확히 말하자면, script fetch 자체는 css 파일 fetch와 병렬로 실행되지만, css 문서 파싱과 해당 css까지 CSSOM에 반영 후 script가 실행(execution)됩니다.
DOMContentLoaded는 스크립트가 로드되길 기다립니다. 위의 경우라면 당연히 스타일시트 역시 기다리게 됩니다.
(참고로 style태그로 불러오는 스타일이나 link태그로 불러오는 스타일 소스가 스크립트보다 아래에 있으면 기다리지 않기 때문에 스타일 적용 전의 값을 참고합니다.)
브라우저 자동 완성
Firefox와 Chrome, Opera의 폼 자동완성(form autofill)은 `DOMContentLoaded`에서 일어납니다.
페이지에 아이디와 비밀번호를 적는 폼이 있고, 브라우저에 아이디, 비밀번호 정보가 저장되어 있다면 `DOMContentLoaded` 이벤트가 발생할 때 인증 정보가 자동으로 채워집니다. 물론 사용자가 자동 완성을 허용했을 때 그렇겠죠.
따라서 실행해야 할 스크립트가 길어서 `DOMContentLoaded` 이벤트가 지연된다면 자동완성 역시 뒤늦게 처리됩니다. 브라우저 자동 완성 기능을 켜 놓은 사용자라면 특정 사이트에서 자동 완성이 늦게 처리되는 걸 경험 해 보셨을 겁니다. 이런 사이트에선 페이지 로딩이 다 끝난 후에야 아이디나 패스워드 같은 브라우저에 저장한 정보가 폼에 뜨죠. 이런 지연이 발생하는 이유는 `DOMContentLoaded` 이벤트가 실행되는 시점 때문입니다.
window.onload
`window` 객체의 `load` 이벤트는 스타일, 이미지 등의 리소스들이 모두 로드되었을 때 실행됩니다. `load` 이벤트는 `onload` 프로퍼티를 통해서도 사용할 수 있습니다.
아래 예시에서 window.onload는 이미지가 모두 로드되고 난 후 실행되기 때문에 이미지 사이즈가 제대로 출력되는 것을 확인할 수 있습니다.
<script>
window.onload = function() { // window.addEventListener('load', (event) => {와 동일합니다.
alert('페이지 전체가 로드되었습니다.');
// 이번엔 이미지가 제대로 불러와 진 후에 얼럿창이 실행됩니다.
alert(`이미지 사이즈: ${img.offsetWidth}x${img.offsetHeight}`);
};
</script>
<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">
window.onunload
`window` 객체의 `unload` 이벤트는 사용자가 페이지를 떠날 때, 즉 문서를 완전히 닫을 때 실행됩니다. unload 이벤트에선 팝업창을 닫는 것과 같은 딜레이가 없는 작업을 수행할 수 있습니다.
그런데 분석 정보를 보내는 것은 예외사항에 속합니다.
사용자가 웹사이트에서 어떤 행동을 하는지에 대한 분석 정보를 모으고 있다고 가정해봅시다.
`unload` 이벤트는 사용자가 페이지를 떠날 때 발생하므로 자연스럽게 `unload` 이벤트에서 분석 정보를 서버로 보내는 게 어떨까 하는 생각이 드네요.
메서드 `navigator.sendBeacon(url, data)`은 바로 이런 용도를 위해 만들어졌습니다. 메서드에 대한 자세한 설명은 https://w3c.github.io/beacon/에서 찾아볼 수 있습니다.
`sendBeacon`는 데이터를 백그라운드에서 전송합니다. 다른 페이지로 전환 시 분석 정보는 제대로 서버에 전송되지만, 딜레이가 없는 것은 바로 이 때문입니다.
`sendBeacon`은 다음과 같이 사용할 수 있습니다.
let analyticsData = { /* 분석 정보가 담긴 객체 */ };
window.addEventListener("unload", function() {
navigator.sendBeacon("/analytics", JSON.stringify(analyticsData));
};
- 요청은 POST 메서드로 전송됩니다.
- 요청 시 문자열뿐만 아니라 폼이나 fetch에서 설명하는 기타 포맷들도 보낼 수 있습니다. 대개는 문자열 형태의 객체가 전송됩니다.
- 전송 데이터는 64kb를 넘을 수 없습니다.
`sendBeacon` 요청이 종료된 시점엔 브라우저가 다른 페이지로 전환을 마친 상태일 확률이 높습니다. 따라서 서버 응답을 받을 수 있는 방법이 없죠. 사용자 분석 정보에 관한 응답은 대개 빈 상태입니다.
`fetch` 메서드는 '페이지를 떠난 후’에도 요청이 가능하도록 해주는 플래그 `keepalive`를 지원합니다. 자세한 내용은 Fetch API에서 확인해보세요.
한편, 다른 페이지로 전환 중에 이를 취소하고 싶은 경우가 생기곤 합니다. `unload`에선 페이지 전환을 취소할 수 없고 `onbeforeunload`를 사용하면 가능합니다.
window.onbeforeunload
사용자가 현재 페이지를 떠나 다른 페이지로 이동하려 할 때나 창을 닫으려고 할 때 beforeunload 핸들러에서 추가 확인을 요청할 수 있습니다.
`beforeunload` 이벤트를 취소하려 하면 브라우저는 사용자에게 확인을 요청합니다.
아래 예시를 실행하고, 브라우저에서 새로 고침을 해 직접 확인해봅시다.
window.onbeforeunload = function() {
return false;
};
`false`말고도 비어있지 않은 문자열을 반환하면 이벤트를 취소한 것과 같은 효과를 볼 수 있는데, 이는 역사적인 이유 때문에 남아있는 기능입니다. 과거엔 문자열을 반환하면 브라우저에서 이 문자열을 보여줬었는데, 근래의 명세서에선 이를 권장하지 않습니다.
예시를 살펴봅시다.
window.onbeforeunload = function() {
return "저장되지 않은 변경사항이 있습니다. 정말 페이지를 떠나실 건 가요?";
};
이렇게 문자열을 반환하도록 해도 얼럿창에 문자열이 보이지 않게 된 이유는 몇몇 사이트 관리자들이 오해가 생길 법하거나 성가신 메시지를 띄우면서 `beforeunload`를 남용했기 때문입니다. 오래된 브라우저에서 위 예시를 실행하고 새로 고침을 누르면 “저장되지 않은…” 메시지가 뜨긴 합니다. 하지만 모던 브라우저에선 beforeunload 이벤트를 취소할 때 보이는 메시지를 커스터마이징 할 수 없습니다.
▶beforeunload 후에 onunload 이벤트가 발생하겠네요.
readyState
이미 문서가 완전히 로드된 후에 `DOMContentLoaded` 핸들러를 설정하면 어떤 일이 발생할까요?
핸들러 설정 전에 이미 문서가 완전히 로드된 후라서 문서가 또 로드될 이유는 없기 때문에 절대 실행되지 않을 겁니다.
그런데 가끔은 문서가 로드되었는지 아닌지를 판단할 수 없는 경우가 있습니다. DOM이 완전히 구성된 후에 특정 함수를 실행해야 할 때는 DOM 트리 완성 여부를 알 수 없어 조금 난감하죠.
이럴 때 현재 로딩 상태를 알려주는 `document.readyState` 프로퍼티를 사용할 수 있습니다.
프로퍼티의 값은 세 종류가 있습니다.
- `loading` – 문서를 불러오는 중일 때
- `interactive` – 문서가 완전히 불러와졌을 때
- `complete` – 문서를 비롯한 이미지 등의 리소스들도 모두 불러와졌을 때
우리는 `document.readyState`의 값을 확인하고 상황에 맞게 핸들러를 설정하거나 코드를 실행하면 됩니다.
예시:
function work() { /*...*/ }
if (document.readyState == 'loading') {
// 아직 로딩 중이므로 이벤트를 기다립니다.
document.addEventListener('DOMContentLoaded', work);
} else {
// DOM이 완성되었습니다!
work();
}
이 외에도 상태가 변경되었을 때 실행되는 이벤트 `readystatechange`를 사용하면 상태에 맞게 원하는 작업을 할 수 있습니다. 개발자 도구의 콘솔창을 열고 아래 예시를 실행해 상태 변화를 직접 출력해봅시다.
// 현재 상태
console.log(document.readyState);
// 상태 변경 출력
document.addEventListener('readystatechange', () => console.log(document.readyState));
이렇게 아주 오래전부터 있었던 readystatechange 이벤트라는 대안을 사용해도 문서 로딩 상태를 파악할 수 있습니다. 그런데 이 이벤트는 요즘엔 잘 사용하지 않습니다.
이제 마무리로 지금까지 배운 이벤트들의 순서에 대해 정리해봅시다.
아래 예시엔 이벤트를 로깅하는 `<iframe>`과 `<img>`를 비롯한 여러 이벤트 핸들러가 있습니다.
<script>
log('초기 readyState:' + document.readyState);
document.addEventListener('readystatechange', () => log('readyState:' + document.readyState));
document.addEventListener('DOMContentLoaded', () => log('DOMContentLoaded'));
window.onload = () => log('window onload');
</script>
<iframe src="iframe.html" onload="log('iframe onload')"></iframe>
<img src="http://en.js.cx/clipart/train.gif" id="img">
<script>
img.onload = () => log('img onload');
</script>
실행 결과는 다음과 같습니다.
[1] initial readyState:loading
[2] readyState:interactive
[2] DOMContentLoaded
[3] iframe onload
[4] img onload
[4] readyState:complete
[4] window onload
대괄호 안에 있는 숫자는 실제 해당 로그가 출력되기까지 걸린 시간을 나타냅니다. 같은 숫자는 1 미리 초 오차 범위 내에서 동시에 실행된 이벤트라는 것을 의미합니다.
`document.readyState`는 `DOMContentLoaded`가 실행되기 바로 직전에 `interactive`가 됩니다. 따라서 `DOMContentLoaded`와 `interactive`는 같은 상태를 나타낸다고 볼 수 있습니다.
`document.readyState`는 `iframe`, `img`를 비롯한 리소스 전부가 로드되었을 때 `complete`가 됩니다. 위 예시에서 우리는 `readyState`의 값이 `img.onload`와 `window.onload`가 실행된 시점과 거의 동일한 시점에 `complete`로 바뀌었다는 것을 확인할 수 있습니다. `readyState`의 값이 `complete`로 바뀐다는 것은 `window.onload`가 실행된다는 것과 동일한 의미입니다. 이 둘의 차이점은 window.onload는 다른 load 핸들러가 전부 실행된 후에야 동작한다는 것에 있습니다.
요약
- DOM 구성 전까지의 단계 간략화 : HTML문서 다운로드 ▶ HTML 문서 처리 중- 파싱(=구문 분석) 및 DOM tree 구성 중 ▶ DOM(DOM tree) 구성 완료 (이후에 외부 리소스 불러오는 것이 기본적임)
- 다음으로 페이지 로드 관련 이벤트는 다음과 같습니다.
- `DOMContentLoaded` – DOM 구성이 완료되었을 때 `document` 객체에서 실행됩니다. 자바스크립트를 사용해 요소를 조작하는 것은 이 이벤트가 실행된 후입니다.
- <script>...</script>나 <script src="..."></script>를 사용해 삽입한 스크립트는 DOMContentLoaded가 실행되는 것을 막습니다. 브라우저는 이 스크립트가 실행되길 기다립니다. (= 기본적으로는 스크립트 태그 만나면 다운로드 및 실행을 한다. 이를 다 기다려야 한다.)
- `<link type="stylesheet" href="style.css">` (스타일시트) 이후에 `<script>`가 있는 경우 스타일시트에 영향을 받는 (DOM) 요소의 프로퍼티를 스크립트에서 사용해야 할 경우도 있을 수 있기 때문에 이 경우에는 link태그의 실행까지 기다린다. 즉, `script`가 `type=stylesheet` 이후에 있으면 스타일시트와 스크립트 순서로 모두 기다린다.

- `DOMContentLoaded`는 실행되어도 이미지를 비롯한 기타 리소스들은 여전히 로드 중일 수 있습니다.(용량이 작아 이미 로드 되었을 수도 있음. 단지 기다리지 않을 뿐.)
- `load` – 페이지를 비롯한 이미지 등의 자원 전부가 모두 불러와졌을 때 `window` 객체에서 실행됩니다. 모든 자원이 로드되는 걸 기다리기에는 시간이 오래 걸릴 수 있으므로 이 이벤트는 잘 사용되지 않습니다.
- `beforeunload` – 사용자가 페이지를 떠나려 할 때 window 객체에서 발생합니다. 이 이벤트를 취소하려 하면 브라우저는 사용자에게 "we have unsaved changes…"등의 메시지를 띄워 정말 페이지를 떠날 건지 물어봅니다.
- `unload` – 사용자가 최종적으로 사이트를 떠날 때 window 객체에서 발생합니다. unload 이벤트 핸들러에선 지연을 유발하는 복잡한 작업이나 사용자와의 상호작용은 할 수 없습니다. 이 제약사항 때문에 unload 이벤트는 아주 드물게 사용됩니다. 하지만 예외적으로 navigator.sendBeacon을 사용해 네트워크 요청을 보낼 수 있습니다.
- `document.readyState` – 문서의 현재 상태를 나타내줍니다. `readystatechange` 이벤트를 사용하면 변화를 추적할 수 있습니다.
- `loading` – 문서를 불러오는 중일 때
- `interactive` – 문서가 완전히 불러와졌을 때. DOMContentLoaded가 실행되기 바로 직전에 해당 값으로 변경됩니다.
- `complete` – 문서를 비롯한 이미지 등의 리소스들도 모두 불러와졌을 때. window.onload가 실행되기 바로 직전에 해당 값으로 변경됩니다.
defer, async 스크립트
모던 웹브라우저에서 돌아가는 스크립트들은 대부분 HTML보다 ‘무겁습니다’. 용량이 커서 다운로드받는 데 오랜 시간이 걸리고 처리하는 것 역시 마찬가지이죠.
브라우저는 HTML을 읽다가 <script>...</script> 태그를 만나면 스크립트를 먼저 실행해야 하므로 DOM 생성을 멈춥니다. 이는 src 속성이 있는 외부 스크립트 <script src="..."></script>를 만났을 때도 마찬가지입니다. 외부에서 스크립트를 다운받고 실행한 후에야 남은 페이지를 처리할 수 있습니다.
이런 브라우저의 동작 방식은 두 가지 중요한 이슈를 만듭니다.
- 스크립트에서는 스크립트 아래에 있는 DOM 요소에 접근할 수 없습니다. 따라서 DOM 요소에 핸들러(on*)를 추가하는 것과 같은 여러 행위가 불가능해집니다.
- 페이지 위쪽에 용량이 큰 스크립트가 있는 경우 스크립트가 페이지를 ‘막아버립니다’. 페이지에 접속하는 사용자들은 스크립트를 다운받고 실행할 때까지 스크립트 아래쪽 페이지를 볼 수 없게 됩니다.
<p>...스크립트 앞 콘텐츠...</p>
<script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
<!-- 스크립트 다운로드 및 실행이 끝나기 전까지 아래 내용이 보이지 않습니다. -->
<p>...스크립트 뒤 콘텐츠...</p>
이런 부작용들을 피할 수 있는 몇 가지 방법이 있습니다. 아래 예시처럼 스크립트를 페이지 맨 아래 놓는 것이 하나의 방법이 될 수 있죠. 이렇게 하면 스크립트 위에 있는 요소에 접근할 수 있습니다. 또한, 페이지 콘텐츠 출력을 막지 않게 됩니다.
<body>
... 스크립트 위 콘텐츠들 ...
<script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
</body>
그런데 이 방법은 완벽한 해결책이 아닙니다. HTML 문서 자체가 아주 큰 경우를 가정해봅시다. 브라우저가 HTML 문서 전체를 다운로드 한 다음에 스크립트를 다운받게 하면 페이지가 정말 느려질 겁니다.
네트워크 속도가 빠른 곳에서 페이지에 접속하고 있다면 이런 지연은 눈에 잘 띄지 않습니다. 하지만 아직도 네트워크 환경이 열악한 곳이 많습니다. 모바일 네트워크 접속이 느린 곳도 많죠.
다행히도 이런 문제를 해결할 수 있는 `<script>` 속성이 있습니다. 바로 `defer`와 `async`입니다.
defer
브라우저는 `defer` 속성이 있는 스크립트(이하 defer 스크립트 또는 지연 스크립트)를 '백그라운드’에서 다운로드 합니다. 따라서 지연 스크립트를 다운로드 하는 도중에도 HTML 파싱이 멈추지 않습니다. 그리고 defer 스크립트 실행은 페이지 구성이 끝날 때까지 지연 됩니다.
▶다운로드는 HTML 파싱을 멈추지 않는다. HTML 파싱되는 와중에 스크립트를 만나면 병렬로 실행된다(스크립트 외부 리소스 전부 병렬로 다운). 실행은 HTML 파싱이 끝난 후(DOM tree 완성 후)에 실행된다.

위쪽 예시와 동일한 코드인데 스크립트에 defer만 붙여보겠습니다.
<p>...스크립트 앞 콘텐츠...</p>
<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
<!-- 바로 볼 수 있네요! -->
<p>...스크립트 뒤 콘텐츠...</p>
- 지연 스크립트는 페이지 생성을 절대 막지 않습니다.
- 지연 스크립트는 DOM이 준비된 후에 실행되긴 하지만 DOMContentLoaded 이벤트 발생 전에 실행됩니다.
예시를 통해 직접 살펴봅시다.
<p>...스크립트 앞 콘텐츠...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("`defer` 스크립트가 실행된 후, DOM이 준비되었습니다!")); // (2)
</script>
<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
<p>...스크립트 뒤 콘텐츠...</p>
- 페이지 콘텐츠는 바로 출력됩니다.
- DOMContentLoaded 이벤트는 지연 스크립트 실행을 기다립니다. 따라서 얼럿창은 DOM 트리가 완성되고 지연 스크립트가 실행된 후에 뜹니다.
지연 스크립트는 일반 스크립트와 마찬가지로 HTML에 추가된 순(상대순, 요소순)으로 실행됩니다.
따라서 길이가 긴 스크립트가 앞에, 길이가 짧은 스크립트가 뒤에 있어도 짧은 스크립트는 긴 스크립트가 실행될 때까지 기다립니다.
<script defer src="https://javascript.info/article/script-async-defer/long.js"></script>
<script defer src="https://javascript.info/article/script-async-defer/small.js"></script>
- 위 코드에서 작은 스크립트는 먼저 다운되지만, 실행은 나중에 됩니다.
브라우저는 성능을 위해 페이지에 어떤 스크립트들이 있는지 쭉 살펴본 후에야 스크립트를 병렬적으로 다운로드합니다. 위 예시에서도 스크립트 다운로드가 병렬적으로 진행되었습니다. 그런데 이 때 크기가 작은 `small.js`이 `long.js`보다 먼저 다운로드 될 수 있습니다. 하지만 명세서에서 스크립트를 문서에 추가한 순서대로 실행하라고 정의했기 때문에 `small.js`는 `long.js` 다음에 실행됩니다.
- defer 속성은 외부 스크립트에만 유효합니다. <scrpt>에 src가 없으면 defer 속성은 무시됩니다.
async
`async` 속성이 붙은 스크립트(이하 async 스크립트 또는 비동기 스크립트)는 페이지와 완전히 독립적으로 동작합니다.
async 스크립트는 defer 스크립트와 마찬가지로 백그라운드에서 다운로드됩니다. 따라서 HTML 페이지는 async 스크립트 다운이 완료되길 기다리지 않고 페이지 내 콘텐츠를 처리, 출력합니다(하지만 async 스크립트 실행중에는 HTML 파싱이 멈춥니다 – 옮긴이).

DOMContentLoaded 이벤트와 async 스크립트는 서로를 기다리지 않습니다.
페이지 구성이 끝난 후에 async 스크립트 다운로딩이 끝난 경우, DOMContentLoaded는 async 스크립트 실행 전에 발생할 수 있습니다,
async 스크립트가 짧아서 페이지 구성이 끝나기 전에 다운로드 되거나 스크립트가 캐싱처리 된 경우, DOMContentLoaded는 async 스크립트 실행 후에 발생할 수도 있습니다.
▶다운로드는 HTML 파싱을 멈추지 않는다. HTML 파싱되는 와중에 스크립트를 만나면 병렬로 실행된다(스크립트 외부 리소스 전부 병렬로 다운). 다운로드가 DOM 완성보다 먼저 된다면, 실행은 HTML 파싱을 멈추고(DOM tree 구성 중) 실행된다. 비동기 스크립트 실행이 끝나면 HTML 파싱을 마저 진행한다.
다른 스크립트들은 async 스크립트를 기다리지 않습니다. async 스크립트 역시 다른 스크립트들을 기다리지 않습니다.
이런 특징 때문에 페이지에 async 스크립트가 여러 개 있는 경우, 그 실행 순서가 제각각이 됩니다. 실행은 다운로드가 끝난 스크립트 순으로 진행됩니다.
<p>...스크립트 앞 콘텐츠...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM이 준비 되었습니다!"));
</script>
<script async src="https://javascript.info/article/script-async-defer/long.js"></script>
<script async src="https://javascript.info/article/script-async-defer/small.js"></script>
<p>...스크립트 뒤 콘텐츠...</p>
- 비동기 스크립트 다운로드는 페이지 로딩을 막지 않기 때문에 페이지 콘텐츠가 바로 출력됩니다.(상황에 따라 비동기 스크립트가 먼저 다운로드 되면, 실행완료 될 때까지 기다려야 하기 때문에 페이지 로딩을 막을 수도 있음)
- `DOMContentLoaded` 이벤트는 상황에 따라 비동기 스크립트 전이나 후에 실행됩니다. 정확한 순서를 예측할 수 없습니다. ▶비동기 스크립트의 다운로드 완료 시점에 달려있음(사실상 예측 불가). DOM tree 완성 전에 먼저 다운로드 되면, 비동기 스크립트가 실행완료 될 때까지 기다린 후에 파싱을 다시 재개해야 파싱이 끝나기 때문에 `DOMContentLoaded` 이벤트의 발생 시점이 늦춰질 수도 있음.
- 비동기 스크립트는 서로를 기다리지 않습니다. 위치상으론 `small.js`가 아래이긴 하지만 `long.js`보다 먼저 다운로드되었기 때문에 먼저 실행됩니다. 이렇게 먼저 로드가 된 스크립트가 먼저 실행되는 것을 'load-first order’라고 부릅니다.(선언된 순서대로 실행되지 않는다는 것일 뿐, 그렇다고 다른 스크립트가 동시 실행되지는 않음.)
비동기 스크립트는 방문자 수 카운터나 광고 관련 스크립트처럼 각각 독립적인 역할을 하는 서드 파티 스크립트를 현재 개발 중인 스크립트에 통합하려 할 때 아주 유용합니다. async 스크립트는 개발 중인 스크립트에 의존하지 않고, 그 반대도 마찬가지이기 때문입니다.
<!-- Google Analytics는 일반적으로 다음과 같이 삽입합니다. -->
<script async src="https://google-analytics.com/analytics.js"></script>
동적 스크립트(async 스크립트처럼 행동)
자바스크립트를 사용하면 문서에 스크립트를 동적으로 추가할 수 있습니다. 이렇게 추가한 스크립트를 동적 스크립트(dynamic script)라고 부릅니다.
let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script); // (*)
위 예시에서 외부 스크립트는 관련 요소가 문서에 추가되자 마자((*)로 표시한 줄) 다운로드가 시작됩니다.
그런데 동적 스크립트는 기본적으로 ‘async’ 스크립트처럼 행동합니다.
따라서 다음과 같은 특징(기본 특징)을 갖습니다.
- 동적 스크립트는 그 어떤 것도 기다리지 않습니다. 그리고 그 어떤 것도 동적 스크립트를 기다리지 않습니다.
- 먼저 다운로드된 스크립트가 먼저 실행됩니다(‘load-first’ order).
아래 예시에선 두 스크립트를 동적으로 문서에 추가합니다. 그런데 script.async=false가 없었다면 이 스크립트들은 'load-first order’로 실행됩니다. 그럼 크기가 작은 small.js가 먼저 실행되겠죠. 하지만 script.async=false가 있기 때문에 실행은 '문서에 추가된 순서’대로 됩니다. ▶스크립트 추가할 때 기본 속성 변경이 가능하다.
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
script.async = false;
document.body.append(script);
}
// async=false이기 때문에 long.js가 먼저 실행됩니다.
loadScript("/article/script-async-defer/long.js");
loadScript("/article/script-async-defer/small.js");
요약
`async`와 `defer` 스크립트는 다운로드 시 페이지 렌더링을 막지 않는다는 공통점이 있습니다. 따라서 async와 defer를 적절히 사용하면 사용자가 오래 기다리지 않고 페이지 콘텐츠를 볼 수 있게 할 수 있습니다.
두 스크립트의 차이점은 다음과 같습니다.
| 순서 | DOMContentLoaded | |
| async | load-first order. 문서 내 순서와 상관없이 먼저 다운로드된 스크립트가 먼저 실행됩니다.(병렬 실행은 불가능) | 비동기 스크립트는 HTML 문서가 완전히 다운로드되지 않은 상태라도 로드 및 실행될 수 있습니다. 스크립트 크기가 작거나 캐싱 처리 되어있을 때 혹은 HTML 문서 길이가 아주 길 때 이런 일이 발생합니다. (DOMContentLoaded 이벤트의 발생 시점은 async script의 다운로드 시점에 달려있다.) |
| defer | 문서에 추가된 순 | 지연 스크립트는 문서 다운로드와 파싱이 완료된 후에, DOMContentLoaded 이벤트 발생 바로 직전에 실행됩니다. |
- 스크립트 다운로드가 끝나지 않았어도 페이지는 동작해야 합니다.
`defer`를 사용하게 되면 스크립트가 실행되기 전에 페이지가 화면에 출력된다는 점에 항상 유의해야 합니다. 사용자는 그래픽 관련 컴포넌트들이 준비되지 않은 상태에서 화면을 보게 될 수 있죠. 따라서 지연 스크립트가 영향을 주는 영역엔 반드시 '로딩 인디케이터’가 있어야 합니다. 관련 버튼도 사용 불가(disabled) 처리를 해줘야 하죠. 이렇게 해야 사용자에게 현재 어떤 것은 사용할 수 있는지, 어떤 것은 사용할 수 없는지를 알려줄 수 있습니다.
실무에선 `defer`를 DOM 전체가 필요한 스크립트나 실행 순서가 중요한 경우에 적용합니다. `async`는 방문자 수 카운터나 광고 관련 스크립트같이 독립적인 스크립트에 혹은 실행 순서가 중요하지 않은 경우에 적용합니다.
(사진 모아 정리)


🚀이어지는 내용 : 모듈