📡
스크립트의 실행 시점을 조절하는 Async와 Defer 속성

로드한 스크립트의 실행 시점을 조절할 수 있게 만들어주는 Async와 Defer 속성에 대해 알아봅니다.

March 01, 2021


JavaScript


<script src="./script.js"></script>
<script src="./script.js" async></script>
<script src="./script.js" defer></script>

동적인 웹 어플리케이션을 만들기 위해서는 JavaScript 파일을 불러오는 것이 필수적입니다. 하지만 복잡한 비즈니스 로직이 포함된 JavaScript 파일이라면 그 용량이 매우 클 것입니다. 따라서 스크립트 파일을 비동기 방식으로 불러오는 방식을 통해 로드 시간을 줄일 수 있습니다.

오늘은 이를 가능하게 만들어주는 <script> 태그의 속성 asyncdefer 에 대해 알아보고, 두 속성 사이의 차이에 대해 정리해보고자 합니다. 이번 포스트를 통해 브라우저에서 스크립트를 비동기로 불러오는 방법에 대해 궁금한 프론트엔드 개발자 분들께 도움이 되기를 바랍니다.

세 줄 요약

  • DOM을 따라 반드시 순서대로 실행되어야 한다면 <script>
  • DOM이나 다른 스크립트에 의존성이 없고, 실행 순서가 중요하지 않은 경우라면 <script async>
  • DOM이나 다른 스크립트에 의존성이 있고, 실행 순서가 중요한 경우라면 <script defer>

스크립트를 비동기 방식으로 불러와야 하는 이유

히히 못가 히히 못가

일반적으로 브라우저는 HTML 파일을 읽어온 후, 위에서부터 아래로 한 줄씩 해석을 시작합니다. 그러다가 중간에 스크립트 파일을 마주하는 경우에는, 해당 파일을 모두 해석하기 전까지 나머지 HTML 렌더를 일시적으로 멈춥니다.

이로 인해서 몇 가지 문제가 발생하곤 합니다.

<script>
  // `null` 의 `innerHTML` 에 접근할 수 없으므로 에러가 발생합니다.
  console.log(document.getElementById("hello").innerHTML);
</script>

<div id="hello">안녕하세요</div>

<script>
  // `안녕하세요` 가 출력됩니다.
  console.log(document.getElementById("hello").innerHTML);
</script>

첫 번째로 같은 코드임에도 불구하고 위치에 따라 스크립트의 동작 여부가 달라질 수 있습니다. 첫 번째 스크립트와 두 번째 스크립트는 같은 코드이지만 두 번째 코드만 정상적으로 동작합니다. 이는 동기적인 해석 방식 때문에, 첫 번째 스크립트는 DOM에 <div> 엘리먼트가 부착되기도 전에 접근하려 했기 때문입니다. 이처럼 DOM의 특정 엘리먼트와 인터렉션 하는 비즈니스 로직이 있다면 문제가 될 수 있습니다.

<script src="jquery.js">
  // 대충 다운로드 받는 데 10초가 걸리는 JavaScript 파일
</script>

<!-- 아래의 엘리먼트는 JavaScript 파일을 모두 로드한 후에 렌더됩니다  -->
<div id="hello">안녕하세요</div>

두 번째로 스크립트 파일을 읽는 도중에는 DOM 파싱이 멈추기 때문에, 용량이 큰 스크립트 파일을 불러올 때는 스크립트 아래의 HTML 문서가 제대로 보이지 않게 됩니다. 사용자에게는 마치 웹페이지가 멈춘 것처럼 보일 수도 있기 때문에, 부정적인 경험을 끼칠 수 있죠.

해결책…?

script 일반적인 스크립트의 로드 순서

일반적인 스크립트의 로드 순서는 위와 같습니다. 예전에는 이러한 부작용을 피하고자 웹 개발자들은 HTML의 <body> 태그가 끝나기 직전에 스크립트 태그를 몰아서 넣곤 했습니다.

<html>
  <head>
    <!-- ... -->
  </head>
  <body>
    <!-- ... -->
    <script src="jquery.js"></script>
    <script src="lodash.js"></script>
  </body>
</html>

즉 렌더해야 할 DOM을 일단 불러온 후, 마지막에 스크립트 파일들을 읽어오는 방식이죠. Bootstrap 같은 라이브러리들의 문서를 보더라도 <body> 태그가 끝나는 부분에 <script>를 넣으라고 제안하고 있습니다.

