Blog

HTML link 태그 성능 최적화 가이드: preload, prefetch, preconnect 완벽 정리

웹 성능을 극대화하는 리소스 힌트를 알아봅니다. preload로 필수 리소스 우선 로딩, preconnect로 외부 서버 사전 연결, prefetch로 다음 페이지 미리 준비하는 방법을 다이어그램과 함께 상세히 설명합니다.

HTML <link> 태그 성능 최적화 가이드

브라우저에게 리소스 로딩을 최적화하도록 힌트를 제공하는 <link> 태그들을 다룹니다. preload, prefetch, preconnect, dns-prefetch 4가지 리소스 힌트의 동작 원리와 실무 사용법을 알아봅니다.


관련 가이드

가이드설명
HTML link 태그 완벽 가이드link 태그 기본 문법, stylesheet, 파비콘, SEO

목차

  1. 리소스 힌트 개요
  2. preconnect - 외부 서버 사전 연결
  3. dns-prefetch - DNS 조회
  4. preload - 리소스 프리로드
  5. prefetch - 사전 가져오기
  6. 리소스 힌트 종합 비교
  7. 성능 최적화 전략

리소스 힌트 개요

4가지 리소스 힌트는 각각 다른 범위와 우선순위로 동작합니다.

힌트하는 일우선순위실행 방식
dns-prefetchDNS 조회만매우 낮음힌트
preconnectDNS + TCP + TLS 전체 연결낮음힌트
preload현재 페이지 필수 리소스 즉시 다운로드높음명령
prefetch다음 페이지 리소스 미리 다운로드최저힌트

preconnect - 외부 서버 사전 연결

외부 서버와의 연결을 미리 수립합니다 (DNS + TCP + TLS).

html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

동작 원리

외부 서버에 리소스를 요청하려면 DNS → TCP → TLS 3단계 연결 과정이 필요합니다. 이 과정만으로 70~320ms가 소요됩니다.

code-highlight
브라우저                                              서버
   │                                                   │
   │  ① DNS 조회 (20~120ms)                            │
   │──── "fonts.gstatic.com의 IP?" ──────→ DNS 서버    │
   │◀─── "142.250.196.106" ──────────── DNS 서버       │
   │                                                   │
   │  ② TCP 3-way Handshake (20~100ms)                 │
   │──── SYN ─────────────────────────────────────→    │
   │     (연결 요청 + 시퀀스 번호)                       │
   │◀─── SYN-ACK ────────────────────────────────      │
   │     (요청 수락 + 서버 시퀀스 번호)                  │
   │──── ACK ─────────────────────────────────────→    │
   │     (확인 완료 → TCP 연결 수립!)                    │
   │                                                   │
   │  ③ TLS 핸드셰이크 (30~100ms)                      │
   │──── ClientHello ────────────────────────────→     │
   │◀─── ServerHello + 인증서 ──────────────────       │
   │──── 키 교환 + Finished ────────────────────→      │
   │◀─── Finished ──────────────────────────────       │
   │                                                   │
   │  ✅ 연결 완료! (총 70~320ms)                       │

각 단계 소요 시간:
┌──────────────┬──────────────┬──────────────┐
│  DNS 조회    │  TCP 연결    │  TLS 협상    │
│  20~120ms    │  20~100ms    │  30~100ms    │
├──────────────┴──────────────┴──────────────┤
│              합계: 70~320ms                 │
└─────────────────────────────────────────────┘

preconnect 사용 전후 비교

preconnect는 이 연결 과정을 HTML 파싱과 병렬로 미리 수행합니다.

code-highlight
시간(ms)  0    50   100   150   200   250   300   350   400   450
          │────│────│─────│─────│─────│─────│─────│─────│─────│

[preconnect 없음] — 리소스 요청 시점에 연결 시작
HTML 파싱  ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
CSS 발견         ░░████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
DNS 조회              ░░░███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
TCP 연결                   ░░██░░░░░░░░░░░░░░░░░░░░░░░░░░
TLS 협상                       ░░░███░░░░░░░░░░░░░░░░░░░░
리소스 전송                           ░░░░██████░░░░░░░░░░
렌더링                                          ░░████████
                                                        ~450ms

[preconnect 사용] — HTML 파싱과 연결을 병렬 처리
HTML 파싱  ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
preconnect ░████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  ← 병렬!
CSS 발견         ░░████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
리소스 전송            ░░░░██████░░░░░░░░░░░░░░░░░░░░░░░░  ← 즉시!
렌더링                          ░░████████░░░░░░░░░░░░░░░
                                                ~300ms

