현대 웹 앱에서 <script> 로딩에 대한 완전 가이드: async, defer, 그리고 ES Modules
Source: Dev.to
번역할 텍스트가 제공되지 않았습니다. 번역이 필요한 전체 내용(코드 블록을 제외한 마크다운 텍스트)을 알려주시면 한국어로 번역해 드리겠습니다.
Source: …
목차
- 스크립트 로딩 전략이 중요한 이유
- 스크립트를 로드하는 네 가지 방법
- 파싱, 실행 및 이벤트가 상호 작용하는 방식
- 올바른 접근 방식 선택
- ES 모듈 심층 분석
- 성능 최적화 기법
- DOM 타이밍 패턴
- 보안 및 규정 준수
- 흔히 발생하는 함정
- 복사‑붙여넣기 템플릿
- 최종 요약
스크립트 로딩 전략이 중요한 이유
스크립트 태그는 HTML 파싱을 차단하고, 인터랙티브가 지연되며, 이벤트 타이밍을 변경할 수 있습니다. 올바른 전략을 선택하면:
- Core Web Vitals(특히 FID/INP와 LCP) 개선
- 레이스 컨디션 및 깨지기 쉬운 의존성 방지
import/export와 코드‑스플리팅을 활용한 확장 가능한 아키텍처 구현
스크립트를 로드하는 네 가지 방법
1️⃣ 클래식 (블로킹)
- 다운로드와 실행이 동시에 진행되는 동안 태그에서 HTML 파싱을 차단합니다.
- 실행 순서는 HTML 순서를 따릅니다.
- 파싱 중에 즉시 실행이 정말 필요할 때 오직 사용하세요.
2️⃣ async (클래식)
- 병렬로 다운로드하고, 준비되는 즉시 실행합니다.
- 실행 순서는 비결정적이며 (다운로드 완료 순서에 따라) 달라집니다.
- 독립적인 스크립트에 이상적입니다: 분석, 광고, 비콘 등.
3️⃣ defer (클래식)
- 병렬로 다운로드하고, HTML이 완전히 파싱된 후 그리고
DOMContentLoaded전에 실행합니다. - HTML 순서를 유지합니다.
- 모듈을 사용할 수 없을 때 앱 코드에 가장 적합한 클래식 선택입니다.
4️⃣ ES 모듈 (type="module")
- 엔트리 스크립트에 대해 기본적으로 defer와 유사하게 동작하며, 의존성 인식 로딩을 제공합니다.
- 스코프가 제한되어 전역 오염 방지, strict mode,
import/export, 동적import(), 최상위await, import map 등을 지원합니다. - 현대 애플리케이션의 기본 방식입니다.
파싱, 실행 및 이벤트 상호 작용
| 로드 유형 | 파싱 영향 | 실행 시점 | 순서 보장 |
|---|---|---|---|
| Classic (blocking) | 중단 태그에서 HTML 파싱 | 다운로드 직후 | HTML 순서 |
async | 차단하지 않음 파싱 | 스크립트 다운로드가 끝나는 즉시 | 없음 (다운로드 순서) |
defer | 차단하지 않음 파싱 | 파싱이 완료된 후, DOMContentLoaded 이전 | HTML 순서 |
| Module (entry) | 차단하지 않음 파싱 | 파싱이 완료된 후, DOMContentLoaded 이전 (defer와 유사) | 의존성 그래프 순서 |
이벤트 시점
DOMContentLoaded는 모든 defer 및 모듈 엔트리 스크립트가 실행된 후에 발생하고,DOMContentLoaded이전에 시작된async스크립트가 완료된 후에 발생합니다.load는 모든 리소스(이미지, CSS 등)가 로드된 후에 발생합니다.
올바른 접근 방식 선택
- 애플리케이션 코드 →
type="module"(기본값). - 모듈을 사용할 수 없는 경우의 클래식 스크립트 →
defer. - 독립적인 서드파티 스크립트 →
async. - 피해야 할 점 차단되는 클래식 스크립트—렌더링 및 인터랙티비티를 지연시킵니다.
ES 모듈 심층 탐구
가져오기와 내보내기
// app.js
export function init() {
console.log('App started');
}
// main.js
import { init } from './app.js';
init();
인라인 모듈
import { init } from '/js/app.js';
init();
온‑디맨드 코드를 위한 동적 import()
const btn = document.querySelector('#showChart');
btn.addEventListener('click', async () => {
const { renderChart } = await import('./chart.js');
renderChart();
});
최상위 await
// config.js
export const cfg = await fetch('/api/config')
.then(r => r.json());
// main.js
import { cfg } from './config.js';
console.log('Config:', cfg);
모듈 스코프와 엄격 모드
// my-module.js
const secret = 42; // Not on window
export const api = { hello: 'world' };
// Expose globally only if you must:
window.myApi = { doSomething() { /* … */ } };
베어 스페시파이어를 위한 Import maps
{
"imports": {
"lodash": "/vendor/lodash-es/lodash.js",
"@app/": "/static/app/"
}
}
import _ from 'lodash';
import util from '@app/util.js';
console.log(_.chunk([1,2,3,4], 2), util);
웹 워커에서 모듈
// main thread
const worker = new Worker('/js/worker.js', { type: 'module' });
// worker.js
import { heavyCompute } from './heavy.js';
self.onmessage = e => postMessage(heavyCompute(e.data));
성능 기법
리소스 힌트를 신중하게 사용하기
캐싱 및 번들링
- 프로덕션 자산에 대해 콘텐츠 해시를 사용한 장기 캐싱을 적용합니다.
- HTTP/2/3 환경에서도 번들링/청킹은 요청 오버헤드를 줄이고 캐시 적중률을 향상시킵니다.
- 동적
import()를 활용해 크고 잘 사용되지 않는 모듈을 지연 로드합니다.
코드 스플리팅 및 지연 로딩
// router.js – load route component only when needed
export async function loadDashboard() {
const { Dashboard } = await import('./pages/dashboard.js');
return new Dashboard();
}
최소화 및 압축
- JavaScript를 gzip 또는 Brotli(
br) 형식으로 제공하세요. - 최소화를 위해 esbuild, SWC, Terser와 같은 도구를 사용합니다.
중복 작업 방지
- 같은 스크립트를 클래식 방식과 모듈 방식으로 동시에 로드하지 않습니다.
- 순서가 중요한 경우 서드파티 라이브러리를 하나의 번들로 통합합니다.
DOM 타이밍 패턴
| Pattern | When to use | Example |
|---|---|---|
DOMContentLoaded listener | 전체 DOM가 필요하지만 외부 리소스는 필요 없는 코드 | document.addEventListener('DOMContentLoaded', initApp); |
load listener | 이미지, 폰트, 기타 미디어에 의존하는 코드 | window.addEventListener('load', () => { /* layout calculations */ }); |
requestIdleCallback | 메인 스레드가 유휴 상태일 때 연기해도 되는 비핵심 작업 | requestIdleCallback(() => { prefetchData(); }); |
setTimeout(..., 0) | 마이크로‑태스크 큐 플러시; 최신 async 패턴에서는 거의 필요 없음 | setTimeout(() => console.log('next tick'), 0); |
보안 및 컴플라이언스
type="module"스크립트는 자동으로 strict mode이며 module‑scope를 가집니다 (우발적인 전역 변수 방지).- 제3자 스크립트에 대해 Subresource Integrity (SRI) 를 사용하십시오:
- Content‑Security‑Policy (
script-src) 를 설정하여 신뢰할 수 있는 출처를 허용하고unsafe-inline을 금지합니다. - 필요한 경우 인라인 모듈에 대해
nonce또는 hash 기반 CSP 를 선호합니다.
Common pitfalls
- 동일 스크립트에
async와defer를 혼용 – 마지막 속성이 적용됩니다; 혼동을 피하세요. - 모듈에서 전역 변수를 의존 – 모듈은 스코프가 제한됩니다; 필요한 것만
export로 노출하세요. - 동적으로 생성된
<script>요소에type="module"을 추가하는 것을 잊음 – 기본값은 클래식 스크립트입니다. - 모듈 로딩이 시작된 후
document.write사용 – 일부 브라우저에서 모듈 로딩을 중단시킬 수 있습니다. modulepreload가 모든 브라우저에서 동작한다고 가정 – 구형 브라우저용 대체<script>를 제공하세요.
복사‑붙여넣기 템플릿
클래식 차단 (드물게 필요)
비동기 서드파티
클래식 앱 번들 지연 로드
ES 모듈 진입점
CSP 논스를 사용한 인라인 모듈
import { init } from '/js/app.js';
init();
임포트 맵 (HTML5)
{
"imports": {
"react": "/libs/react/react.production.min.js",
"react-dom": "/libs/react/react-dom.production.min.js"
}
}
최종 요약
- ES 모듈을 기본으로 사용하세요; 새로운 코드라면 모두 ES 모듈을 기본 선택하십시오. 이는 defer‑와 같은 로딩, strict 모드, 그리고 깔끔한 의존성 그래프를 제공합니다.
- **
defer**는 모듈을 지원하지 않는 환경에서 클래식 스크립트를 지원해야 할 때만 사용하십시오. - **
async**는 DOM 준비나 순서가 필요 없는 완전히 독립적인 서드파티 리소스에만 사용하십시오. - 올바른 로딩 전략을 리소스 힌트, 캐싱, 그리고 CSP/SRI와 결합하여 성능과 보안을 극대화하십시오.
적절한 스크립트 로딩 기법을 의식적으로 선택함으로써 더 빠르고, 더 안정적이며, 더 안전한 웹 경험을 제공할 수 있습니다.
실행 순서와 예시
async vs defer
모듈 및 의존성 순서
// entry.js
import './a.js'; // a.js imports b.js
console.log('entry');
// a.js
import './b.js';
console.log('a');
// b.js
console.log('b');
실행 순서: b → a → entry
DOM‑Timing 패턴
defer / 모듈을 사용한 안전한 DOM 접근
// app.js
document.querySelector('#btn').addEventListener('click', () => {
console.log('clicked');
});
DOM을 기다려야 하는 비동기 스크립트
// metrics.js
window.addEventListener('DOMContentLoaded', () => {
// Safe to query the DOM
});
보안 및 컴플라이언스
헤더, CORS, MIME
- JavaScript를 올바른 MIME 타입
application/javascript로 제공하십시오. - ES 모듈은 CORS를 따릅니다; 교차 출처 가져오기는 적절한 CORS 헤더가 필요합니다.
- 모듈에
file://URL 사용을 피하고 로컬 HTTP(S) 서버를 사용하십시오.
CSP 및 서브리소스 무결성 (SRI)
Enterprise note: 서드파티 CDN, 라이브러리 또는 분석 도구를 추가하기 전에 조직의 보안, 프라이버시 및 컴플라이언스 가이드라인에 부합하는지 확인하십시오.
일반적인 함정
브라우저 import에서 .js 확장자 누락
// ❌ Wrong
import { x } from './utils';
// ✅ Correct
import { x } from './utils.js';
Import‑map 위치
…
…
기타 주의사항
async가 순서를 보장한다는 가정. 실제로는 그렇지 않으니, 순서를 기대하며 여러 async 스크립트를 연결하지 마세요.- 널리 공유되는 모듈에서 최상위
await를 사용하면 전체 앱이 지연될 수 있습니다. 가능한 경우 지연 로딩을 선호하세요.
복사‑붙여넣기 템플릿
모듈이 포함된 최신 앱 + 레거시 폴백
Modern Module App
{
"imports": {
"@app/": "/js/"
}
}
defer가 있는 클래식
Classic Defer
Click
독립적인 서드파티 스크립트를 위한 Async
Async Example
최종 요약
- **
type="module"**을 선호하세요: 현대적인 개발을 위해 확장 가능한 아키텍처, 안전한 스코핑, 그리고 내장 코드 스플리팅을 제공합니다. - 모듈 사용이 어려운 경우 클래식 스크립트에 **
defer**를 사용하세요. - 독립적이며 순서에 구애받지 않는 스크립트에만 **
async**를 사용하세요. - 이벤트 타이밍, 캐싱, 보안(CSP, SRI, CORS)을 유념하세요.
- 앱이 성장함에 따라
modulepreload, 동적 import, import map을 활용하세요.
서드파티 CDN이나 라이브러리를 사용할 계획이라면, 해당 서비스가 조직의 내부 보안 및 컴플라이언스 요구사항을 충족하는지 확인하세요.