본문으로 건너뛰기
yutils

resource hint(preload·preconnect·prefetch) 는 어떻게 동작할까?

resource hint 총정리 — preload·preconnect·dns-prefetch·prefetch·modulepreload·fetchpriority·103 Early Hints·Speculation Rules, 각각 언제 쓰는지.

약 8분 읽기

브라우저는 HTML 을 위에서 아래로 parse 하면서 필요한 리소스를 하나씩 발견한다. font 는 CSS 를 받아 parse 한 뒤에야, LCP image 는 그걸 참조하는 마크업을 만난 뒤에야 요청된다. resource hint 는 이 발견을 앞당기는 도구 — <link rel> 한 줄로 "이건 곧 필요하니 미리 받아둬" 를 브라우저에게 알린다. 단, 잘못 쓰면 대역폭만 낭비하고 정작 critical 리소스의 우선순위를 깎는다. 이 가이드는 hint family 각각이 무엇을 하는지, 언제 써야 하는지, 그리고 흔한 실수를 정리한다.

왜 hint 가 필요한가 — 발견의 지연

<!-- 브라우저가 보는 순서 -->
1. HTML 받음 → parse 시작
2. <link rel="stylesheet" href="app.css"> 발견 → CSS 요청
3. CSS 받음 → parse → @font-face url(...) 발견 → font 요청   ← 여기!
4. <img src="hero.jpg"> 발견 → LCP image 요청

font 와 LCP image 는 chain 끝에서야 발견된다.
preload 로 1번 직후 병렬 요청하면 수백 ms 단축.

핵심은 critical chain 단축. 브라우저가 스스로 발견하기 전에 우리가 먼저 알려주는 것. 자세한 critical path 는 how-browsers-render-pages 가이드 참조.

dns-prefetch — DNS 만 미리

<link rel="dns-prefetch" href="https://cdn.example.com">
  • 해당 origin 의 DNS 해석만 미리 수행. TCP / TLS 는 안 함.
  • 가장 싼 hint — 거의 비용 없음. 실제 연결을 약속하지 않음.
  • 언제 — 곧 쓸지도 모르는 덜 critical한 3rd-party origin (analytics, 광고, 폰트 CDN 후보). 연결까지 보장하긴 아까울 때.
  • DNS lookup 은 보통 20-120ms. 모바일·느린 네트워크에서 체감 ↑.

preconnect — DNS + TCP + TLS 까지

<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  • DNS 해석 + TCP handshake + (HTTPS 면) TLS negotiation 까지 미리. 연결을 "데워둔다(warm)".
  • 언제 — 곧 확실히 쓸 critical 3rd-party origin. font host, image CDN, API origin.
  • crossorigin 주의 — font 는 anonymous CORS 모드로 받기 때문에 crossorigin 속성이 없으면 preconnect 한 연결이 재사용되지 않고 새 연결이 또 열린다. font origin 엔 거의 항상 crossorigin 필요.
  • 너무 많이 쓰지 말 것 — origin 당 연결 자원을 잡는다. 4-6 개를 넘기면 오히려 critical 연결과 경쟁. 정말 쓰는 origin 에만.
<!-- 흔한 Google Fonts 패턴 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- googleapis = CSS (same-origin fetch), gstatic = font (crossorigin) -->

font 가 실제로 어떻게 로드되고 FOUT / FOIT 가 왜 생기는지는 how-web-fonts-load 가이드에서 다룬다. CDN 연결 비용의 내부는 how-cdns-actually-work 참조.

preload — 이 페이지에 필요한 걸 높은 우선순위로

<link rel="preload" href="/fonts/inter.woff2" as="font"
      type="font/woff2" crossorigin>
<link rel="preload" href="/hero.avif" as="image"
      fetchpriority="high">
<link rel="preload" href="/critical.css" as="style">
  • 지금 이 페이지에 곧 필요한 리소스를 즉시, 높은 우선순위로 fetch. dns-prefetch / preconnect 가 "연결" 을 데우는 것과 달리 preload 는 실제 리소스를 받는다.
  • as 필수as="font" / as="image" / as="style" / as="script". as 가 없으면 브라우저가 우선순위·CORS 모드·Accept 헤더를 정하지 못해 hint 가 무시되거나 리소스를 두 번 받는다.
  • font 는 crossorigin — font 는 same-origin 이라도 CORS 모드로 받으므로 crossorigin 없으면 double-fetch.
  • 언제 — late-discovered critical 리소스: CSS 가 참조하는 폰트, LCP image, JS 가 동적으로 부르는 critical chunk. 처음부터 HTML 에 보이는 리소스는 보통 preload 불필요.
  • unused 경고 — preload 한 리소스를 몇 초 안에 실제로 쓰지 않으면 콘솔에 "was preloaded but not used" 경고. 대역폭만 낭비한 신호.