절약 시간: 약 100~200ms

연결 풀 관리와 4-6개 제한 이유

preconnect로 수립된 연결은 브라우저 **연결 풀(connection pool)**에 보관됩니다. 사용하지 않는 연결은 리소스를 낭비합니다.

code-highlight
브라우저 연결 풀:
┌──────────────────────────────────────────────┐
│ 도메인당 최대 동시 연결: 6개 (HTTP/1.1)       │
│ 미사용 연결 자동 해제: ~10초 후               │
└──────────────────────────────────────────────┘

[적절한 사용: 4개] ✅
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ fonts   │ │  cdn    │ │  api    │ │analytics│
│ 사용 ✅  │ │ 사용 ✅  │ │ 사용 ✅  │ │ 사용 ✅  │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
→ 모든 연결이 실제 사용됨

[과도한 사용: 10개] ❌
→ 미사용 연결이 해제될 때까지 CPU, 메모리, 소켓 점유
→ 중요 리소스 다운로드의 대역폭과 경쟁

적합한 사용처

html
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- CDN -->
<link rel="preconnect" href="https://cdn.example.com">

<!-- API 서버 -->
<link rel="preconnect" href="https://api.example.com">

핵심: preconnect는 "이 서버와 곧 통신할 테니 미리 연결해 둬라"는 지시입니다. 최대 4-6개, 가장 중요한 외부 도메인에만 사용하세요.


dns-prefetch - DNS 조회

DNS 조회만 미리 수행합니다 (preconnect보다 가벼움).

html
<link rel="dns-prefetch" href="//cdn.example.com">
<link rel="dns-prefetch" href="//analytics.example.com">

DNS 조회 과정

도메인 이름을 IP 주소로 변환하는 DNS 조회는 계층적 캐시 구조를 따릅니다.

code-highlight
브라우저: "cdn.example.com의 IP 주소가 뭐야?"
┌──────────────────────────────────────────────────┐
│  1단계: 브라우저 DNS 캐시 확인 (~0ms)             │
│  → 최근 방문한 도메인이면 즉시 반환               │
└──────────────┬───────────────────────────────────┘
     캐시 미스 │
┌──────────────────────────────────────────────────┐
│  2단계: OS DNS 캐시 확인 (~1ms)                   │
│  → /etc/hosts 파일 및 OS 레벨 캐시               │
└──────────────┬───────────────────────────────────┘
     캐시 미스 │
┌──────────────────────────────────────────────────┐
│  3단계: ISP DNS 서버 (리커시브 리졸버) (~5-30ms)  │
└──────────────┬───────────────────────────────────┘
     캐시 미스 │
┌──────────────────────────────────────────────────┐
│  4단계: 재귀 DNS 조회 (~30-120ms)                 │
│  루트 DNS → TLD 서버 → 권한 서버 → IP 반환       │
└──────────────────────────────────────────────────┘

dns-prefetch vs preconnect 커버 범위

code-highlight
     DNS 조회         TCP 연결         TLS 협상         리소스 전송
  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
  │  20~120ms    │ │  20~100ms    │ │  30~100ms    │ │    가변      │
  └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
  ◀─ dns-prefetch ▶
  ◀────────────── preconnect ──────────────────────▶
  ◀────────────────────────── preload ─────────────────────────────▶

리소스 비용:
  dns-prefetch  │ ▓░░░░░░░░░░░░░░░░░░░ │ 매우 적음 (DNS 패킷만)
  preconnect    │ ▓▓▓▓▓▓▓░░░░░░░░░░░░░ │ 중간 (소켓 + 메모리)
  preload       │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░ │ 높음 (전체 다운로드)

조합 사용 (권장)

html
<!-- 중요한 도메인: preconnect + dns-prefetch (폴백) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="dns-prefetch" href="//fonts.googleapis.com">

<!-- 덜 중요한 도메인: dns-prefetch만 -->
<link rel="dns-prefetch" href="//www.google-analytics.com">
<link rel="dns-prefetch" href="//www.googletagmanager.com">
구분preconnectdns-prefetch
수행 작업DNS + TCP + TLSDNS만
리소스 비용높음낮음
사용처중요한 외부 도메인덜 중요한 도메인
연결 수4-6개 제한많아도 괜찮음

핵심: dns-prefetchpreconnect가벼운 대안입니다. 중요 도메인에는 preconnect를, 나머지에는 dns-prefetch를 사용하세요.


preload - 리소스 프리로드

현재 페이지에서 즉시 필요한 리소스를 우선 로드합니다.

