[JS] <script> 태그의 defer 속성
by Roel DowneyHTML <script> 태그
정의 및 특징
<script> 태그는 자바스크립트와 같은 클라이언트 사이드 스크립트(client-side scripts)를 정의할 때 사용한다.
브라우저는 HTML을 읽다가 <script>...</script> 태그를 만나면 스크립트를 먼저 실행해야 하므로 DOM 생성을 멈춘다.
이는 src 속성이 있는 외부 스크립트 <script src="..."></script>를 만났을 때도 마찬가지이다. 외부에서 스크립트를 다운받고 실행한 후에야 남은 페이지를 처리할 수 있다.
이런 브라우저의 동작 방식은 두 가지 중요한 이슈를 만든다.
- 스크립트에서는 스크립트 아래에 있는 DOM 요소에 접근할 수 없다. 따라서 DOM 요소에 핸들러를 추가하는 것과 같은 여러 행위가 불가능해진다.
- 페이지 위쪽에 용량이 큰 스크립트가 있는 경우 스크립트가 페이지를 ‘막아버린다’. 페이지에 접속하는 사용자들은 스크립트를 다운받고 실행할 때까지 스크립트 아래쪽 페이지를 볼 수 없게 된다.
이런 문제를 해결 할 수 있는 <script> 속성이 있다. defer와 async 이다.
<script> 요소는 스크립트 코드를 요소 내부에 직접 명시하거나, src 속성을 사용하여 외부 스크립트 파일을 참조할 수 있다. 하지만 src 속성이 명시된 <script> 요소에는 스크립트 코드를 직접 명시해서는 안 된다.
참조된 외부 스크립트 파일을 다음과 같이 여러 가지 방법으로 실행시킬 수 있다.
- async 속성이 명시된 경우 : 브라우저가 페이지를 파싱되는 동안 스크립트가 실행됨.
- async 속성은 명시되어 있지 않고 defer 속성만 명시된 경우 : 브라우저가 페이지의 파싱을 모두 끝내면 스크립트가 실행됨.
- async 속성과 defer 속성이 모두 명시되어 있지 않은 경우 : 브라우저가 페이지를 파싱하기 전에 스크립트를 가져와 바로 실행시킴.
이렇게 정의된 자바스크립트는 일반적으로 이미지 조정, 폼(form)에 대한 검증, 콘텐츠의 동적 변경 등에 사용된다.
또한, <noscript> 요소를 사용하여 클라이언트 사이드 스크립트(client-side scripts)를 사용하지 않도록 설정했거나, 스크립트를 지원하지 않는 브라우저를 위한 별도의 콘텐츠를 정의할 수 있다.
사용 할 수 있는 속성
async
async 속성이 붙은 스크립트(이하 async 스크립트 또는 비동기 스크립트)는 페이지와 완전히 독립적으로 동작한다.
- async 스크립트는 defer 스크립트와 마찬가지로 백그라운드에서 다운로드 된다. 따라서 HTML 페이지는 async 스크립트 다운이 완료되길 기다리지 않고 페이지 내 콘텐츠를 처리, 출력한다. (하지만 async 스크립트 실행중에는 HTML 파싱이 멈춘다 )
- DOMContentLoaded 이벤트와 async 스크립트는 서로를 기다리지 않는다.
- 페이지 구성이 끝난 후에 async 스크립트 다운로딩이 끝난 경우, DOMContentLoaded는 async 스크립트 실행 전에 발생할 수 있다,
- async 스크립트가 짧아서 페이지 구성이 끝나기 전에 다운로드 되거나 스크립트가 캐싱처리 된 경우, DOMContentLoaded는 async 스크립트 실행 후에 발생할 수도 있다.
- 다른 스크립트들은 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 이벤트는 상황에 따라 비동기 스크립트 전이나 후에 실행된다. 정확한 순서를 예측할 수 없다.
- 비동기 스크립트는 서로를 기다리지 않는다. 위치상으론 small.js가 아래이긴 하지만 long.js보다 먼저 다운로드되었기 때문에 먼저 실행된다. 이렇게 먼저 로드가 된 스크립트가 먼저 실행되는 것을 'load-first order’라고 부른다.
비동기 스크립트는 방문자 수 카운터나 광고 관련 스크립트처럼 각각 독립적인 역할을 하는 서드 파티 스크립트를 현재 개발 중인 스크립트에 통합하려 할 때 아주 유용하다.
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).
아래 예시에선 두 스크립트를 동적으로 문서에 추가한다. 그런데 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");
defer
브라우저는 defer 속성이 있는 스크립트(이하 defer 스크립트 또는 지연 스크립트)를 '백그라운드’에서 다운로드 한다. 따라서 지연 스크립트를 다운로드 하는 도중에도 HTML 파싱이 멈추지 않는다. 그리고 defer 스크립트 실행은 페이지 구성이 끝날 때까지 지연 된다.
위쪽 예시와 동일한 코드인데 스크립트에 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 다음에 실행된다.
<script>에 src가 없으면 defer 속성은 무시된다.
요약
async와 defer 스크립트는 다운로드 시 페이지 렌더링을 막지 않는다는 공통점이 있다. 따라서 async와 defer를 적절히 사용하면 사용자가 오래 기다리지 않고 페이지 콘텐츠를 볼 수 있게 할 수 있다.
두 스크립트의 차이점은 다음과 같다.
순서 | DOMContentLoaded | |
async | load-first order. 문서 내 순서와 상관없이 먼저 다운로드된 스크립트가 먼저 실행 | 비동기 스크립트는 HTML 문서가 완전히 다운로드되지 않은 상태라도 로드 및 실행될 수 있다. 스크립트 크기가 작거나 캐싱 처리 되어있을 때 혹은 HTML 문서 길이가 아주 길 때 이런 일이 발생한다. |
defer | 문서에 추가된 순 | 지연 스크립트는 문서 다운로드와 파싱이 완료된 후에, DOMContentLoaded 이벤트 발생 전에 실행된다. |
실무에선 defer를 DOM 전체가 필요한 스크립트나 실행 순서가 중요한 경우에 적용한다.
async는 방문자 수 카운터나 광고 관련 스크립트같이 독립적인 스크립트에 혹은 실행 순서가 중요하지 않은 경우에 적용한다.
'Web > JavaScript' 카테고리의 다른 글
[JS] 데이터 타입 (0) | 2021.12.29 |
---|---|
[JS] 변수 (0) | 2021.12.29 |
[JS] requestAnimationFrame , CSS3 transition 활용 (0) | 2020.05.26 |
[JS] DOM APIs 연습하기 (0) | 2020.05.26 |
[JS] 웹 애니메이션 이해와 setTimeout 활용 (0) | 2020.05.25 |
블로그의 정보
What doing?
Roel Downey