fetchpriority — 우선순위 힌트

<img src="hero.avif" fetchpriority="high">      <!-- LCP image -->
<img src="below-fold.jpg" fetchpriority="low">  <!-- 화면 밖 -->
<script src="analytics.js" fetchpriority="low"></script>
  • high / low / auto 브라우저의 기본 우선순위를 위/아래로 조정. img, script, link, fetch() 에 적용 가능.
  • 브라우저는 기본적으로 첫 image 를 low priority 로 받기 시작한다 (LCP 여부를 아직 모름). LCP image 에 fetchpriority="high" 를 주면 즉시 끌어올린다 — 가장 효과 큰 단일 변경 중 하나.
  • 반대로 화면 밖 image, 비핵심 script 는 low 로 내려 critical 리소스에 대역폭을 양보.
  • preload 와 결합 — <link rel="preload" as="image" fetchpriority="high"> 로 발견과 우선순위를 동시에 처리.

prefetch — 다음 navigation 용 저우선순위 캐시

<link rel="prefetch" href="/dashboard" as="document">
<link rel="prefetch" href="/next-page.js" as="script">
  • 다음에 갈 가능성이 높은 페이지·리소스를 가장 낮은 우선순위로 미리 받아 cache 에 저장. 현재 페이지가 한가할 때(idle) fetch.
  • preload 와의 차이 — preload = 이 페이지 지금, prefetch = 다음 페이지 나중. 우선순위와 사용 시점이 정반대.
  • 언제 — pagination "다음", 마법사의 다음 단계, hover 시 상세 페이지 등 사용자 다음 행동이 거의 확실할 때.
  • 틀린 추측이면 받은 데이터가 버려져 대역폭 낭비. 확실한 경로에만.

modulepreload — ES module 을 받고 parse 까지

<link rel="modulepreload" href="/app.js">
<link rel="modulepreload" href="/router.js">
<link rel="modulepreload" href="/store.js">
  • ES module 전용 preload. 단순히 받기만 하는 게 아니라 parse + module map 등록 + dependency graph 해석까지 미리 한다.
  • as="script" 가 암시되어 적지 않아도 된다. 일반 preload as="script" 와 달리 import 그래프를 따라 내려간다.
  • 언제 — 깊은 import chain 을 가진 entry module. bundler(Vite 등)가 동적 import 의 chunk 들에 대해 자동 삽입하는 경우가 많다.

103 Early Hints — 응답 전에 hint 부터

-- 서버 흐름
1. 요청 도착 → 서버가 페이지 렌더 시작 (DB 조회 등, 느림)
2. 본문이 준비되기 전에 먼저:
   HTTP/1.1 103 Early Hints
   Link: </app.css>; rel=preload; as=style
   Link: <https://cdn.example.com>; rel=preconnect
3. 본문 준비 완료:
   HTTP/1.1 200 OK
   ...실제 HTML...
  • 서버가 최종 200 응답을 만드는 동안(렌더·DB 대기) 먼저 103 으로 preconnect / preload 힌트만 보낸다. 브라우저는 그 시간에 연결을 데우고 리소스를 받기 시작.
  • HTML 안의 <link> 보다 더 빠르다 — HTML 의 첫 바이트(TTFB)를 기다리지 않기 때문.
  • Cloudflare, Fastly 등 CDN 과 Chrome 이 지원. server-side 렌더링이 느린 페이지에서 특히 효과.

Speculation Rules API — 다음 페이지를 미리 받거나 렌더

