참고
DOMContentLoaded, load, beforeunload, unload 이벤트
defer, async 스크립트
HTML 문서의 생명주기엔 다음과 같은 3가지 주요 이벤트가 관여한다.
DOMContentLoaded
: 브라우저가 HTML을 전부 읽고 DOM 트리를 완성하는 즉시 발생한다. 이미지 파일이나 스타일시트 등의 기타 리소스들은 기다리지 않는다. load
: HTML로 DOM 트리를 만들고 나서 이미지, 스타일시트 등 외부 리소스도 모두 불러온 뒤에야 발생한다. beforeunload/unload
: 사용자가 페이지를 떠날 때 발생한다. DOMContentLoaded
이벤트는 document 객체에서 발생한다. 그러므로 이 이벤트를 다루려면 addEventListener를 사용해야 한다.
document.addEventListener("DOMContentLoaded", func)
DOMContentLoaded
에는 몇가지 특이사항이 존재한다.
브라우저는 HTML 문서를 처리하는 도중에 <script>
태그를 만나면, DOM 트리 구성을 멈추고 <script>
를 실행한다. 스크립트 실행이 끝난 후에야 나머지 HTML 문서를 처리한다. <script>
에 있는 스크립트 코드들이 DOM 조작과 관련된 로직을 담당하고 있기 때문에 이런 프로세스를 지니는 것이다. 따라서 DOMContentLoaded
이벤트 역시 <script>
안에 있는 스크립트 코드가 처리되고 난 후에 발생한다.
스크립트가 모두 실행되고 난 후에야 DOMContentLoaded
이벤트가 발생한다.
예외(DOMContentLoaded
를 막지 않는 스크립트)는 존재한다.
DOMContentLoaded
를 막지 않는다. document.createElement('script')
로 동적으로 생성되고 웹페이지에 추가된 스크립트는 DOMContentLoaded
를 막지 않는다. 외부 스타일시트는 DOM에 영향을 주지 않기 때문에 DOMContentLoaded
는 외부 스타일시트가 로드되는 걸 기다리지 않는다.
다만 한 가지 예외가 있다. 스타일시트를 불러오는 태그 바로 다음에 스크립트가 위치하면 이 스크립트는 스타일시트가 로드되기 전까지 실행되지 않는다.
<link type="text/css" rel="stylesheet" href="style.css">
<script>
// 이 스크립트는 위 스타일시트가 로드될 때까지 실행되지 않습니다.
alert(getComputedStyle(document.body).marginTop);
</script>
스크립트에서 스타일에 영향을 받는 요소의 프로퍼티를 사용할 가능성이 있기 때문이다.
기본적으로 DOMContentLoaded
는 스크립트가 로드되기를 기다린다. 위 경우라면 당연히 스타일시트를 기다린다.
Firefox와 Chrome, Opera의 폼 자동완성(form autofill)은 DOMContentLoaded
에서 일어난다.
실행해야 할 스크립트가 길어서 DOMContentLoaded
이벤트가 지연된다면 자동완성 역시 뒤늦게 처리된다.
window 객체의 load 이벤트는 스타일, 이미지 등의 리소스들이 모두 로드되었을 때 실행된다. load 이벤트는 onload 프로퍼티를 통해서도 사용할 수 있다.
window 객체의 unload 이벤트는 사용자가 페이지를 떠날 때, 즉 문서를 완전히 닫을 때 실행된다. unload 이벤트에선 팝업창을 닫는 것과 같은 딜레이가 없는 작업을 수행할 수 있다.
unload 이벤트는 사용자가 페이지를 떠날 때 발생하기 때문에
navigator.sendBeacon(url, data)
와 같은 메서드를 사용하기에 좋다.
let analyticsData = { /* 분석 정보가 담긴 객체 */ };
window.addEventListener("unload", function() {
navigator.sendBeacon("/analytics", JSON.stringify(analyticsData));
};
/*
1. 요청은 POST 메서드로 전송된다.
2. 요청 시 문자열 뿐만 아니라 폼이나 fetch에서 설명하는 기타 포맷들도 보낼 수 있다. 대개는 문자열 형태의 객체가 전송된다.
3. 전송 데이터는 64kb를 넘을 수 없다.
*/
sendBeacon 요청이 종료된 시점엔 브라우저가 다른 페이지로 전환을 마친 상태일 확률이 높다. 따라서 서버 응답을 받을 수 있는 방법이 없다. 사용자 분석 정보에 관한 응답은 대개 빈 상태이다.
fetch 메서드는 '페이지를 떠난 후’에도 요청이 가능하도록 해주는 플래그 keepalive를 지원한다.
한편, 다른 페이지로의 전환 중에 이를 취소하고 싶을 때는 onbeforeunload
이벤트를 사용하면 된다. unload
이벤트에서는 불가능하다.
사용자가 현재 페이지를 떠나 다른 페이지로 이동하려 할 때나 창을 닫으려고 할 때, beforeunload 핸들러에서 추가 확인을 요청할 수 있다.
예를 들어 아래 코드를 실행하고 새로고침을 하면 사이트 새로고침을 정말하겠냐는 alert 창이 뜬다.
window.onbeforeunload = function() {
return false;
};
문서가 완전히 로드된 후에 DOMContentLoaded 핸들러를 설정하면 절대 실행되지 않는다.
그런데 가끔은 문서가 로드되었는지 아닌지 판단할 수 없는 경우가 있다. DOM이 완전히 구성된 후에 특정 함수를 실행해야 할 때는 DOM 트리 완성 여부를 아는 게 필요하다.
이때 현재 로딩 상태를 알려주는 document.readyState
프로퍼티를 사용할 수 있다.
프로퍼티의 값은 다음과 같다.
"loading"
– 문서를 불러오는 중일 때"interactive"
– 문서가 완전히 불러와졌을 때"complete"
– 문서를 비롯한 이미지 등의 리소스들도 모두 불러와졌을 때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));
document.readyState
는 DOMContentLoaded가 실행되기 바로 직전에 interactive가 됩니다. 따라서 DOMContentLoaded
와 interactive
는 같은 상태를 나타낸다고 볼 수 있다.
document.readyState
는 iframe, img를 비롯한 리소스 전부가 로드되었을 때 complete가 됩니다. readyState의 값이 complete로 바뀐다는 것은 window.onload
가 실행된다는 것과 동일한 의미입니다. 이 둘의 차이점은 window.onload는 다른 load 핸들러가 전부 실행된 후에야 동작한다는 것에 있습니다.
모던 웹브라우저에서 돌아가는 스크립트들은 대부분 HTML보다 ‘무겁다’. 용량이 커서 다운로드받는 데 오랜 시간이 걸리고 처리하는 것 역시 마찬가지이다.
브라우저는 HTML을 읽다가 <script>...</script>
태그를 만나면 스크립트를 먼저 실행해야 하므로 DOM 생성을 멈춘다. 이는 src 속성이 있는 외부 스크립트 를 만났을 때도 마찬가지이다. 외부에서 스크립트를 다운받고 실행한 후에야 남은 페이지를 처리할 수 있다.
이런 브라우저의 동작 방식은 두 가지 중요한 이슈를 만든다.
이런 부작용들을 피할 수 있는 몇 가지 방법이 있다. 스크립트를 페이지 맨 아래 놓는 것이 하나의 방법이 될 수 있다. 이렇게 하면 스크립트 위에 있는 요소에 접근할 수 있다. 또한, 페이지 콘텐츠 출력을 막지 않게 된다.
그런데 이 방법은 완벽한 해결책이 아니다. HTML 문서 자체가 아주 큰 경우를 가정해보면, 브라우저가 HTML 문서 전체를 다운로드 한 다음에 스크립트를 다운받게 하면 페이지가 정말 느려질 것이다.
네트워크 속도가 빠른 곳에서 페이지에 접속하고 있다면 이런 지연은 눈에 잘 띄지 않는다. 하지만 아직도 네트워크 환경이 열악한 곳이 많다. 모바일 네트워크 접속이 느린 곳도 많다.
다행히도 이런 문제를 해결할 수 있는 <script>
속성이 있습니다. 바로 defer와 async이다.
브라우저는 defer 속성이 있는 스크립트(이하 defer 스크립트 또는 지연 스크립트)를 '백그라운드’에서 다운로드 한다. 따라서 지연 스크립트를 다운로드 하는 도중에도 HTML 파싱이 멈추지 않는다. 그리고 defer 스크립트 실행은 페이지 구성이 끝날 때까지 지연 된다.
지연 스크립트는 일반 스크립트와 마찬가지로 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 속성은 외부 스크립트에만 유효합니다.
<script>
에 src가 없으면 defer 속성은 무시됩니다.
async 속성이 붙은 스크립트(이하 async 스크립트 또는 비동기 스크립트)는 페이지와 완전히 독립적으로 동작한다.
이런 특징 때문에 페이지에 async 스크립트가 여러 개 있는 경우, 그 실행 순서가 제각각이 된다. 실행은 다운로드가 끝난 스크립트 순으로 진행된다.
비동기 스크립트는 방문자 수 카운터나 광고 관련 스크립트처럼 각각 독립적인 역할을 하는 서드 파티 스크립트를 현재 개발 중인 스크립트에 통합하려 할 때 아주 유용하다. async 스크립트는 개발 중인 스크립트에 의존하지 않고, 그 반대도 마찬가지이기 때문이다.
<!-- Google Analytics는 일반적으로 다음과 같이 삽입합니다. -->
<script async src="https://google-analytics.com/analytics.js"></script>
자바스크립트를 사용하면 문서에 스크립트를 동적으로 추가할 수 있습니다. 이렇게 추가한 스크립트를 동적 스크립트(dynamic script)라고 부른다.
let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script); // (*)
위 예시에서 외부 스크립트는 관련 요소가 문서에 추가되자 마자((*
)로 표시한 줄) 다운로드가 시작된다.
그런데 동적 스크립트는 기본적으로 ‘async’ 스크립트처럼 행동한다.
그러므로 다음과 같은 특징을 지닌다.
‘load-first’ order
).동적 스크립트가 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를 적절히 사용하면 사용자가 오래 기다리지 않고 페이지 콘텐츠를 볼 수 있게 할 수 있다.
두 스크립트의 차이점은 다음과 같다.
async
defer
스크립트 다운로드가 끝나지 않았어도 페이지는 동작해야 한다.
defer를 사용하게 되면 스크립트가 실행되기 전 에 페이지가 화면에 출력된다는 점에 항상 유의해야 한다. 사용자는 그래픽 관련 컴포넌트들이 준비되지 않은 상태에서 화면을 보게 될 수 있다.
따라서 지연 스크립트가 영향을 주는 영역엔 반드시 '로딩 인디케이터’가 있어야 한다. 관련 버튼도 사용 불가(disabled) 처리를 해줘야 한다. 이렇게 해야 사용자에게 현재 어떤 것은 사용할 수 있는지, 어떤 것은 사용할 수 없는지를 알려줄 수 있다.
실무에선 defer를 DOM 전체가 필요한 스크립트나 실행 순서가 중요한 경우에 적용한다. async는 방문자 수 카운터나 광고 관련 스크립트같이 독립적인 스크립트에 혹은 실행 순서가 중요하지 않은 경우에 적용한다.
defer나 async를 붙이지 않은 일반적인 <script>
태그의 경우 아래와 같은 프로세스를 거친다.
<script>
브라우저가 HTML 파일을 파싱하다가, 멈추고 스크립트를 다운로드한다. 그리고 실행까지 끝낸 뒤에야 다시 HTML 파싱을 재개한다.
<script async>
async 키워드를 붙일 경우 아래와 같다. HTML 문서를 파싱하는 동시에 백그라운드에서 스크립트를 다운로드한다. 다운로드가 끝나면 실행하는 동안은 HTML 파싱을 멈춘다.
<script defer>
defer 키워드를 사용하면 아래와 같다. 마찬가지로 HTML 문서를 파싱하는 동안 백그라운드에서 스크립트를 다운로드한다. 다만 파싱이 끝난 뒤에 실행한다. 정확히는 태그를 만났을 때 실행한다. 그리고 DOMContentLoaded 이벤트 발생 전에 실행된다.
async vs defer attributes
Async & Defer — How to Load JavaScript Properly