이번 편의 결과물: 소개 영상과 웹폰트가 preload/preconnect로 미리 준비되고, 지금까지 각 페이지에 흩어 둔 함수를 js/utils.js로 분리해 js/main.js가 import로 불러 쓴다. · 다루는 개념: 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 | 도메인 이름만 미리 조회 | 연결 여부가 불확실한 외부 도메인 |
preconnect | DNS 조회+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.html의 head에 영상 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.html과 contact.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/portfoliofile://index.html로 직접 열면 모듈 import가 CORS 오류로 막히므로, 반드시 위처럼 로컬 서버를 띄워서 확인한다.
5. 확인
- 개발자 도구 Network 탭에서
fonts.gstatic.com연결이 문서 파싱 초반에 이미 열려 있다(Connection Start 열이 짧게 표시됨). intro.mp4가 다른 리소스보다 먼저 요청되기 시작한다.- 콘솔에 “리소스가 로드되었지만 사용되지 않았습니다” 같은
preload경고가 없다. - 콘솔에 모듈 관련 오류(
Failed to load module script등)가 없다.
직접 해보기
contact.html에도 실수로intro.mp4를preload해 보고, 콘솔에 어떤 경고가 뜨는지 확인해 본다. 확인 후에는 반드시 지운다.js/utils.js에qsa를 쓰지 않는 페이지가 있는지 찾아보고, 안 쓰는 import는 지워 본다.
답 확인
contact.html에는 영상이 없으므로intro.mp4가 로드는 되지만 어떤 요소도 그것을 사용하지 않는다. Chrome은 몇 초 안에 사용되지 않으면 “The resource was preloaded using link preload but not used” 경고를 콘솔에 남긴다. 이 경고가 뜨면 그 페이지에서는preload를 지우는 것이 맞다.- import했지만 실제로 호출하지 않는 함수는 번들러가 없는 환경에서는 브라우저가 그냥 무시하고 넘어가지만, 코드 가독성을 위해 지우는 편이 좋다. 사용하는 함수만 구조 분해로 남긴다.
자주 하는 실수
| 증상 | 원인 | 고치는 법 |
|---|---|---|
preload했는데 콘솔 경고가 뜬다 | as 속성을 빠뜨렸거나, 실제로 쓰지 않는 리소스를 preload했다 | as를 정확히 쓰고, 그 페이지에서 진짜 쓰는 리소스만 preload한다 |
| 폰트가 두 번 연결된다 | preconnect에 crossorigin을 빠뜨렸다 | 폰트처럼 CORS로 요청되는 리소스는 crossorigin을 함께 쓴다 |
import가 정의되지 않았다는 오류 | script 태그에 type="module"을 빠뜨렸다 | importmap과 import 문법은 반드시 type="module"과 세트로 쓴다 |
| 로컬에서 모듈 로딩이 막힌다 | file://로 직접 열었다 | npx serve 같은 로컬 서버로 열어서 확인한다 |
prefetch를 아무 페이지에나 남발한다 | 사용자가 갈 가능성이 낮은 페이지까지 미리 받는다 | 실제 내비게이션 동선에서 다음 방문 확률이 높은 페이지에만 쓴다 |