Skip to Content
WebHTML모던 HTML13. 리소스 힌트와 모듈 스크립트 정리

이번 편의 결과물: 소개 영상과 웹폰트가 preload/preconnect로 미리 준비되고, 지금까지 각 페이지에 흩어 둔 함수를 js/utils.js로 분리해 js/main.jsimport로 불러 쓴다. · 다루는 개념: preconnect/dns-prefetch, preload/prefetch, script type="module"importmap

이 편에서 만드는 파일

web-practice/portfolio/ ├── index.html ~ (head 리소스 힌트 + importmap, script type="module") ├── projects.html ~ (head 리소스 힌트, script type="module") ├── contact.html ~ (head 리소스 힌트, script type="module") └── js/ ├── main.js ~ (모듈로 정리, utils import) └── utils.js + (공용 헬퍼 분리)

개념 정리

리소스 힌트는 브라우저에게 “이 리소스가 곧 필요하다”고 미리 알려서 대기 시간을 줄이는 <link> 태그다. 넷 다 목적이 다르다.

힌트하는 일쓰는 시점
dns-prefetch도메인 이름만 미리 조회연결 여부가 불확실한 외부 도메인
preconnectDNS 조회+TCP 연결+TLS 협상까지 미리 완료이번 페이지에서 확실히 쓸 외부 도메인(폰트 CDN 등)
preload이 문서에서 곧 쓸 리소스를 최고 우선순위로 미리 받아 둠자바스크립트가 나중에 발견할 영상·폰트처럼 늦게 발견되면 손해인 리소스
prefetch다음에 방문할 가능성이 있는 페이지의 리소스를 낮은 우선순위로 미리 받아 둠사용자가 곧 이동할 것으로 예상되는 다른 페이지

preload는 반드시 as 속성으로 리소스 종류(video, font, image, script 등)를 명시해야 한다. as가 없으면 브라우저가 우선순위를 정하지 못해 리소스를 두 번 받는 경우가 생긴다. 폰트를 preconnect할 때는 crossorigin을 함께 써야 한다. 폰트 요청은 CORS 모드로 나가기 때문에, crossorigin 없이 연결한 소켓은 재사용되지 않고 새로 열린다.

<script type="module">은 클래식 스크립트와 다르게 동작한다. 기본적으로 defer처럼 지연 실행되고, 모듈마다 독립된 스코프를 가져서 전역 변수를 오염시키지 않으며, import/export 문법을 쓸 수 있다. 다만 file:// 프로토콜로 열면 모듈 로딩이 CORS 정책에 막혀 실패하므로 반드시 로컬 서버로 열어야 한다. <script type="importmap">import 문에 쓸 짧은 이름(bare specifier)을 실제 경로에 연결하는 표다. 이 과목에서는 파일이 몇 개 없어 크게 필요하진 않지만, 상대 경로가 길어지는 것을 막는 용도로 가볍게 하나만 넣는다.

실습

1. 공용 헬퍼를 utils.js로 분리하기

지금까지 main.js 안에 늘어놓았던 짧은 함수 중 여러 편에서 재사용하는 것만 js/utils.js로 뺀다.

// js/utils.js export function qs(selector, scope = document) { return scope.querySelector(selector) } export function qsa(selector, scope = document) { return Array.from(scope.querySelectorAll(selector)) } export function announce(message) { const live = qs('#live-region') if (!live) return live.textContent = '' requestAnimationFrame(() => { live.textContent = message }) }

announce는 검색 필터링(08편)이나 폼 검증(12편) 결과처럼 화면 변화만으로는 스크린 리더가 알기 어려운 상태 변화를 aria-live 영역에 문장으로 전달할 때 쓴다.

2. main.js를 모듈로 정리하기

// js/main.js import { qs, qsa, announce } from './utils.js' // ...기존 코드 유지 (dialog 열기/닫기, popover 보조 로직, 검색 필터링, 폼 검증) // 위 함수들은 전부 이 파일 안에서 qs/qsa/announce를 그대로 불러 쓴다. // 예: 검색 필터링 함수 안에서 function filterProjects(keyword) { const cards = qsa('.project-card') const visible = cards.filter((card) => { const match = card.textContent.toLowerCase().includes(keyword.toLowerCase()) card.hidden = keyword.length > 0 && !match ? 'until-found' : false return match }) announce(`${visible.length}개의 프로젝트를 찾았습니다.`) }