<script type="speculationrules">
{
  "prefetch": [
    { "where": { "href_matches": "/articles/*" }, "eagerness": "moderate" }
  ],
  "prerender": [
    { "where": { "selector_matches": "a.next" }, "eagerness": "eager" }
  ]
}
</script>
  • JSON 규칙으로 브라우저에게 "이 링크들은 prefetch / prerender 해도 좋다" 를 선언. prerender 는 다음 페이지를 백그라운드에서 실제로 렌더까지 해둬, 클릭 시 즉시 표시.
  • deprecated 된 <link rel="prerender"> 의 현대적 대체. URL 패턴 매칭, eagerness(conservative / moderate / eager), hover·pointerdown 트리거 등 세밀한 제어.
  • prerender 는 비용이 크다(전체 페이지 렌더). eagerness 를 낮춰 오발 prerender 를 줄이고, 분석·결제 같은 부작용 있는 페이지는 제외.

흔한 실수

1. over-preloading

"빠르면 좋으니" 모든 걸 preload 하면, 모두가 high priority 가 되어 결국 아무것도 우선순위가 없는 상태가 된다. 진짜 critical 한 LCP image·font 가 그다음 줄의 비핵심 리소스와 대역폭을 다툰다. preload 는 소수 정예로.

2. as / crossorigin 누락

<!-- Bad — as 없음 → 무시되거나 double-fetch -->
<link rel="preload" href="/inter.woff2">

<!-- Bad — font 인데 crossorigin 없음 → 연결 재사용 안 됨 -->
<link rel="preload" href="/inter.woff2" as="font">

<!-- Good -->
<link rel="preload" href="/inter.woff2" as="font"
      type="font/woff2" crossorigin>

3. unused preload 경고

preload 했지만 몇 초 안에 안 쓰면 "was preloaded but not used" 경고. 보통 오타난 URL(쿼리·해시·버전이 실제 사용 URL 과 다름) 또는 더 이상 안 쓰는 리소스가 원인. URL 이 1바이트라도 다르면 별개로 취급되어 double-fetch.

4. preconnect 남발

실제로 안 쓰는 origin 에 preconnect 하면 연결만 열고 버린다. TLS handshake 는 공짜가 아니다. 정말 첫 화면에 쓰는 critical origin 2-4 개만.

5. prefetch 추측 빗나감

사용자가 가지 않을 페이지를 prefetch 하면 그대로 낭비. data saver 모드·느린 연결에서는 특히 민폐. Speculation Rules 의 conservative eagerness 가 안전한 기본값.

어떤 hint 를 언제 쓰나 — 요약 표

hint            받는 것              우선순위   언제
──────────────────────────────────────────────────────────────
dns-prefetch    DNS 만               -          덜 critical 3rd-party
preconnect      DNS+TCP+TLS          -          확실히 쓸 critical origin
preload         리소스 (이 페이지)   높음        late-discovered font/LCP/CSS
modulepreload   ES module + graph    높음        깊은 import chain entry
prefetch        리소스 (다음 페이지) 가장 낮음   거의 확실한 다음 navigation
fetchpriority   (속성, 새 fetch X)   조정        LCP=high, 화면밖=low
103 Early Hints 서버발 preconnect/preload         TTFB 느린 SSR 페이지
Speculation     다음 페이지 prefetch/prerender    SPA 류 navigation 예측

Core Web Vitals 와의 연결

resource hint 의 실전 가치는 대부분 LCP CLS 로 측정된다. LCP image 를 preload + fetchpriority="high" 로 끌어올리면 LCP 가 직접 줄고, font 를 preconnect / preload 로 일찍 받으면 텍스트가 늦게 swap 되며 생기는 layout shift(CLS)와 invisible text 구간이 줄어든다. 다만 측정 없이 hint 를 추가하면 역효과 — 반드시 Lighthouse·WebPageTest 으로 before/after 확인. 측정 지표 자체는 how-core-web-vitals-work 가이드에서 다룬다.

참고 자료

요약

  • resource hint = 브라우저의 리소스 발견을 앞당겨 critical chain 을 단축하는 도구.
  • dns-prefetch(DNS) < preconnect(DNS+TCP+TLS) < preload(실제 리소스) 순으로 "데우는" 정도가 깊어진다.
  • preload 는 as 필수, font 는 crossorigin 필수. 안 그러면 무시되거나 double-fetch.
  • fetchpriority="high" 를 LCP image 에 — 가장 효과 큰 단일 변경.
  • prefetch / Speculation Rules 는 다음 navigation 용. preload 는 페이지용.
  • 103 Early Hints 는 TTFB 가 느린 SSR 에서 응답 전에 연결을 데운다.
  • 가장 흔한 실수는 over-preloading 과 as/crossorigin 누락. 적게, 정확히, 그리고 측정.
가이드 목록으로