목차10개 항목
출처와 참고
javascript.info(모던 JavaScript 튜토리얼)의 「DOMContentLoaded, load, beforeunload, unload 이벤트」, 「Resource loading: onload and onerror」 글을 바탕으로, 페이지를 떠날 때의 권장 방식은 MDN으로 확인해 재구성한 노트다. 숨은 페이지의 동작은 MDN의 「Page Visibility API」로 보강했다.
한눈에 보기
flowchart TB
subgraph loading["document.readyState: loading"]
p1["HTML 파싱"] --> p2["동기 스크립트 실행"] --> p3["defer·module 스크립트 실행"]
end
subgraph interactive["document.readyState: interactive"]
e1["DOMContentLoaded (document)<br/>DOM 완성. 이미지·iframe은 아직일 수 있다"] --> e2["이미지·iframe·스타일시트 등 load"]
end
subgraph complete["document.readyState: complete"]
e3["load (window)<br/>모든 리소스 완료"]
end
use["…사용 중…<br/>visibilitychange(hidden/visible) 탭 전환마다"]
subgraph leave["떠날 때"]
l1["beforeunload"] --> l2["pagehide"] --> l3["visibilitychange(hidden)"] --> l4["(unload)"]
end
p3 --> e1
e2 --> e3
e3 --> use --> l1| 이벤트 | 대상 | 쓰는 곳 |
|---|---|---|
DOMContentLoaded | document | DOM을 찾아 리스너를 붙이고 UI를 초기화한다 |
load | window | 이미지 크기처럼 모든 리소스가 있어야 아는 값이 필요할 때. 기다림이 길어 잘 안 쓴다 |
beforeunload | window | 저장하지 않은 변경이 있을 때 떠나기 전 확인 |
visibilitychange | document | 숨겨질 때 상태 저장·분석 데이터 전송. 마지막으로 확실히 일어나는 이벤트 |
pagehide·pageshow | window | 다른 페이지로 갈 때와 돌아올 때. bfcache 복원 감지 |
DOMContentLoaded
DOMContentLoaded는 document에서 일어나고 addEventListener로만 받는다. document.onDOMContentLoaded = …는 동작하지 않는다.
| DOMContentLoaded가 기다린다 | 기다리지 않는다 |
|---|---|
속성 없는 <script>(인라인·외부)의 다운로드와 실행 | async 스크립트 |
defer·type="module" 스크립트 | document.createElement('script')로 넣은 동적 스크립트 |
| 스크립트 앞의 스타일시트(스크립트가 스타일을 읽을 수 있어서) | 이미지, iframe, 폰트 |
| 스크립트가 뒤따르지 않는 스타일시트 |
- 스타일시트 자체는 DOM을 바꾸지 않아
DOMContentLoaded가 기다리지 않는다. 하지만 바로 뒤에 동기 스크립트가 있으면 그 스크립트가getComputedStyle같은 값을 읽을 수 있어 CSS를 기다리고,DOMContentLoaded는 그 스크립트를 기다린다. 결국 CSS를 기다리게 된다. - 브라우저의 아이디·비밀번호 자동 완성은
DOMContentLoaded시점에 채워진다. 동기 스크립트가 길어 이 이벤트가 늦어지면 자동 완성도 늦다. - 스크립트 배치(
async·defer·module)가 이 시점에 주는 영향은 스크립트·스타일 로딩 순서에 있다.
이미 지나갔을 수 있다
문서가 다 로드된 뒤에 붙인 DOMContentLoaded 리스너는 영원히 실행되지 않는다. 언제 실행될지 모르는 코드(나중에 불러온 라이브러리)는 document.readyState를 먼저 본다.
readyState | 상태 |
|---|---|
'loading' | 문서를 파싱하는 중 |
'interactive' | 파싱 끝. DOMContentLoaded 직전에 이 값이 된다 |
'complete' | 리소스까지 모두 로드됨. window의 load 직전에 이 값이 된다 |
function init() { /* … */ }
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init)
} else {
init() // 이미 DOM이 있다
}상태가 바뀔 때마다 readystatechange 이벤트도 일어나지만, 위 분기로 충분해 거의 쓰지 않는다.
페이지를 떠날 때
확인 창: beforeunload
저장하지 않은 변경이 있을 때만 리스너를 붙이고, 저장하면 뗀다.
const confirmLeave = (event) => {
event.preventDefault() // 확인 창을 띄우는 표준 방법
event.returnValue = true // 옛 브라우저용
}
editor.addEventListener('input', () => window.addEventListener('beforeunload', confirmLeave))
saveButton.addEventListener('click', () => window.removeEventListener('beforeunload', confirmLeave))- 확인 창의 문구는 브라우저가 정한 일반 문구다. 과거에는 반환한 문자열을 보여 줬지만 남용 때문에 지금은 바꿀 수 없다.
- 사용자가 페이지와 한 번도 상호작용하지 않았으면(스티키 활성화가 없으면) 창이 뜨지 않는다.
beforeunload리스너가 있으면 일부 브라우저가 페이지를 bfcache에 넣지 않는다. 늘 붙여 두지 않는 이유다.onbeforeunload에서 문자열이나false를 반환하는 방식은 옛 방식이다.
마지막 데이터 보내기: visibilitychange
unload는 쓰지 않는다. 모바일에서는 앱을 전환한 뒤 앱 관리자로 브라우저를 닫으면 일어나지 않고, bfcache와 호환되지 않아 뒤로 가기를 느리게 만든다. 크롬은 기본으로 unload를 더는 실행하지 않는다.
| 대신 | 언제 | 쓰임 |
|---|---|---|
visibilitychange에서 document.visibilityState === 'hidden' | 탭 전환, 다른 앱으로 전환, 페이지 이동, 닫기 | 분석 데이터 전송, 상태 저장. 가장 확실하다 |
pagehide | 세션 기록의 다른 페이지로 갈 때. event.persisted면 bfcache에 들어간다 | 이동 감지. 모바일에서는 visibilitychange보다 덜 확실하다 |
pageshow | 페이지가 보일 때. event.persisted면 bfcache에서 복원됐다 | 복원 시 오래된 데이터 갱신 |
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
navigator.sendBeacon('/analytics', JSON.stringify(analyticsData))
}
})navigator.sendBeacon(url, data)는 페이지가 사라져도 브라우저가 백그라운드에서 POST로 끝까지 보낸다. 응답은 받을 수 없고, 보낼 수 있는 크기에 제한(보통 64KB 안팎)이 있다.- 헤더나 메서드를 정해야 하면
fetch(url, { method: 'POST', body, keepalive: true })를 쓴다.keepalive요청도 크기 제한이 같다. - 웹 바이탈 지표를 보고할 때도 같은 방식을 쓴다(웹 성능 지표와 Lighthouse). bfcache를 막는 원인 확인은 DevTools Application 패널에 있다.
숨은 페이지에서 일어나는 일
Page Visibility API(document.visibilityState, document.hidden, visibilitychange)는 페이지가 실제로 보이는지를 알려 준다. 탭을 바꾸거나 창을 최소화하거나 화면이 꺼지면 hidden이 된다. 창의 blur·focus는 다른 창으로 포커스가 갔다는 뜻일 뿐, 페이지가 가려졌다는 뜻이 아니다. <iframe>은 부모 문서의 상태를 따르고, CSS로 display: none해도 상태가 바뀌지 않는다.
브라우저는 숨은 페이지의 자원을 스스로 줄인다.
| 정책 | 내용 |
|---|---|
requestAnimationFrame | 숨은 탭·숨은 iframe에서는 콜백을 멈춘다 |
| 타이머 | 숨은 탭의 setTimeout·setInterval은 최소 1초 간격으로 묶인다. 예산 기반 제한도 있어 실행 시간만큼 예산이 줄고 초당 10ms씩 회복된다. Chrome은 5분 넘게 숨은 페이지의 연쇄 타이머를 1분에 한 번으로 더 줄인다 |
| 예외 | 소리를 내는 탭, WebSocket·WebRTC 연결이 있는 탭, IndexedDB 작업은 제한하지 않는다 |
그래서 숨은 동안의 일은 브라우저에 맡기지 말고 직접 멈춘다. 캐러셀 자동 넘김, 대시보드 폴링, 영상·효과음 재생은 hidden에서 멈추고 visible에서 다시 시작한다. 타이머가 늦게 도는 원리는 타이머에 있다.
document.addEventListener('visibilitychange', () => {
if (document.hidden) stopPolling()
else { refreshNow(); startPolling() }
})리소스의 load와 error
외부 src를 가진 요소(스크립트, 이미지, iframe, <link> 스타일시트 등)는 로딩 결과를 load·error 이벤트로 알린다.
const script = document.createElement('script')
script.src = 'https://cdn.example.com/lib.js'
script.onload = () => lib.init() // 받아서 **실행까지** 끝난 뒤
script.onerror = () => showFallback() // 404·네트워크 오류. 어떤 HTTP 오류인지는 알 수 없다
document.head.append(script)| 주의 | 내용 |
|---|---|
load·error는 로딩만 본다 | 스크립트가 받아졌으면 안에 실행 오류가 있어도 load다. 실행 오류는 window의 error 이벤트(window.onerror)가 받는다 |
이미지는 src를 넣는 순간 받는다 | 다른 리소스는 문서에 붙어야 받기 시작하지만, <img>는 문서 밖에서도 src가 생기면 바로 받는다. 미리 받아 두기에 쓰인다 |
<iframe>은 늘 load다 | 역사적인 이유로 페이지를 찾지 못해도 load가 일어난다 |
| 이미 끝났을 수 있다 | 캐시된 이미지는 리스너를 붙이기 전에 끝날 수 있다. img.complete를 먼저 보거나 await img.decode()를 쓴다 |
내 코드의 의존성이라면 동적 <script> 대신 동적 import()가 Promise로 결과를 주어 더 간단하다(모듈: export와 import).
다른 출처 스크립트의 오류 정보
같은 출처 정책 때문에 다른 출처(도메인·포트·프로토콜 중 하나라도 다름)에서 받은 스크립트의 실행 오류는 window.onerror에 "Script error."와 0:0만 온다. 메시지·줄 번호·스택이 숨겨진다. 오류 수집 서비스가 정보를 얻으려면 CORS로 허락받는다.
crossorigin 속성 | 요청 | 서버가 보내야 할 헤더 |
|---|---|---|
| 없음 | 일반 요청. 오류 상세는 숨겨진다 | 없음 |
anonymous | 쿠키·인증 정보 없이 CORS 요청 | Access-Control-Allow-Origin: * 또는 내 출처 |
use-credentials | 쿠키·인증 정보를 실어 CORS 요청 | Access-Control-Allow-Origin: 내 출처 + Access-Control-Allow-Credentials: true |
<script crossorigin="anonymous" src="https://cdn.example.com/app.js"></script>같은 속성은 다른 출처 이미지를 <canvas>에 그린 뒤 픽셀을 읽을 때, integrity(하위 리소스 무결성)를 검사할 때도 필요하다. CORS 자체는 AJAX와 HTTP 요청에 있다.
정리
- DOM을 다루는 초기화는
DOMContentLoaded(또는defer·모듈 스크립트)에서, 이미 지났을 수 있으면readyState로 분기한다. load는 모든 리소스를 기다리므로 꼭 필요할 때만 쓴다.- 떠나기 전 확인은 변경이 있을 때만
beforeunload+preventDefault(). - 마지막 전송은
unload가 아니라visibilitychange(hidden) +sendBeacon·keepalive. - 스크립트
onload는 실행 후,onerror는 로딩 실패만 알린다. 다른 출처 스크립트의 오류를 보려면crossorigin과 CORS 헤더가 필요하다.
관련 노트: 스크립트·스타일 로딩 순서와 점진적 렌더링, DOM 이벤트: 전파, 위임, 커스텀 이벤트, 웹 성능 지표와 Lighthouse, DevTools Application 패널, 모듈: export와 import, 이벤트 루프
원본 기록6
- 원문 (새 창) ko.javascript.info/onload-ondomcontentloaded
- 원문 (새 창) ko.javascript.info/onload-onerror
- 원문 (새 창) developer.mozilla.org/en-US/docs/Web/API/Window/unload_event
- 원문 (새 창) developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
- 원문 (새 창) developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event
- 원문 (새 창) developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API