기본 콘텐츠로 건너뛰기

script async와 defer 차이, 실행 시점과 선택 기준까지 한 번에 정리

script async와 defer 차이, 실행 시점과 선택 기준까지 한 번에 정리

빠른 답

  • async는 다운로드가 끝나는 즉시 실행되어 HTML 파싱을 끊을 수 있습니다.
  • defer는 문서 파싱이 끝난 뒤 선언 순서대로 실행되어 앱 초기화 코드에 안전합니다.
  • 의존성이 없고 독립적으로 동작하는 외부 스크립트는 async가 잘 맞습니다.
  • 실행 순서와 DOM 준비 시점이 중요하면 defer를 먼저 검토하는 것이 실무적으로 안전합니다.

한눈에 비교

Point 1
다운로드: async 와 defer 모두 HTML 파싱과 병렬로 다운로드를 시작할 수 있습니다.
Point 2
실행 시점: async 는 다운로드가 끝나는 즉시 실행되고, defer 는 문서 파싱이 끝난 뒤 실행됩니다.
Point 3
실행 순서: 여러 async 스크립트는 먼저 받아진 순서대로 실행되고, 여러 defer 스크립트는 문서에 적은 순서대로 실행됩니다.
Point 4
DOM 접근 안정성: async 실행 시점에는 DOM이 아직 덜 만들어졌을 수 있지만, defer 는 문서 파싱 후라 DOM 접근이 훨씬 예측 가능합니다.
Point 5
DOMContentLoaded 관계: defer 와 기본 module 스크립트는 DOMContentLoaded 전에 실행되며, 이 이벤트는 그 실행을 기다립니다. async 는 보통 그 흐름 바깥에서 실행됩니다.

왜 둘 다 비동기인데 체감은 다를까

헷갈리는 이유는 둘 다 “비동기로 로드된다”는 문장만 기억하기 쉽기 때문입니다. 하지만 실제 성능과 렌더링에 더 큰 영향을 주는 것은 다운로드 자체보다 스크립트가 언제 실행되느냐입니다.

브라우저는 HTML을 읽으면서 DOM을 만들고, CSS를 읽으면서 CSSOM을 만들고, 둘을 합쳐 렌더 트리를 구성한 뒤 레이아웃, 페인트, 컴포지팅으로 화면을 그립니다. 이 흐름 도중 자바스크립트가 실행되면 DOM을 바꾸거나 스타일을 읽고 바꿀 수 있으므로, 브라우저는 그 시점에 파서와 렌더링 작업을 조심스럽게 조정합니다.

핵심은 이것입니다.

  • asyncdefer는 “자바스크립트를 백그라운드에서 실행”하는 기능이 아닙니다.
  • 두 속성은 “브라우저가 외부 스크립트를 언제 실행할지”를 조정하는 로딩 힌트입니다.
  • 실행 자체는 여전히 메인 스레드에서 일어나므로, 실행이 길면 스타일 계산, 레이아웃, 페인트도 뒤로 밀릴 수 있습니다.

즉, asyncdefer는 단순한 문법 차이가 아니라 브라우저 렌더링 파이프라인에 개입하는 방식의 차이입니다.

흐름으로 보기

1
HTML 파싱
2
DOM 생성
3
CSSOM 생성
4
렌더 트리 구성
5
레이아웃 계산
6
페인트와 컴포지팅

이 단계는 완전히 분리되어 순차적으로 끝나는 것이 아니라, 가능한 범위 안에서 겹쳐 진행됩니다. 그래서 스크립트가 어디서 실행되느냐에 따라 첫 화면이 얼마나 빨리 안정적으로 그려지는지가 달라집니다.

예를 들어 async 스크립트가 파싱 중간에 실행되면 DOM 생성 흐름이 잠깐 멈출 수 있습니다. 반대로 defer는 파싱을 먼저 끝내고 실행하므로, 브라우저가 DOM 구조를 더 안정적으로 만든 뒤 초기화 코드를 처리할 수 있습니다. 초기 렌더링의 예측 가능성은 이 차이에서 나옵니다.

HTML 파싱과 렌더링 사이에서 스크립트가 끼어드는 지점