하지만 이 방법 역시 완벽한 해결책은 아닙니다. 만약 스크립트를 해석하는 도중에 사용자가 버튼을 클릭하거나 텍스트를 입력하는 것처럼 웹과 상호작용을 시도하게 된다면, 제대로 동작하지 않을 것입니다. 사용자는 분명히 완성된 것처럼 보이는 웹사이트에 접속해 있지만 실제로는 백그라운드에서 JavaScript를 해석해서 실행하는 중이기 때문입니다. 특히 모바일 디바이스처럼 인터넷 연결이 원활하지 않은 환경에서 이런 불편이 자주 발생할 수 있습니다.

이 때문에 브라우저가 스크립트 파일을 병렬로 불러오는 방식으로 DOM 렌더 과정을 막지 않게 선언할 수 있습니다. 그리고 이를 가능하게 하는 키워드가 바로 asyncdefer 입니다.

async

async async 스크립트의 로드 순서

async 스크립트는 DOM 렌더 과정을 방해하지 않도록 병렬로 로드합니다.

<script src="analytics.js" async></script>

이는 브라우저가 DOM을 구성하는 동시에 백그라운드에서 스크립트를 불러올 수 있음을 의미합니다. 즉 async 속성 적용하면 스크립트를 불러오는 과정에서 DOM 렌더를 차단하지 않도록 보장합니다.

하지만 async 스크립트는 오직 파일을 불러오는 것만 병렬로 실행한다는 것이 중요합니다. 파일의 로딩을 마치게 된다면, 그 즉시 DOM 렌더를 멈추고 async 방식으로 불러온 스크립트 파일의 해석을 시작합니다. 때문에 async 속성으로 파일을 불러온다고 해도, 스크립트의 해석이 얼마나 오래 걸릴지는 스크립트의 파일의 오버헤드에 달려 있습니다. 따라서 DOM에 접근하는 스크립트를 async 방식으로 불러오는 것은 권장되지 않습니다.

이러한 특성 때문에 async 스크립트는 실행 순서가 보장되지 않습니다.

<!-- large.js 는 로드되는데 5초가 걸립니다 -->
<script src="large.js" async></script>

<!-- small.js 는 로드되는데 1초가 걸립니다 -->
<script src="small.js" async></script>

위의 예시처럼 불러오는데 서로 다른 시간이 걸리는 async 스크립트가 있다면, 먼저 로드가 되는 스크립트가 먼저 실행됩니다. 스크립트의 실행 순서를 조정할 수 없기 때문에, 만약 두 스크립트가 서로 의존성이 있다면 제대로 동작하지 않을 수 있습니다.

또한 async 스크립트는 완전히 비동기로 불러오기 때문에, DOM이 모두 로드된 경우 발생하는 DOMContentLoaded 이벤트 콜백으로 로드를 보장할 수 없습니다. DOM 구성이 끝나기 전에 로드가 완료되었다면 DOMContentLoaded 에서 확인할 수 있지만, 그렇지 않다면 확인할 수 없죠.

때문에 async 스크립트는 DOM에 직접 접근하지 않거나, 다른 스크립트에 의존적이지 않은 스크립트들을 독립적으로 실행해야 할 때 효과적입니다.

<!-- Google Analytics 같은 서드파티 스크립트는 기존의 어플리케이션과 완전히 독립적으로 동작하므로 async가 어울립니다 -->
<script async src="https://google-analytics.com/analytics.js"></script>

defer

defer defer 스크립트의 로드 순서

defer 스크립트는 역시 async 와 비슷하게 동작합니다. defer 스크립트 역시 DOM 렌더를 방해하지 않고 병렬로 로드합니다. 하지만 로드가 완료된 후 즉시 그 내용이 실행되는 async 스크립트와는 다르게, defer 스크립트는 모든 DOM이 로드된 후에야 실행됩니다.

<script src="jquery.js" defer></script>

또한 defer 스크립트는 선언한대로 실행 순서가 보장됩니다.

<!-- large.js 는 로드되는데 5초가 걸립니다 -->
<script src="large.js" defer></script>

<!-- small.js 는 로드되는데 1초가 걸립니다 -->
<script src="small.js" defer></script>

실제로 더 빨리 로드되는 스크립트가 있다고 하더라도, 실행은 항상 선언한 순서대로 실행됩니다. 물론 스크립트 파일을 제외한 DOM 구성이 끝난 이후에 말이죠.

또한 defer 스크립트는 단순히 먼저 로드한 스크립트라 하더라도 실행하는 시점을 지연시키는 것이기 때문에, DOMContentLoaded 이벤트가 발생되기 전에 이미 실행된 상태입니다.

이 때문에 기본적으로 DOM의 모든 엘리먼트에 접근할 수 있고, 실행 순서도 보장하기 때문에 가장 범용적으로 사용할 수 있는 속성입니다. 또한 스크립트 파일끼리의 의존성이 있는 경우에도 정답이 될 수 있습니다.

참고 자료