본문 바로가기

JavaScript

defer & async in script tag

일반적으로 script태그를 아무런 속성없이 가져오게 되면 브라우저 HTML을 파싱하는 도중 멈추고 해당 스크립트를 가져오고 실행할 때까지 기다렸다가 다시 HTML을 파싱합니다. 이는 JavaScript코드가 DOM Tree에 접근하여 이를 수정할 수 있기 때문입니다.

async 와 defer 속성을 사용한다면, script태그를 만나도 HTML파싱이 중단되지 않습니다. 또한 두 속성을 동시에 적는다면 async 가 적용되고, async 를 지원하지 않는 브라우저는 defer로 fallback이 일어납니다.

 

 

script 태그의 defer속성은 페이지가 모두 로드되고 나서 해당 스크립트가 실행됨을 나타냅니다. defer 속성은 불리언(boolean) 속성으로 명시하지 않으면 false를 가지게 되고 명시하면 true를 가집니다. 이 속성은 script요소가 외부 스크립트를 참조하는 경우에만 사용할 수 있으므로,src 속성이 명시된 경우에만 사용할 수 있습니다.

 

<script src="/examples/scripts/script_src.js" defer></script>

 

모던 웹브라우저에서 돌아가는 스크립트들은 html보다 무겁고 용량이 커서 다운받는데 시간이 오래걸립니다.

브라우저는 HTML을 읽다가 script태그를 만나면 스크립트 태그를 먼저 실행해야하므로 DOM생성을 멈춥니다. 이는 src 속성이 있는 외부 스크립트를 만났을 때도 마찬가지입니다. 외부에서 스크립트를 다운받고 실행한 후에야 남은 페이지를 처리할 수 있습니다.

이방식은 두가지 중요한 이슈를 만듭니다.

  1. 스크립트에서는 스크립트 아래에 있는 DOM요소에 접근할 수 없다. 따라서 DOM요소에 핸들러를 추가하는 것과 같은 행위가 불가능하다.
  2. 페이지 위쪽에 용량이 큰 스크립트가 있는 경우 스크립트가 페이지를 막아버린다. 페이지에 접속하는 사용자들은 스크립트를 다운받고 실행될 때까지 스크립트 아래쪽 페이지를 볼 수 없다.

브라우저는 defer 속성이 있는 스크립트(지연 스크립트)를 백그라운드에서 다운로드 합니다. 따라서 지연 스크립트를 다운로드 하는 도중에도 HTML 파싱이 멈추지 않습니다. 지연 스크립트는 페이지 생성, 즉 DOM생성을 막지않고 DOM이 준비된 후에 실행되긴 하지만 DOMContentLoaded 이벤트 발생 전에 문서상의 순서대로 실행됩니다. async 와 다르게 fetch만 하고, 실제 스크립트 실행 시점은 모든 파싱이 끝난 이후에 실행된다는 것입니다.

onDOMContentLoaded 이벤트는 브라우저가 HTML을 전부 읽고 파싱하여 DOM트리를 완성하는 즉시 발생하는 이벤트이며, 이미지 파일이나 CSS등 기타 자원들이 도착하지 않았더라도 DOM Tree를 만들었다면 트리거 됩니다. 이 이벤트는 Document객체에서 발생하면 DOM 노드에 이벤트 핸들러를 부착하는 작업 등이 가능해집니다. defer를 사용하면 실제로 스크립트가 DOM Node를 변경하게 되더라도 이 변경사항이 모두 반영된 뒤에 onDOMContentLoaded 이벤트가 수행되는 것을 보장할 수 있습니다.

 

 

<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>
  1. 페이지 콘텐츠는 바로 출력
  2. DOMContentLoaded 이벤트는 지연 스크립트 실행을 기다립니다. 따라서 alert는 DOM트리가 완성되고 지연 스크립트가 실행된 후에 뜹니다.

지연 스크립트는 일반 스크립트와 마찬가지로 HTML에 추가된 순서로 실행됩니다. 따라서 길이가 긴 스크립트가 앞에 길이가 짧은 스크립트가 뒤에있어도 짧은 스크립트는 긴 스크립트가 실행될 때까지 기다립니다.

async 속성이 붙은 스크립트는 페이지와 완전히 독립적으로 동작합니다. async 스크립트는 defer 스크립트와 마찬가지로 백그라운드에서 다운로드 됩니다. 따라서 HTML 페이지는 async 스크립트 다운이 완료되길 기다리지 않고 페이지 내 콘텐츠 처리, 출력을 합니다. (하지만 async 스크립트 실행 중에는 HTML 파싱이 멈춥니다.)

