async/defer와preload/prefetch는 자주 같이 묶이지만 사실 다른 그룹이다. 앞 둘은 스크립트 실행 타이밍, 뒤 둘은 리소스 사전 다운로드를 다룬다.
1. 일반 <script>의 문제
<script src="app.js"></script>
브라우저가 HTML을 파싱하다 이걸 만나면 파싱을 멈추고 JS를 다운로드·실행한 뒤 재개한다. 즉 파싱을 막는다(parser-blocking). 스크립트가 크면 화면이 늦게 뜬다. 이걸 푸는 게 async와 defer다.
2. 타임라인으로 보기
async와 defer 모두 다운로드는 파싱과 병렬로 진행한다(파싱을 안 막음). 차이는 언제 실행하느냐다.
3. async — 받는 즉시 실행 (순서 보장 X)
<script async src="analytics.js"></script>
- 다운로드는 병렬, 다운로드가 끝나는 즉시 실행 → 이 순간만 파싱이 잠깐 멈춤
- 여러 개면 먼저 받아진 게 먼저 실행 → 순서 보장 안 됨
- 용도: 다른 코드와 독립적인 스크립트 (애널리틱스, 광고 등)
4. defer — 파싱 후 순서대로 실행
<script defer src="app.js"></script>
- 다운로드는 병렬, 실행은 HTML 파싱이 끝난 뒤 문서 순서대로
DOMContentLoaded이전에 실행됨- 용도: DOM을 다루거나 순서·의존성이 있는 일반 스크립트 → 대부분 defer가 안전
5. 비교
| 다운로드 | 실행 시점 | 순서 | |
|---|---|---|---|
| 일반 | 파싱 멈춤 | 즉시 (블로킹) | 순서대로 |
async | 병렬 | 받는 즉시 | ❌ 보장 안 됨 |
defer | 병렬 | 파싱 끝난 후 | ✅ 순서대로 |
type="module"스크립트는 기본이 defer처럼 동작한다.
6. 리소스 힌트 — preload vs prefetch
이건 <script>가 아니라 <link rel>로, "이 리소스를 미리 받아둬"라고 브라우저에 힌트를 준다. 받기만 하고 실행/적용은 별개다.
preload — 지금 페이지에 곧 필요한 것 (높은 우선순위)
<link rel="preload" href="hero.woff2" as="font" crossorigin />
- 현재 페이지에서 곧 필요한 중요 리소스를 일찍, 높은 우선순위로 받음
- 브라우저가 늦게 발견하는 자원에 특히 유용 (CSS 안에서 참조되는 폰트, 히어로 이미지, 동적 import될 중요 JS)
as속성 필수 (종류를 알려줘야 우선순위·CORS가 맞음)
prefetch — 다음 페이지에 쓸 것 (낮은 우선순위)
<link rel="prefetch" href="/next-page.js" />
- 미래의 내비게이션에서 쓸 자원을 한가할 때(idle), 낮은 우선순위로 받아 캐시에 저장
- 다음 페이지로 이동하면 캐시에서 즉시 → 체감 속도 ↑
- 예측이 빗나가면 낭비 → 확신이 있을 때만
비교
| 대상 | 시점·우선순위 | 용도 | |
|---|---|---|---|
preload | 현재 페이지 | 일찍, 높음 | 곧 쓸 중요 자원 |
prefetch | 다음 페이지 | idle, 낮음 | 다음 내비게이션 대비 |
7. 친척 힌트들
<link rel="preconnect" href="https://api.example.com" /> <!-- 미리 연결 (DNS+TCP+TLS) -->
<link rel="dns-prefetch" href="https://api.example.com" /> <!-- DNS만 미리 -->
<link rel="modulepreload" href="module.js" /> <!-- ESM 모듈 preload -->
preconnect: 곧 요청할 도메인과 연결을 미리 맺어 핸드셰이크 지연 제거dns-prefetch: DNS 조회만 미리 (preconnect의 가벼운 버전)modulepreload: ESM 모듈 그래프를 미리 받기
8. React와의 연결
이 개념들은 React 앱에서 번들 로딩·코드 스플리팅·라우트 prefetch로 그대로 이어진다.
defer ↔ 번들 로딩
Vite·Next가 주입하는 스크립트는 type="module"이라 기본이 defer다. 그래서 React 번들은 HTML 파싱을 막지 않고, 파싱 후 실행돼 앱을 마운트/hydrate한다. "일반 스크립트엔 defer"가 번들러의 기본값인 셈이다.
preload/prefetch ↔ 코드 스플리팅 (React.lazy)
import()로 쪼갠 청크에 그대로 힌트를 붙인다.
// 곧 필요 → preload (높은 우선순위)
const Modal = lazy(() => import(/* webpackPreload: true */ "./Modal"));
// 나중에 갈 수도 → prefetch (idle에 미리)
const Settings = lazy(() => import(/* webpackPrefetch: true */ "./Settings"));
prefetch ↔ Next.js <Link> (가장 실무적)
<Link href="/products">제품</Link>
// 링크가 뷰포트에 들어오면 다음 페이지 JS·데이터를 idle에 prefetch
// → 클릭 시 캐시에서 즉시 전환
SPA 라우팅이 빠르게 느껴지는 핵심이 바로 이 prefetch다.
async/defer 전략 ↔ next/script
<Script src="..." strategy="beforeInteractive" /> // hydrate 전
<Script src="..." strategy="afterInteractive" /> // hydrate 후 (≈ defer/async)
<Script src="..." strategy="lazyOnload" /> // idle에
리소스 힌트 ↔ React 19 API
<link rel="preload">를 HTML에 쓰던 걸 컴포넌트 안에서 호출하면 React가 <head>로 호이스팅한다.
import { preload, preinit, preconnect } from "react-dom";
preinit("https://.../analytics.js", { as: "script" });
preload("https://.../font.woff2", { as: "font" });
preconnect("https://api.example.com");
next/font(폰트 preload)·next/image의 priority(LCP 이미지 preload)도 같은 원리다.
한눈에 보기
| 브라우저 개념 | React / 프레임워크 |
|---|---|
defer | 번들러의 type="module" (기본 defer) |
코드 스플리팅 + preload/prefetch | React.lazy + import() 청크 (매직 코멘트) |
prefetch | Next.js <Link> 자동 prefetch |
async/defer 전략 | next/script의 strategy |
| 리소스 힌트 | React 19 preload/preinit/preconnect |
9. 정리
async/defer는 스크립트를 언제 실행할까(다운로드는 둘 다 병렬, 차이는 실행 타이밍·순서)를,preload/prefetch는 리소스를 언제·어떤 우선순위로 받아둘까(실행은 별개)를 다룬다. 일반 스크립트엔 defer, 독립 스크립트엔 async, 현재 페이지 중요 자원엔 preload, 다음 페이지 대비엔 prefetch. React 세계에선 이게 번들 로딩(defer) → 코드 스플리팅 청크 → Next.js<Link>prefetch → React 19 힌트 API로 한 단계 추상화돼 자동·선언적으로 처리된다.
스크립트가 파싱을 막는 맥락과 렌더링 파이프라인은 브라우저 렌더링 과정 글에서 다룬다.