defer와 async로 최적화된 스크립트 로딩

기본 스크립트 태그의 한계

HTML 문서를 파싱하던 도중 <script> 태그를 만나면 브라우저는

  • 해당 스크립트를 다운로드하고
  • 다운로드가 완료된 후 스크립트를 실행한 다음
  • 다시 파싱을 이어갑니다.

이 과정에서 스크립트가 크거나 다운로드 속도가 느리다면, 문서 파싱과 화면 출력이 지연되는 문제가 발생합니다.

defer: DOM 완료 후 순차 실행

defer 속성이 붙은 스크립트는 백그라운드에서 다운로드되는 동안 HTML 파싱을 계속 진행합니다. 스크립트 실행은 DOM 파싱이 끝난 후(DOMContentLoaded 직전), 문서 내 순서대로 진행됩니다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>defer로 최적화</title>
  <style>
    #welcome { font-size: 24px; color: #0277bd; }
  </style>
</head>
<body>
  <div id="welcome">환영합니다!</div>
  <p>콘텐츠는 스크립트 로드를 기다리지 않습니다.</p>

  <script defer src="https://cdn.example.com/init.js"></script>
  <script defer src="https://cdn.example.com/feature.js"></script>

  <script>
    document.addEventListener('DOMContentLoaded', () => {
      console.log('DOM 완성: defer 스크립트 실행 대기 중');
    });
  </script>
</body>
</html>
  • defer: init.js, feature.js가 백그라운드에서 로드
  • init.js가 먼저 실행 후 feature.js 실행

async: 다운로드 완료 즉시 실행

async 속성이 붙은 스크립트 역시 백그라운드에서 다운로드되지만, 문서 파싱과는 별개로 다운로드가 완료되는 즉시 실행됩니다.

HTML 파싱과 병행되며, 실행 순서는 다운로드 완료 순서에 따라 지기 때문에 여러 async 스크립트가 있다면 실행 순서가 보장되지 않으며, DOMContentLoaded 이벤트와 상관없이 실행될 수 있습니다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>async로 비동기 처리</title>
</head>
<body>
  <header>페이지 헤더</header>
  <main>메인 콘텐츠</main>

  <script async src="https://cdn.example.com/tracker.js"></script>
  <script async src="https://cdn.example.com/ad.js"></script>

  <script>
    console.log('인라인 스크립트 실행');
    document.addEventListener('DOMContentLoaded', () => {
      console.log('DOM 완성');
    });
  </script>
</body>
</html>
  • tracker.js, ad.js는 다운로드 완료 시점에 독립 실행
  • 두 스크립트 간 실행 순서 보장되지 않음

동적 스크립트 로딩과 제어

동적으로 추가된 <script> 요소는 기본적으로 async처럼 동작합니다. 즉, 동적 스크립트는 페이지에 삽입되는 즉시 백그라운드에서 다운로드되고, 다운로드 완료 순서대로 실행됩니다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>동적 스크립트 로딩</title>
  <style>
    #status { font-size: 18px; color: #388e3c; }
  </style>
</head>
<body>
  <div id="status">로딩 상태: 대기 중</div>
  <button onclick="loadScripts()">스크립트 로드</button>
  <script>
    const status = document.getElementById('status');

    function loadScripts() {
      status.textContent = '로딩 상태: 진행 중';

      const script1 = document.createElement('script');
      script1.src = 'https://cdn.example.com/core.js';
      script1.async = false; // 순서 보장
      script1.onload = () => console.log('core.js 로드 완료');

      const script2 = document.createElement('script');
      script2.src = 'https://cdn.example.com/ui.js';
      script2.async = false; // 순서 보장
      script2.onload = () => {
        console.log('ui.js 로드 완료');
        status.textContent = '로딩 상태: 완료';
      };

      document.body.appendChild(script1);
      document.body.appendChild(script2);
    }
  </script>
</body>
</html>

동적으로 추가한 두 스크립트가 async = false로 인해 문서에 추가된 순서대로 실행됩니다.