html
<link rel="preload" href="리소스URL" as="타입">

동작 원리

브라우저는 HTML을 위에서 아래로 파싱하면서 리소스를 발견합니다. 폰트처럼 CSS 안에 선언된 리소스는 HTML → CSS 파싱 → 리소스 발견 순서를 거쳐야 하므로 다운로드가 늦어집니다. preload는 이 순차 발견 체인을 우회하여 HTML 파싱 단계에서 즉시 다운로드를 시작합니다.

code-highlight
시간 ──────────────────────────────────────────────────────────────>

[일반 로딩] 리소스 발견이 늦어지는 문제

HTML ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
CSS        ░░░░██████░░░░░░░░░░░░░░░░░░░░░░░░░
폰트              ░░░░░░░░██████████░░░░░░░░░░░  ← CSS 파싱 후에야 발견
렌더링                              ░░░░████████

[preload 사용] HTML 단계에서 즉시 요청

HTML ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
CSS        ░░░░██████░░░░░░░░░░░░░░░░░░░░░░░░░
폰트  ░░░░██████████░░░░░░░░░░░░░░░░░░░░░░░░░  ← HTML에서 즉시 시작!
렌더링              ░░░░████████░░░░░░░░░░░░░░░
             렌더링이 더 빨라짐!

as 속성 값 (필수)

as 속성은 브라우저 캐시의 **키(key)**를 결정합니다. 없으면 리소스가 두 번 다운로드됩니다.

as 값리소스 타입우선순위
styleCSSHighest
scriptJavaScriptHighest
font폰트 파일High
fetchXHR/Fetch 요청Medium
image이미지Low
documentHTML 문서-
audio오디오-
video비디오-

as 속성의 역할: 이중 다운로드 방지

code-highlight
[as 속성이 있는 경우] ✅ 1회만 다운로드

<link rel="preload" href="font.woff2" as="font" crossorigin>

  preload 요청              @font-face 요청 (CSS)
  ┌──────────────────┐      ┌──────────────────┐
  │ as="font"        │      │ 타입: font        │
  │ CORS: anonymous  │      │ CORS: anonymous   │
  └────────┬─────────┘      └────────┬─────────┘
           │        캐시 키 일치!     │
           ▼                         ▼
  ┌─────────────────────────────────────────────┐
  │            브라우저 캐시 (1회 다운로드)        │
  └─────────────────────────────────────────────┘


[as 속성이 없는 경우] ❌ 2회 다운로드 (낭비)

<link rel="preload" href="font.woff2">  ← as 없음!

  preload 요청              @font-face 요청 (CSS)
  ┌──────────────────┐      ┌──────────────────┐
  │ 타입: 불명        │      │ 타입: font        │
  │ CORS: no-cors    │      │ CORS: anonymous   │
  └────────┬─────────┘      └────────┬─────────┘
           │   캐시 키 불일치!        │
           ▼                         ▼
  ┌──────────────────┐      ┌──────────────────┐
  │ 캐시 A (사용 안됨) │      │ 캐시 B (실제 사용) │
  └──────────────────┘      └──────────────────┘
  → 동일 파일을 2번 다운로드하게 됨!

폰트 preload 시 crossorigin이 필수인 이유

CSS @font-face 스펙에 따라 모든 폰트 요청은 CORS anonymous 모드로 전송됩니다. preload도 이 모드와 일치해야 캐시를 공유합니다.

code-highlight
CSS @font-face 규격:
┌──────────────────────────────────────────────┐
│ 모든 폰트 요청은 CORS anonymous 모드로 전송   │
└─────────────────────┬────────────────────────┘
         ┌────────────┴─────────────┐
         ▼                          ▼
┌──────────────────┐      ┌──────────────────┐
│ preload +         │      │ preload            │
│ crossorigin      │      │ (crossorigin 없음) │
│ → CORS anonymous │      │ → no-cors 모드     │
│ → 캐시 키 일치 ✅ │      │ → 캐시 키 불일치 ❌ │
│ → 1회 다운로드    │      │ → 2회 다운로드      │
└──────────────────┘      └──────────────────┘

결론: <link rel="preload" as="font" crossorigin> ← 반드시 3개 세트!

사용 예시

html
<!-- 폰트 프리로드 (crossorigin 필수!) -->
<link rel="preload"
      href="/fonts/noto-sans-kr.woff2"
      as="font"
      type="font/woff2"
      crossorigin>

<!-- 주요 CSS 프리로드 -->
<link rel="preload" href="/css/critical.css" as="style">
<link rel="stylesheet" href="/css/critical.css">