가장 먼저 비교해야 할 대상은 asyncdefer만이 아니라, 아무 속성도 없는 기본 스크립트입니다.

<head>
  <link rel="stylesheet" href="/assets/app.css">

  <script src="/blocking.js"></script>
  <script async src="/analytics.js"></script>
  <script defer src="/app.js"></script>
</head>

이 코드에서 브라우저는 대략 이렇게 동작합니다.

  • blocking.js: 파서가 이 지점에서 멈춥니다. 파일을 받아 실행한 뒤에야 HTML 파싱이 다시 진행됩니다.
  • analytics.js: 다운로드는 병렬로 하되, 준비되는 즉시 실행합니다. 실행 순간에는 파싱이 잠깐 멈출 수 있습니다.
  • app.js: 다운로드는 미리 해두고, 문서 파싱이 끝난 뒤 실행합니다.

렌더링 관점에서 보면 차이는 더 분명합니다. DOM과 CSSOM이 만들어져야 렌더 트리를 구성할 수 있고, 그 다음에 레이아웃과 페인트가 이어집니다. 중간에 스크립트가 DOM을 수정하거나 스타일 계산을 유발하면 브라우저는 스타일 재계산과 레이아웃을 다시 해야 할 수 있습니다. 특히 async가 파싱 도중 끼어들어 무거운 작업을 하면, 첫 페인트가 늦어지거나 화면이 덜 안정적인 상태에서 다시 그려질 수 있습니다.

또 하나 놓치기 쉬운 점은 CSS입니다. 스크립트가 스타일 정보를 읽을 수 있기 때문에, 이미 만난 스타일시트가 아직 로드 중이면 스크립트 실행이 그 CSS를 기다리는 경우가 있습니다. 그래서 “JS는 비동기 로드인데 왜 DOMContentLoaded가 늦지?” 같은 상황이 생깁니다. 병목이 자바스크립트 파일이 아니라 느린 CSS일 수도 있습니다.

async가 맞는 경우와 defer가 맞는 경우

실무 판단 기준은 “독립성”과 “순서 보장 필요 여부”입니다.

async가 맞는 경우는 다음에 가깝습니다.

  • 다른 스크립트와 실행 순서 의존성이 거의 없다.
  • DOM이 아직 완성되지 않아도 문제없다.
  • 앱 초기화 흐름과 분리돼 있다.
  • 늦게 실행되거나 먼저 실행돼도 기능적으로 큰 문제가 없다.

대표 예시는 방문 통계, 분석 태그, 일부 외부 위젯, 광고 스크립트입니다. 다만 여기서도 주의할 점이 있습니다. 다운로드가 빨라도 실행 자체가 무거우면 메인 스레드를 오래 점유해 렌더링을 방해할 수 있습니다. async는 “언제든 튀어나와 실행될 수 있다”는 성질 때문에 오히려 성능 분석 시 원인 파악을 어렵게 만들기도 합니다.

defer가 맞는 경우는 더 넓습니다.

  • 앱 번들 초기화
  • DOM 조회와 이벤트 바인딩
  • 여러 파일 간 실행 순서 보장
  • 라우팅, 하이드레이션, 마운트 코드
  • DOMContentLoaded 전에 준비돼야 하는 메인 로직

대부분의 웹 애플리케이션 엔트리 스크립트는 defer가 더 안전합니다. 이유는 단순히 “느리지 않아서”가 아니라, 파싱 완료 후 순서대로 실행된다는 보장이 있기 때문입니다. 실무에서 자주 하는 실수는 “비동기니까 더 빠르겠지”라고 생각하고 메인 번들에 async를 붙이는 것입니다. 이 경우 DOM 준비 상태와 초기화 순서가 흔들리면서 간헐적인 오류가 생기기 쉽습니다.

설정 예시로 보는 배치 전략

하나의 페이지에는 성격이 다른 스크립트가 함께 들어갑니다. 그래서 전부 async나 전부 defer로 몰아가는 것보다 역할별로 나누는 편이 낫습니다.

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="/assets/app.css">

  <script async src="https://www.googletagmanager.com/gtag/js?id=G-1234567890"></script>
  <script>
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag("js", new Date());
    gtag("config", "G-1234567890");
  </script>

  <script async src="https://example-ad-network.com/ad.js"></script>

  <script defer src="/assets/runtime.js"></script>
  <script defer src="/assets/main.js"></script>
