[우테코] level-4 / 브라우저 지식 쌓기
요약
브라우저에 대한 궁금증을 해결하며 지식을 쌓아 나가는 공간입니다.
평소 지식으로만 쌓고 넘어갔던 부분들을 깊에 탐구해봅니다
목차
주제 : 브라우저가 HTML을 파싱하는 방식 (preload)
질문 1 ) html에서의 다운로드 순서 조절
✔️ Question ) 브라우저는 html은 위에서부터 아래로 차례로 파싱하는걸로 알고 있다. 그럼 preload 구문을 사용하지 않더라도 <link>의 순서를 조절하는 것 만으로도 다운로드 순서를 조절할 수 있는걸까?
✔️ Answer ) 아니요. ❌
<link>의 순서를 바꿔도 다운로드 순서를 직접 제어할 수는 없습니다.
브라우저는 HTML을 위에서부터 순차적으로 파싱하긴 하지만, 리소스를 발견하는 즉시 병렬 요청 큐에 넣고,
리소스 타입별 우선순위(CSS > JS > 폰트 > 이미지) 와 의존 관계를 기준으로 다운로드 시점을 스스로 조정합니다.
따라서 순서 변경만으로는 다운로드 순서를 제어할 수 없고,
rel=”preload” 만이 명시적으로 브라우저에게 “이 리소스를 먼저 받아라”라고 지시할 수 있습니다. ✅
질문 2 ) 병렬 요청 큐(리소스 우선순위 큐)란?
✔️ Question ) 리소스 발견 후 들어간다는 ‘병렬 요청 큐’가 정확히 어떤건가?
✔️ Answer ) 브라우저는 HTML 파싱 중 만난 외부 리소스들을 즉시 네트워크 요청 후보 등록(queue)에 등록합니다.
이 큐에는 각 리소스의 우선순위, 의존성, 속성(preload, async..)이 함께 저장됩니다.
실제 다운로드는 이 큐를 기반으로 브라우저의 네트워크 스레드가 병렬로 실행합니다.
즉, HTML 파싱은 단일 스레드, 리소스 다운로드는 병렬로 이루어집니다.
preload를 사용하면 이 큐에서 해당 리소스가 상위 우선순위로 이동하므로 HTML 파싱 도중 즉시 다운로드를 시작하게 됩니다.
질문 3 ) 리소스 우선순위 큐에 들어가면 바로 다운로드 되는걸까?
✔️ Question ) 그럼 리소스 우선순위 큐에 들어가더라도 바로 다운로드 되는 리소스랑 그렇지 않는 리소스가 구분되는건가? 그렇다면 그 기준은 뭘까?
✔️ Answer ) 소스 우선순위 큐에 들어가도 바로 다운로드되지 않는 경우가 있습니다.
브라우저는 각 리소스의 ‘긴급도’를 평가한 뒤,
‘즉시 다운로드 할지’ VS ‘대기(pending)할지‘를 결정합니다.
-
기준 1 ) 리소스 타입 CSS나 blocking JS는 즉시 다운로드 되고,
폰트나 이미지처럼 의존성이 있는 리소스는 일단 대기 상태로 등록됩니다. -
기준 2) 의존 관계 여기서 의존성이란, 이 리소스가 실제로 언제 필요한지를 추론하는 것을 의미합니다
- 폰트 : CSS 파싱 완료 후에야 ‘필요’로 간주
- JS(defer) : HTML 파싱 완료 후 실행
-
네트워크 상태와 슬롯 제한 브라우저는 도메인 당 동시 연결 제한(HTTP/1.1 : 약 6개, HTTP/2는 수십-수백 개)을 관리합니다. 슬롯이 가득 차 있으면, 우선순위가 낮은 리소스는 큐 안에서 Pending상태로 남아있습니다. 이 상태에 있는 리소스들은 다운로드 요청이 ‘예약된 상태’이지, 실제 다운로드는 아직 시작되지 않습니다.
주제 : 브라우저의 병렬 작업 (defer, async)
질문 1 ) 브라우저는 어떻게 병렬 작업이 가능할까?
✔️Question ) 브라우저는 어떻게 HTML 파싱과 JS 파일 다운로드를 동시에 하는걸까? 자바스크립트는 싱글 스레드 아닌가?
Answer ) 자바스크립트 엔진은 ‘싱글 스레드’가 맞지만, 브라우저는 ‘멀티 스레드’입니다.
브라우저는 여러 개의 스레드를 사용해 작업을 분담합니다.
- 메인 스레드 (Main Thread): HTML 파싱, CSS 계산, DOM 생성, 그리고 JS 실행을 담당합니다.
- 네트워크 스레드 (Network Thread): JS 파일, 이미지, CSS 파일 등 리소스를 다운로드하는 일을 전담합니다.
defer나 async가 붙으면, JS 실행은 ‘메인 스레드’가 해야 하지만, JS 다운로드는 ‘네트워크 스레드’가 백그라운드에서 처리합니다. 덕분에 메인 스레드는 멈추지 않고 HTML 파싱을 계속할 수 있습니다
질문 2 ) script 태그가 html 파싱을 멈추는 이유
✔️ Question ) 그렇다면 왜 기본 <script> 태그는 HTML 파싱을 멈출까? (최적이 아닌 것 같은데)
Answer ) 자바스크립트가 HTML 문서를 동적으로 변경할 수 있기 때문입니다. (feat. document.write())
이는 안정성과 일관성을 보장하기 위한 설계입니다.
만약 브라우저가 HTML을 파싱하는 동시에 JS를 실행했는데, 그 JS가 document.write() 같은 명령어로 파싱 중인 DOM 구조를 바꿔버린다면?
전체 DOM이 엉망이 될 수 있습니다.
따라서 브라우저는 “일단 멈춰!”를 외치고, JS를 다운로드해서 실행까지 끝낸 뒤, 변경된 내용을 확인하고 나서야 안전하게 다음 HTML 파싱을 이어갑니다.
질문 3 ) defer와 async 속성은 파싱을 멈추지 않는 이유
✔️ Question ) 그렇게 위험한데, 왜 defer와 async는 파싱을 멈추지 않고도 안전한걸까?
Answer ) ‘개발자의 약속’을 기반으로 동작하기 때문입니다. 단, 두 속성의 약속은 성격이 다릅니다.
defer나 async 속성을 추가하는 행위는 브라우저에게 “이 스크립트는 기본 스크립트와 다르게 동작해도 괜찮아!“라고 말해주는 것과 같습니다.
-
defer의 약속: “파싱이 끝날 때까지 기다렸다가 실행할게요.”-
defer는 “DOM을 절대 건드리지 않겠다”는 뜻이 아닙니다. 오히려 “DOM을 수정할 거지만, HTML 파싱이 100% 완료될 때까지 참을성 있게 기다릴게요”라는 약속입니다.
-
DOM 트리가 완벽하게 구축된 후에 실행되므로,
document.getElementById('root')같은 코드를 사용해도 완벽하게 안전합니다. React 같은 메인 앱 로직에 사용되는 이유입니다.
-
-
async의 약속: “저는 이 페이지와 상관없이 독립적으로 돌게요.”
-
async는 “저는 이 페이지의 DOM과 의존성이 없는 독립적인 코드입니다”라는 약속입니다. (ex. 광고, 로그, 구글 애널리틱스)
-
그래서 다운로드가 완료되는 즉시, 파싱을 잠시 멈추고 실행됩니다.
-
질문 4 ) 만약 async 스크립트가 DOM을 조작한다면?
✔️ Question ) 만약 async 스크립트가 약속을 깨고 DOM을 건드리면 어떻게 될까?
Answer ) 브라우저가 막는 게 아니라, 스크립트가 스스로 에러를 내고 멈춥니다.
브라우저는 async 스크립트가 DOM을 건드리려 해도 일단 실행합니다.
하지만 async는 파싱 도중에 실행되므로, 스크립트가 접근하려는 요소(ex. <div id="footer">)가 아직 파싱되지 않았을 확률이 높습니다.
결국 스크립트는 null에 접근하려다 TypeError를 발생시키며 그 자리에서 멈춰버립니다.
이것이 async 스크립트가 DOM 의존성이 없어야만 하는 이유입니다.
Never miss a story from us, subscribe to our
newsletter