<!-- LCP 이미지 프리로드 -->
<link rel="preload" as="image" href="/images/hero-banner.webp">

<!-- 반응형 이미지 프리로드 -->
<link rel="preload" as="image"
      href="/images/hero-mobile.webp"
      media="(max-width: 600px)">
<link rel="preload" as="image"
      href="/images/hero-desktop.webp"
      media="(min-width: 601px)">

<!-- fetchpriority로 우선순위 조절 -->
<link rel="preload" href="/critical.js" as="script" fetchpriority="high">
<link rel="preload" href="/analytics.js" as="script" fetchpriority="low">

핵심: preload는 "지금 당장 필요한 리소스를 미리 받아라"는 명령입니다. 브라우저는 반드시 실행하며, 3초 내에 사용하지 않으면 콘솔에 경고를 출력합니다.

주의사항

  • as 속성 필수: 없으면 리소스가 두 번 다운로드될 수 있음
  • 필요한 리소스만: 불필요한 preload는 다른 중요 리소스의 대역폭을 빼앗음
  • crossorigin 주의: CORS 리소스(폰트 등)는 crossorigin 속성 필수

prefetch - 사전 가져오기

미래 탐색에서 필요할 수 있는 리소스를 낮은 우선순위로 미리 로드합니다.

html
<link rel="prefetch" href="/next-page.html">
<link rel="prefetch" href="/js/checkout.js" as="script">

동작 원리

prefetch현재 페이지의 모든 리소스가 로딩된 후, 브라우저가 유휴(idle) 상태일 때 실행됩니다.

code-highlight
시간 ──────────────────────────────────────────────────────────────>

┌─────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐ ┌──────────────┐
│  HTML   │→│   CSS    │→│   JS     │→│  이미지 │→│  유휴 시간   │
│  파싱   │ │  다운로드 │ │  실행    │ │  로드   │ │  (idle)      │
└─────────┘ └──────────┘ └──────────┘ └────────┘ └──────┬───────┘
◀──────── 현재 페이지 필수 리소스 (높은 우선순위) ────────▶      │
                                              ┌─────────────────┐
                                              │  prefetch 실행   │
                                              │  (최저 우선순위)  │
                                              └─────────────────┘

※ 브라우저가 "한가할 때" = 현재 페이지의 모든 리소스 로딩 완료 후
※ 네트워크가 바쁘면 prefetch는 무시될 수 있음

캐시 저장 방식: preload vs prefetch

code-highlight
┌───────────────────────┐  ┌───────────────────────┐
│ preload 캐시           │  │ prefetch 캐시          │
├───────────────────────┤  ├───────────────────────┤
│ 메모리 캐시 (빠름)     │  │ 디스크 캐시 (지속성)   │
│ 현재 페이지에서 사용    │  │ 다음 탐색에서 사용     │
│ 페이지 이동 시 소멸     │  │ 브라우저 닫아도 유지   │
│ 유효 기간: ~10-20초    │  │ 유효 기간: 수분~수시간 │
└───────────────────────┘  └───────────────────────┘

필수(preload) vs 선택(prefetch) 동작 비교

code-highlight
preload (필수 실행 - 명령)
──────────────────────────
브라우저 → "이 리소스를 반드시 다운로드하라"
     ├─ 네트워크 바쁨?     → 그래도 다운로드 (높은 우선순위)
     ├─ 배터리 부족?       → 그래도 다운로드
     └─ 3초 내 미사용?     → 콘솔에 경고 메시지 출력

prefetch (선택 실행 - 힌트)
──────────────────────────
브라우저 → "여유 있으면 이것도 받아두면 좋겠다"
     ├─ 네트워크 바쁨?     → 건너뜀
     ├─ 배터리 부족?       → 건너뜀
     ├─ 데이터 세이버 ON?  → 건너뜀
     └─ Safari?           → 미지원 (완전 무시)

preload vs prefetch

구분preloadprefetch
용도현재 페이지 필수 리소스미래 페이지 리소스
우선순위높음 (강제)낮음 (힌트)
브라우저 준수반드시 실행무시 가능
Safari 지원OX
사용 시점즉시 필요한 리소스다음 페이지 리소스

사용 예시

html
<!-- 다음 페이지 HTML -->
<link rel="prefetch" href="/checkout.html">

<!-- 다음 페이지에서 필요한 스크립트 -->
<link rel="prefetch" href="/js/payment.js" as="script">