</head>

이 배치에서 분석 태그와 광고 스크립트는 독립성이 높아 async가 자연스럽습니다. 반면 runtime.jsmain.js는 실행 순서가 중요하고, 보통 DOM 마운트나 앱 초기화에 관여하므로 defer가 더 적절합니다.

예전 글에서는 스크립트를 body 맨 아래에 두라고 많이 설명했지만, 지금은 head에서 defer를 쓰는 방식이 더 실용적인 경우가 많습니다. 브라우저가 리소스를 더 빨리 발견해 다운로드를 먼저 시작할 수 있기 때문입니다. 파서를 막지 않으면서도 네트워크 시작 시점을 당길 수 있다는 점이 중요합니다.

콘솔과 DevTools로 실행 순서 확인하기

속성 차이는 문장으로 읽는 것보다 실제 로그를 보면 바로 이해됩니다. 아래처럼 아주 작은 재현 페이지를 만들면 됩니다.

<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">

  <script>
    console.log("[inline] parser start", document.readyState);
  </script>

  <script async src="/async.js"></script>
  <script defer src="/defer-a.js"></script>
  <script defer src="/defer-b.js"></script>

  <script>
    document.addEventListener("DOMContentLoaded", () => {
      console.log("[event] DOMContentLoaded", document.readyState);
    });

    window.addEventListener("load", () => {
      console.log("[event] load", document.readyState);
    });
  </script>
</head>
<body>
  <div id="app">hello</div>
</body>
</html>

각 파일 내용은 짧게 유지해도 충분합니다.

// async.js
console.log("[async.js] executed", document.readyState);

// defer-a.js
console.log("[defer-a.js] executed", document.readyState);

// defer-b.js
console.log("[defer-b.js] executed", document.readyState);

예를 들어 네트워크 상황상 async.js가 먼저 도착했다면 콘솔과 성능 기록은 다음처럼 보일 수 있습니다.

[Console]
[inline] parser start loading
[async.js] executed loading
[defer-a.js] executed interactive
[defer-b.js] executed interactive
[event] DOMContentLoaded interactive
[event] load complete

[Performance summary]
15.2 ms   Parse HTML
31.7 ms   Evaluate Script async.js
32.5 ms   Parse HTML resumed
73.4 ms   Evaluate Script defer-a.js
74.1 ms   Evaluate Script defer-b.js
74.8 ms   DOMContentLoaded
96.3 ms   Load

이 로그에서 읽어야 할 포인트는 명확합니다.

  • loading 상태에서 async.js가 실행됐다는 것은 파싱 중간에 끼어들었다는 뜻입니다.
  • interactive 상태에서 defer-a.js, defer-b.js가 순서대로 실행됐다는 것은 문서 파싱이 끝난 뒤 실행됐다는 뜻입니다.
  • DOMContentLoadeddefer 실행 이후에 찍히므로, 메인 초기화 로직이 이 이벤트 전에 끝나야 한다면 defer가 잘 맞습니다.

DevTools에서는 NetworkPerformance를 같이 보는 것이 좋습니다.

  • Network: 각 JS 파일의 응답 완료 시점, DOMContentLoaded, Load 마커를 함께 확인합니다.
  • Performance: Evaluate Script, Recalculate Style, Layout, Paint가 어떤 순서로 이어지는지 봅니다.
  • Long Taskasync 스크립트 직후 나타난다면, 병목은 다운로드보다 실행 비용일 가능성이 큽니다.

즉, “언제 내려받았는가”보다 “언제 메인 스레드를 점유했는가”를 봐야 진짜 원인이 보입니다.

자주 놓치는 예외: inline script와 module script

많은 설명이 외부의 고전적 스크립트만 기준으로 끝납니다. 하지만 실제 페이지에서는 인라인 스크립트와 모듈 스크립트까지 함께 봐야 합니다.