DOMContentLoaded 이벤트와 async 스크립트는 서로를 기다리지 않습니다. 이런 특징 때문에 페이지에 async 스크립트가 여러개 있는 경우 실행 순서가 제각각입니다. 비동기 스크립트는 서로를 기다리지 않습니다. 따라서 로드가 먼저 된 스크립트를 먼저 실행하는 것을 load-first-order라고 부릅니다. (사용가능해지는 즉시 평가를 수행한다. 평가가 진행될 때, 즉 스크립트가 실행될 때는 HTML파싱이 중단되었다가 스크립트가 실행을 완료하면 다시 파싱을 수행한다.)

마지막으로 자바스크립트를 사용하면 문서에 동적으로 스크립트를 추가할 수 있는데, 동적스크립트는 기본적으로 async 스크립트처럼 행동합니다. 따라서 다음과 같은 특징을 갖습니다.

  • 동적 스크립트는 그 어떤 것도 기다리지 않는다. 그 어떤 것도 동적스크립트를 기다리지 않는다.
  • 먼저 다운로드된 스크립트가 먼저 실행됩니다.(load first order)

 

let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script); // (*)

 

scirpt.async = false 가 없었다면 이 스크립트들은 load-first order로 실행됩니다. 하지만 아래와 같은 경우 실행은 문서에 추가된 순서를 따릅니다.

 

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는 load-first order. 문서 내 순서 상관 없이 다운로드 순으로 실행 , defer는 문서에 추가된순 이고 문서 다운로드가 완료된 후에, DOMContentLoaded이벤트 발생전에 실행됩니다.

스크립트가 다운로드 되지 않았어도 페이지는 동작해야하는데, defer는 스크립트가 실행되기전에 DOM을 모두 그려버립니다. 사용자는 그래픽 관련 컴포넌트들이 준비되지 않은 상태에서 화면을 볼 수 있습니다. 따라서 지연스크립트가 영향을 주는 영역엔 반드시 로딩 인디케이터가 필요합니다. 관련 버튼도 사용불가 처리를 해줘야하고요. 이렇게해야 어떤 것은 사용할 수 있고 어떤것은 사용할 수 없는지 사용자에게 알려줄 수 있습니다.

실무에서는 defer를 DOM 필요한 스크립트나 실행순서가 중요한 경우에 적용한다고 합니다. async는 방문자수 카운터나 광고 관련 스크립트 같이 독립적인 스크립트에 혹은 실행 순서가 중요하지 않은 경우에 사용한다고 합니다.

module

module 속성은 async 와 defer속성과 약간 다른 특성을 지니고 있습니다. 스크립트 자체를 JavaScript 모듈로 간주합니다. module 속성은 몇가지 특징을 지닙니다.

1. src 속성이 동일한 외부스크립트는 한번만 실행된다.

<script type="module" src="index.js"></script>
<script type="module" src="index.js"></script>

2. Cross Origin 스크립트를 불러오려면 모듈이 저장되어있는 원격 서버가 Access-Control-Allow-Origin 헤더를 제공해야만 외부 모듈을 불러올 수 있다. (* 를 사용하거나 특정 도메인을 명시하는 식으로 처리 가능)

// anothersite.com 이 Access-Control-Allow-Origin 을 지원해야만 외부 모듈을 불러올 수 있다.
// 그렇지 않으면 스크립트는 실행되지 않는다.
<script type="module" src="http://another-site.com/their.js"></script>

3. 일반적으로 module은 defer처럼 동작한다. 즉 HTML이 파싱이 완료된 이후에 순차적으로 실행되는 것이다. 하지만 async 속성을 추가로 명시하면 HTML이 파싱도중에도 파싱을 중단하고 스크립트를 실행할 수 있다.

nomodule

nomodule속성은 구형 브라우저 지원을 위한 속성이다. 구형 브라우저는 type=”module” 을 해석하는 방법을 갖고있지 않기 때문에 모듈 타입의 스크립트를 만나면 이를 무시하고 넘어간다. 따라서 type=”module”을 해석하지 못하는 브라우저에 대한 대응을 처리해야한다

<script type="module">
    alert("모던 브라우저를 사용하고 계시군요");
</script>

<script nomodule>
    alert("오래된 브라우저를 사용한다면 type=module이 붙은 스크립트는 무시된다. 대신 이 alert문이 실행된다.");
</script>

 

실제로 Next.js 에서는 다음과 같이 nomodule 태그를 사용하여 module 타입을 지원하지 않는 브라우저에 대한 polyfill번들을 추가로 불러오도록 처리하고 있다.

 

 

참고.

모던 자바스크립트 튜토리얼

 

defer, async 스크립트

 

ko.javascript.info

https://yeoulcoding.tistory.com/269?category=767183

'JavaScript' 카테고리의 다른 글

[Core JavaScript] class  (0) 2022.03.04
[Modern JavaScript Deep Dive] Array  (0) 2022.03.03
[Modern JavaScript Deep Dive] Iterable  (0) 2022.03.02
[JavaScript] CustomEvent  (0) 2022.03.01
eager evaluation과 lazy evaluation  (0) 2022.02.24