자바스크립트 defer, async

Clapsheep·2025년 3월 5일
post-thumbnail

브라우저는 DOM을 구성할 때, HTML뿐만 아니라 javascript로 만든 동적인 태그들을 사용할 수 있다. 기본적으로 자바스크립트는 기존에 HTML로 만들어진 DOM을 기준으로 동적인 태그들의 위치를 선정하게 되는데, 개발을 하다보면 아직 생성되지 않은 HTML을 기준으로 잡는 경우에 화면에 원하는대로 DOM이 구성되지 않는 경험을 해보았을 것이다. 이를 막기위해 브라우저가 자바스크립트를 읽는 원리에 대해서 살펴보도록 하겠다.

기본적인 동작, Blocking

<p>...first...</p>

<script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>

<!-- 스크립트 다운로드 및 실행이 끝나기 전까지 아래 내용이 보이지 않습니다. -->
<p>...second...</p>

실행 순서

화면에 first 보임 -> 자바스크립트 실행 -> second 보임

위의 코드를 실행하면 first가 화면에 표시되고 script의 실행이 온전히 끝난 후 second가 생성되는 것을 확인할 수 있다.
이는 기본적으로 브라우저가 한 줄씩 HTML을 읽다가 script 태그를 만나면 HTML DOM 파싱을 차단(Blocking)한 후, script를 다운로드하고, 실행을 다 한 후에 다음 HTML을 읽는 방식으로 작동하기 때문이다.

이로 인해 의도하지 않게 작동이 멈추거나 아직 원하는 DOM Node가 생성되지 않아 원하는 방식으로 작동하지 않는 상황이 발생할 수 있다.
그래서 예전 자료들을 찾아보면 script 태그를 HTML의 가장 맨 마지막에 넣어 DOM 파싱을 마치고 script가 실행되도록 하는 문서들을 종종 마주할 수 있지만, 이 경우도 자바스크립트 파일이 클 경우 스크립트를 다운받는 시간이 오래 걸려 사용자의 경험이 좋지 않을 수 있다.

이를 방지하기 위해서 defer나 async 속성을 사용할 수 있다.
스크립트

왜 이렇게 설계가 되어있을까?

역사적으로 웹 초기에는 자바스크립트가 지금처럼 복잡하지 않았고, 간단한 기능만 수행했기 때문에, blocking 되더라도 사용자가 느끼지 못하던 시절이 있었다...

defer

defer
defer 옵션은 스크립트 태그를 만나면 백그라운드에서 해당 스크립트 다운로드를 진행하고 있다가 실행을 DOMContentLoaded이벤트 발생 직전에 실행하는 방식으로 작동된다.

*`DOMContentLoaded`이벤트는 브라우저의 DOM 생명주기와 관련된 메서드로 HTML을 읽고 DOM 트리를 구성이 완료되면 실행되는 이벤트입니다. (이미지, CSS, 스크립트 등의 외부 리소스 로딩은 기다리지 않습니다.)*

defer외에 DOMContentLoaded를 막지 않는 스크립트
1. async 속성이 있는 스크립트는 DOMContentLoaded를 막지 않는다.
2. document.createElement('script')로 동적으로 생성되고 웹페이지에 추가된 스크립트는 DOMContentLoaded를 막지 않는다.

<p>...first...</p>

<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>

<!-- 스크립트 다운로드 및 실행이 끝나기 전까지 아래 내용이 보이지 않습니다. -->
<p>...second...</p>

실행 순서

DOM 트리 생성 -> 스크립트 실행

위의 예제와 같은 코드로 defer 옵션만 추가했을 때, first와 second가 화면에 보이고, 스크립트가 작동하는 것을 확인할 수 있다.

<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>

만약 다운받는 스크립트가 여러개일 경우는 어떠할까?

<p>...first...</p>

<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>

<p>...second...</p>

위의 코드를 보면 long 스크립트가 short보다 먼저 선언되어있다. 하지만 Network창으로 확인해서 보면 용량이 작은 short 스크립트를 먼저 다운받고 long 스크립트를 다운받도록 브라우저가 최적화 되어있다.

그렇다고 short이 먼저 실행될까? 아니다! 실행순서는 보장 받기 때문에 long이 실행된 후 short이 실행된다.

이렇게 최적화에 의해 다운로드 순서가 달라지더라도 defer옵션은 스크립트 실행 순서를 변경하지 않는다는 특징을 가지고 있다.

async

async
async는 비동기 스크립트로써 defer스크립트와 마찬가지로 백그라운드에서 다운로드 된다. 하지만 차이점으로는 다운로드를 마치면 바로 실행되며 이 동안 HTML 파싱을 멈추게 된다. 그리고 비동기라는 이름처럼 특정 이벤트(DOMContentLoaded)나 스트립트에 영향을 주지도, 받지고 않고 독립적으로 작동한다는 특징이 있다.

이에 따라 defer와 다르게 실행순서에 따라 다운로드 되고, 실행 또한 다운로드가 끝난 스크립트 순으로 작동한다.

<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>

실행 순서

DOM 트리 생성 -> alert("DOM"이 준비되었습니다.) -> short -> long

위의 예시에서 DOM 생성이 완료되었을 때 DOMContentLoaded 이벤트가 실행되므로 가장 먼저 alert를 볼 수 있다. 그 과정에서 async 스크립트를 다운로드 할 텐데, 이는 DOM 생성을 막지 않기 때문에 콘텐츠가 바로 출력된다.
(그러나 DOMContentLoaded 이벤트는 상황에 따라 비동기 스크립트 전이나 후에 실행되므로 정확한 순서를 예측할 수 없다.)

이후 long.js가 short.js보다 앞서 다운로드를 시작하지만 short.js가 먼저 다운로드가 완료되어 실행되므로 short 실행 후 long을 실행한다.(load-first order)

비동기 스크립트(async)의 활용

이러한 이벤트나, 다른 스크립트로 부터 영향 받지 않는 특성에 따라 비동기 스크립트는 방문자 수 카운터, 광고 등 각각 독립적인 역할을 하는 서드파티 스크립트를 현재 스크립트에 불러와 사용할 때 유용하다.

동적 스크립트

스트립트 내에서 동적으로 스크립트를 부르는 경우 어떻게 동작할까? async와 동일하게 작동하는 것을 기본으로 생각하면 된다.

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

script 노드를 생성한 후 src로 자바스크립트 파일을 할당해주었다. 그 후 body 태그에 해당 노드를 동적으로 삽입하는 코드이다.

이 방식은 async와 동일하게 작동하기 때문에, 그 어떤 것도 기다리지 않고 스크립트가 다운로드 되면 바로 실행한다.

만약 실행 순서를 보장하고 싶다면 js에서 명시적으로 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: DOM 구성 완료 후 실행, 실행 순서 보장
  • async: 다운로드 완료 즉시 실행, 순서 보장 없음
  • 동적 스크립트: 기본적으로 async처럼 동작

자바스크립트의 활용이 늘어나면서 기존에 스크립트를 읽는 기본 방식에서 벗어나기 위한 옵션인 deferasync를 알아보았다. 브라우저의 작동원리를 이해하고 어떤 상황에 어떤 동작이 일어나는지 알고 있다면 개발을 진행하면서 발생하는 문제들을 빠르게 파악하는 것에 훨씬 도움이 될 것이라 생각이든다.

profile
왜 사용하는지 적어보려고 블로그를 합니다.

0개의 댓글