script async와 defer 차이, 실행 시점과 선택 기준까지 한 번에 정리
빠른 답
- async는 다운로드가 끝나는 즉시 실행되어 HTML 파싱을 끊을 수 있습니다.
- defer는 문서 파싱이 끝난 뒤 선언 순서대로 실행되어 앱 초기화 코드에 안전합니다.
- 의존성이 없고 독립적으로 동작하는 외부 스크립트는 async가 잘 맞습니다.
- 실행 순서와 DOM 준비 시점이 중요하면 defer를 먼저 검토하는 것이 실무적으로 안전합니다.
목차
한눈에 비교
왜 둘 다 비동기인데 체감은 다를까
헷갈리는 이유는 둘 다 “비동기로 로드된다”는 문장만 기억하기 쉽기 때문입니다. 하지만 실제 성능과 렌더링에 더 큰 영향을 주는 것은 다운로드 자체보다 스크립트가 언제 실행되느냐입니다.
브라우저는 HTML을 읽으면서 DOM을 만들고, CSS를 읽으면서 CSSOM을 만들고, 둘을 합쳐 렌더 트리를 구성한 뒤 레이아웃, 페인트, 컴포지팅으로 화면을 그립니다. 이 흐름 도중 자바스크립트가 실행되면 DOM을 바꾸거나 스타일을 읽고 바꿀 수 있으므로, 브라우저는 그 시점에 파서와 렌더링 작업을 조심스럽게 조정합니다.
핵심은 이것입니다.
async와defer는 “자바스크립트를 백그라운드에서 실행”하는 기능이 아닙니다.- 두 속성은 “브라우저가 외부 스크립트를 언제 실행할지”를 조정하는 로딩 힌트입니다.
- 실행 자체는 여전히 메인 스레드에서 일어나므로, 실행이 길면 스타일 계산, 레이아웃, 페인트도 뒤로 밀릴 수 있습니다.
즉, async와 defer는 단순한 문법 차이가 아니라 브라우저 렌더링 파이프라인에 개입하는 방식의 차이입니다.
흐름으로 보기
이 단계는 완전히 분리되어 순차적으로 끝나는 것이 아니라, 가능한 범위 안에서 겹쳐 진행됩니다. 그래서 스크립트가 어디서 실행되느냐에 따라 첫 화면이 얼마나 빨리 안정적으로 그려지는지가 달라집니다.
예를 들어 async 스크립트가 파싱 중간에 실행되면 DOM 생성 흐름이 잠깐 멈출 수 있습니다. 반대로 defer는 파싱을 먼저 끝내고 실행하므로, 브라우저가 DOM 구조를 더 안정적으로 만든 뒤 초기화 코드를 처리할 수 있습니다. 초기 렌더링의 예측 가능성은 이 차이에서 나옵니다.
HTML 파싱과 렌더링 사이에서 스크립트가 끼어드는 지점
가장 먼저 비교해야 할 대상은 async와 defer만이 아니라, 아무 속성도 없는 기본 스크립트입니다.
<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.js와 main.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가 순서대로 실행됐다는 것은 문서 파싱이 끝난 뒤 실행됐다는 뜻입니다.DOMContentLoaded가defer실행 이후에 찍히므로, 메인 초기화 로직이 이 이벤트 전에 끝나야 한다면defer가 잘 맞습니다.
DevTools에서는 Network와 Performance를 같이 보는 것이 좋습니다.
Network: 각 JS 파일의 응답 완료 시점,DOMContentLoaded,Load마커를 함께 확인합니다.Performance:Evaluate Script,Recalculate Style,Layout,Paint가 어떤 순서로 이어지는지 봅니다.Long Task가async스크립트 직후 나타난다면, 병목은 다운로드보다 실행 비용일 가능성이 큽니다.
즉, “언제 내려받았는가”보다 “언제 메인 스레드를 점유했는가”를 봐야 진짜 원인이 보입니다.
자주 놓치는 예외: 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>
여기서 중요한 예외는 두 가지입니다.
첫째, 인라인 스크립트에는 async와 defer가 사실상 의미가 없습니다. 외부 파일 다운로드 과정이 없기 때문에 파서가 해당 지점을 만나는 즉시 실행됩니다. 즉, 위의 첫 번째 예시는 defer라고 적혀 있어도 실제로는 지연 실행되지 않습니다.
둘째, type="module" 스크립트는 기본적으로 defer처럼 동작합니다. 문서 파싱이 끝난 뒤 실행되고, DOMContentLoaded도 그 실행을 기다립니다. 반면 type="module" async는 준비되는 즉시 실행될 수 있어 순서 보장이 약해집니다.
그래서 요즘 프런트엔드 앱에서 엔트리 파일이 모듈 스크립트라면, “스크립트는 무조건 body 끝에 둔다” 같은 오래된 규칙만으로는 동작을 정확히 설명할 수 없습니다. 현재 기준에서는 “외부 고전 스크립트인지, 모듈 스크립트인지, 실행 순서 보장이 필요한지”까지 함께 봐야 합니다.
관련 레퍼런스는 아래 문서가 가장 안정적입니다.
- MDN
script: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script - HTML Standard
scriptelement: 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
댓글
댓글 쓰기