<!-- 사용자가 클릭할 가능성이 높은 리소스 -->
<link rel="prefetch" href="/images/product-detail.webp" as="image">

주의사항

  • Safari 미지원: Safari는 prefetch를 지원하지 않음 (배터리/데이터 절약 정책)
  • 저우선순위: 다른 모든 리소스 로드 후 실행
  • 확실하지 않은 예측은 피하기: 사용되지 않을 리소스는 대역폭 낭비

리소스 힌트 종합 비교

페이지 로드 중 4가지 힌트의 동작 타임라인

code-highlight
시간 ──────────────────────────────────────────────────────────────>
     0ms        100ms       200ms       500ms       1000ms+

HTML ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
파싱 (link 태그 발견)
        │  │  │  │
        │  │  │  └──── prefetch ──────────────────────────────────
        │  │  │                                  ░░░░░░████████
        │  │  │                                  (유휴 시간에 실행)
        │  │  │
        │  │  └─── preload ─────────────────────>
        │  │       ██████████████████             (즉시 고우선순위)
        │  │
        │  └──── preconnect ────>
        │        ███[DNS]███[TCP]███[TLS]          (연결만 수립)
        └───── dns-prefetch ──>
               ███[DNS]                            (DNS만 조회)

네트워크 레벨 비교

DNS 조회TCP 연결TLS 협상리소스 다운로드
dns-prefetch✅ 미리---
preconnect✅ 미리✅ 미리✅ 미리-
preload✅ 미리✅ 미리✅ 미리✅ 즉시 전송
prefetch✅ 유휴✅ 유휴✅ 유휴✅ 유휴 시 전송

종합 비교 테이블

절약 시간실행 방식리소스 비용개수 제한브라우저 지원
dns-prefetch~50ms힌트매우 낮음거의 없음매우 좋음
preconnect~130ms힌트중간4-6개좋음
preload~200ms명령높음필요한 만큼좋음
prefetch가변힌트중간적당히Safari 미지원

리소스 힌트 선택 가이드

code-highlight
                  ┌─────────────────────┐
                  │  외부 리소스가       │
                  │  필요한가?          │
                  └──────────┬──────────┘
                  ┌──────────▼──────────┐
                  │  현재 페이지에서     │
            ┌─YES─┤  즉시 필요한가?     ├─NO─┐
            │     └─────────────────────┘    │
            ▼                                ▼
  ┌──────────────────┐            ┌─────────────────────┐
  │    preload        │            │  다음 페이지에서     │
  │ as 속성 필수     │      ┌─YES─┤  필요한가?          ├─NO─┐
  │ 폰트→crossorigin │      │     └─────────────────────┘    │
  └──────────────────┘      ▼                                ▼
                       ┌──────────┐           ┌──────────────────┐
                       │ prefetch │           │  외부 도메인인가? │
                       └──────────┘     ┌─YES─┤                  ├─NO─┐
                                        │     └──────────────────┘    │
                                        ▼                             ▼
                             ┌──────────────────┐          ┌────────────┐
                             │  중요한 도메인?   │          │  힌트 불필요 │
                        ┌YES─┤  (4-6개 이내)     ├NO─┐     └────────────┘
                        │    └──────────────────┘   │
                        ▼                           ▼
             ┌─────────────────┐         ┌─────────────────┐
             │   preconnect    │         │  dns-prefetch   │
             │ DNS+TCP+TLS    │         │ DNS만 조회       │
             └─────────────────┘         └─────────────────┘

성능 최적화 전략

권장 head 태그 순서

html
<head>
    <!-- 1. 메타 태그 -->
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <!-- 2. 리소스 힌트 (연결 관련, 가장 먼저) -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link rel="dns-prefetch" href="//cdn.example.com">

    <!-- 3. 프리로드 (현재 페이지 필수) -->
    <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
    <link rel="preload" href="/css/critical.css" as="style">

    <!-- 4. 스타일시트 -->
    <link rel="stylesheet" href="/css/critical.css">
    <link rel="stylesheet" href="/css/main.css">

    <!-- 5. 프리페치 (다음 페이지, 마지막) -->
    <link rel="prefetch" href="/js/checkout.js" as="script">
</head>

순서가 중요합니다: 연결 힌트 → 프리로드 → 스타일시트 → 프리페치

주의사항 요약

힌트주의사항
preconnect4-6개 제한, 중요한 도메인만
dns-prefetch많이 사용해도 OK, preconnect 폴백으로
preloadas 속성 필수, 폰트는 crossorigin 필수
prefetchSafari 미지원, 확실한 경우만 사용

참고 자료