<script defer>
  console.log("inline script");
</script>

<script type="module" src="/main.js"></script>
<script type="module" async src="/widget.js"></script>

여기서 중요한 예외는 두 가지입니다.

첫째, 인라인 스크립트에는 asyncdefer가 사실상 의미가 없습니다. 외부 파일 다운로드 과정이 없기 때문에 파서가 해당 지점을 만나는 즉시 실행됩니다. 즉, 위의 첫 번째 예시는 defer라고 적혀 있어도 실제로는 지연 실행되지 않습니다.

둘째, type="module" 스크립트는 기본적으로 defer처럼 동작합니다. 문서 파싱이 끝난 뒤 실행되고, DOMContentLoaded도 그 실행을 기다립니다. 반면 type="module" async는 준비되는 즉시 실행될 수 있어 순서 보장이 약해집니다.

그래서 요즘 프런트엔드 앱에서 엔트리 파일이 모듈 스크립트라면, “스크립트는 무조건 body 끝에 둔다” 같은 오래된 규칙만으로는 동작을 정확히 설명할 수 없습니다. 현재 기준에서는 “외부 고전 스크립트인지, 모듈 스크립트인지, 실행 순서 보장이 필요한지”까지 함께 봐야 합니다.

관련 레퍼런스는 아래 문서가 가장 안정적입니다.

  • MDN script: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script
  • HTML Standard script element: https://html.spec.whatwg.org/multipage/scripting.html#the-script-element

선택 기준을 한 줄로 정리하면

실무에서 가장 덜 틀리는 기준은 이것입니다.

  • 아무 순서로 실행돼도 괜찮고 DOM 준비 상태도 중요하지 않으면 async
  • 앱 초기화 코드이거나 실행 순서, DOM 준비가 중요하면 defer

그리고 성능 문제를 볼 때는 “비동기 로드냐 아니냐”만 보지 말고, 실제로 어떤 시점에 Evaluate Script, 스타일 재계산, 레이아웃, 페인트가 발생했는지를 DevTools에서 함께 확인해야 합니다. 브라우저는 스크립트 다운로드보다 스크립트 실행 비용에서 더 자주 느려집니다.

원문 참고

https://www.maeil-mail.kr/question/37

댓글

이 블로그의 인기 게시물

아이콘 폰트 (icomoon 사용법)

 장난감 프로젝트를 만들다 보면, 아이콘이 필요한 경우가 있다. 간단하게 아이콘을 인터넷에서 검색하여, 이미지로 넣어두고 이미지 태그를 이용하여, 사용하는 경우가 일반적이였지만...  요즘에는 대부분 폰트를 이용하여 아이콘을 노출 한다. 나 같은 경우에도 기본적으로  https://material.io/resources/icons 를 참고하여 아이콘 폰트를 이용할 수 있도록 처리하고, 추가적으로 필요한 아이콘이고, 일상적으로 사용 되지 않는 아이콘의 경우에는  https://icomoon.io 에서 제작하여, 아이콘 폰트로 이용 하곤 한다.  그래서 이번에는 아이콘  https://icomoon.io 의 사용법을 간단히 공유하고자 한다.   들어가자 마자 위의 icoMoonApp버튼을 누르면 아래와 같은 화면이 나타난다.  icomoon에서 무료로 제공하는 아이콘들이 보이면 위에 파란색으로 표시 되어있는 집 모양 세가지를 선택한 후, 아래의 빨간색으로 표시되어있는 Generate Font를 눌러보자.  그리고 나서 바로 다운로드를 요청해보자. icomoon.zip이 다운로드가 될텐데, 압축을 해제해 보면, 아래의 폴더 및 파일들이 있다. 아래에서 중요한 것은 font 폴더와 style.css이다. demo-files fonts demo.html Read Me.txt selection.json style.css <!doctype html > <html> <head> <link rel ="stylesheet" href ="style.css" ></head> </head> <body> <span class ="icon-home" ></span> <span class ="icon-home2" ></span> <span class ="icon-hom...

Chart js와 amchart 비교