파일 전체를 클래식 스크립트에서 모듈로 바꾸는 것뿐이므로, import 한 줄을 추가하고 최상단을 감싸던 즉시 실행 함수(IIFE)가 있었다면 지운다. 모듈은 파일 자체가 독립 스코프라 IIFE가 필요 없다.

3. 세 페이지 head에 리소스 힌트와 importmap 넣기

index.htmlhead에 영상 preload와 폰트 preconnect, 다음 페이지 prefetch를 추가한다.

<!-- index.html --> <head> <!-- ...기존 meta·title·stylesheet 유지 --> <link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="dns-prefetch" href="https://fonts.googleapis.com" /> <link rel="preload" href="videos/intro.mp4" as="video" type="video/mp4" /> <link rel="prefetch" href="projects.html" /> <script type="importmap"> { "imports": { "utils": "./js/utils.js" } } </script> </head> <!-- ...기존 body 유지 --> <script type="module" src="js/main.js"></script>

projects.htmlcontact.html도 같은 폰트 preconnect 두 줄과 script type="importmap"을 넣는다. preload·prefetch는 페이지마다 다르게 판단한다. projects.html은 영상이 없으므로 preload를 넣지 않고, 대신 contact.html로 이동할 가능성을 고려해 <link rel="prefetch" href="contact.html" />만 추가한다. contact.html은 다음 이동이 불확실하므로 prefetch를 넣지 않는다.

4. 실행

npx serve web-practice/portfolio

file://index.html로 직접 열면 모듈 import가 CORS 오류로 막히므로, 반드시 위처럼 로컬 서버를 띄워서 확인한다.

5. 확인

  • 개발자 도구 Network 탭에서 fonts.gstatic.com 연결이 문서 파싱 초반에 이미 열려 있다(Connection Start 열이 짧게 표시됨).
  • intro.mp4가 다른 리소스보다 먼저 요청되기 시작한다.
  • 콘솔에 “리소스가 로드되었지만 사용되지 않았습니다” 같은 preload 경고가 없다.
  • 콘솔에 모듈 관련 오류(Failed to load module script 등)가 없다.

직접 해보기

  1. contact.html에도 실수로 intro.mp4preload해 보고, 콘솔에 어떤 경고가 뜨는지 확인해 본다. 확인 후에는 반드시 지운다.
  2. js/utils.jsqsa를 쓰지 않는 페이지가 있는지 찾아보고, 안 쓰는 import는 지워 본다.

답 확인

  1. contact.html에는 영상이 없으므로 intro.mp4가 로드는 되지만 어떤 요소도 그것을 사용하지 않는다. Chrome은 몇 초 안에 사용되지 않으면 “The resource was preloaded using link preload but not used” 경고를 콘솔에 남긴다. 이 경고가 뜨면 그 페이지에서는 preload를 지우는 것이 맞다.
  2. import했지만 실제로 호출하지 않는 함수는 번들러가 없는 환경에서는 브라우저가 그냥 무시하고 넘어가지만, 코드 가독성을 위해 지우는 편이 좋다. 사용하는 함수만 구조 분해로 남긴다.

자주 하는 실수

증상원인고치는 법
preload했는데 콘솔 경고가 뜬다as 속성을 빠뜨렸거나, 실제로 쓰지 않는 리소스를 preload했다as를 정확히 쓰고, 그 페이지에서 진짜 쓰는 리소스만 preload한다
폰트가 두 번 연결된다preconnectcrossorigin을 빠뜨렸다폰트처럼 CORS로 요청되는 리소스는 crossorigin을 함께 쓴다
import가 정의되지 않았다는 오류script 태그에 type="module"을 빠뜨렸다importmapimport 문법은 반드시 type="module"과 세트로 쓴다
로컬에서 모듈 로딩이 막힌다file://로 직접 열었다npx serve 같은 로컬 서버로 열어서 확인한다
prefetch를 아무 페이지에나 남발한다사용자가 갈 가능성이 낮은 페이지까지 미리 받는다실제 내비게이션 동선에서 다음 방문 확률이 높은 페이지에만 쓴다

확인 문제

문제 14지선다
preconnect와 dns-prefetch의 차이로 옳은 것은?
문제 24지선다
link rel=preload를 쓸 때 반드시 함께 지정해야 하는 속성은?
문제 34지선다
script type=module에 대한 설명으로 옳지 않은 것은?
문제 44지선다
importmap의 역할로 가장 정확한 것은?

참고 자료

Last updated on