Chart js 특징은 위의 그림으로 대체 할 수 있을 듯 하다. 오픈 소스이고, 기본으로 제공하는 차트 종류가 8가지 Canv a s를 이용해서 차트를 그리고, 반응형을 지원한다. amchart amchart는 기본적으로 유료이며, 기본으로 제공하는 차트 종류가 기본적인 차트 + 주식 처럼 보이는 차트 + 지도에 관련된 차트(?) 까지 하면, 기본 제공 하는 종류가 20개 내외 이려나, 일일이 세기에는 양이 좀 많아 보인다. 렌더링은 svg를 통하여 그려지고, 당연 반응형도 지원이 된다. 그러면, 이 둘중에 어떤것이 내 프로젝트에 적합 하냐는 것이 문제이다. 일단, 주식 처럼 보이는 차트나 지도에 관련된 차트(?)가 필요하면, amchart를 선택해야 되는 것은 맞다. 그건 당연한 것이니 빼고 얘기 해보자! 여러 종류의 차트가 필요하다면, 일단은 amchart를 염두해 두는 것이 좋다. 돈 낸 만큼은 하는 듯 하다. 하지만, 기본적인 막대 그래프, 도넛 차트 등, 아주 기본적인 차트들인데, Chart js도 amchart도 그러한 차트가 없을 때가 문제가 된다. 그렇다면, 조금이라도 커스텀이 용이한 것을 찾는 것이 좋을 것이다.  일단 amchart에서 custom이라고 검색 하였을 때, 검색 결과가 61가지가 나온다. 차트의 종류도 많고, 각 차트마다 들어가는 속성이 매우 많기 때문에, 웬만한 내용들은 속성 값을 어떻게 주느냐에 따라서 변경이 가능 하게 된다. 커스텀의 예를 들면, 기본적으로 도넛 파이의 형태를 띄면서, 화살표로 목표를 표시해주는 차트가 필요하다고 생각 해보자. 이것은 amchart로 만든 그래프이고 이것은 chart js로 만든 그래프이다. 모양이 살짝 다르긴 하지만, 완벽하게 똑같이 구현 할 수도 있다. amchart로 만든 그래프의 경우, 저것은 도넛그래프가 아닌 guage 그래프이다. 원래 게이지 그래프는 이와 같...

javascript 압축 파일 다운로드

이번에는 전 게시글의 응용판? 이라고 해야하나....? 어쨋든! 우리는 각각의 파일들을 다운로드 해보았다. 그런데 생각보다 귀찮음?을 느꼇을 것이다. 파일을 각각 다운 받아야 한다는 현실때문에! 그래 파일 두개야 뭐 그렇다 치지... 하지만, 개발자도 사용자도 게으름뱅이이다. 자 결국, 우리가 해야 하는 것은 파일을 한 번에 둘다! 다운 받는 것이다. 물론, 클릭 한번에 여러개의 함수를 엮어서 다운받게 하면 되지만! 크롬에서 자주 봤듯이, 여러개의 파일을 다운로드를 시도하면 <- 여러개의 파일을 다운로드 합니다. 허용 합니까? 하고 물어보는 것을 볼 수 있다. 게다가 다운로드 한 파일들을 찾기도 귀찮다는 것. 자 해결책을 제시해보자면, https://github.com/Stuk/jszip 클라이언트 단에서 파일을 zip파일로 압축을 할 수가 있다! 필요한 작업은 아래와 같다. 0. 데이터 준비 1. BLOB(binary large object)를 만든다. 2. Blob을 URL.createObjectURL을 사용하여, 해당 binary의 주소를 생성. 3. 다운로드가 필요한 파일들을 Zip 객체에 셋팅! 4. a태그를 이용하여, 해당 url 셋팅 하고, 다운로드. 전 게시물과 별로 달라진게 없네... 자 그럼 샘플! 샘플을 보자! http://embed.plnkr.co/NMprnRxqYG0fkHa2J55D/ var util = {} function fixBinary(bin) { //binary to arrayBuffer var length = bin.length var buf = new ArrayBuffer(length) var arr = new Uint8Array(buf) for (var i = 0; i < length; i++) { arr[i] = bin.charCodeAt(i) } return buf } window.onload = function() { ...