웹 브라우저에 URL을 입력하고 엔터를 치면 어떤 일이 벌어질까?
이 글에서는 브라우저의 구성요소부터 실제 동작 과정까지 전체 흐름을 정리한다.
1. 브라우저의 구성요소
브라우저는 7개의 주요 구성요소로 이루어져 있다.
1.1 사용자 인터페이스 (User Interface)
주소 표시줄, 뒤로/앞으로 버튼, 북마크 메뉴 등 사용자가 직접 조작하는 모든 부분이다. 실제 웹페이지가 표시되는 영역을 제외한 나머지 모든 부분이라고 보면 된다.
1.2 브라우저 엔진 (Browser Engine)
사용자 인터페이스와 렌더링 엔진 사이의 동작을 제어하는 중개자 역할이다. 사용자의 요청을 렌더링 엔진에 전달하고, 렌더링 엔진의 상태를 UI에 반영한다.
이름에 "엔진"이 들어가지만 실제로는 MVC 패턴의 Controller와 유사한 역할을 한다.
1.3 렌더링 엔진 (Rendering Engine)
브라우저의 핵심이다. HTML, CSS를 파싱하고 화면에 표시하는 역할을 담당한다.
주요 렌더링 엔진:
- Chrome/Edge: Blink
- Safari: WebKit
- Firefox: Gecko
각 엔진마다 약간씩 동작 방식이 달라서, 같은 코드라도 브라우저마다 다르게 보일 수 있다.
1.4 네트워킹 (Networking)
HTTP, HTTPS 같은 프로토콜을 사용해서 네트워크 호출을 처리한다. 서버로부터 HTML, CSS, JavaScript, 이미지 등의 리소스를 가져오는 역할을 하며, 캐싱도 여기서 처리된다.
1.5 JavaScript 엔진 (JavaScript Engine)
JavaScript 코드를 해석하고 실행한다.
주요 JavaScript 엔진:
- Chrome/Edge: V8
- Safari: JavaScriptCore (Nitro)
- Firefox: SpiderMonkey
V8 엔진은 Node.js에서도 사용된다. (자세한 내용은 뒤에서 다룬다)
1.6 UI 백엔드 (UI Backend)
브라우저의 기본 위젯(버튼, 체크박스, 드롭다운 등)을 운영체제의 네이티브 UI를 사용해서 그리는 레이어다.
예를 들어 <select> 태그는:
- Windows에서는 Windows 스타일 드롭다운
- macOS에서는 macOS 스타일 드롭다운
같은 HTML 코드인데 OS마다 다르게 보이는 이유가 이것이다.
1.7 데이터 저장소 (Data Storage)
쿠키, LocalStorage, IndexedDB, 캐시 등 브라우저가 로컬에 데이터를 저장하는 계층이다. 최근에는 보안과 프라이버시를 위해 점점 더 정교해지고 있다.
2. 브라우저의 동작 과정
사용자가 주소창에 URL을 입력하고 엔터를 치면 다음과 같은 과정이 진행된다.
2.1 URL 파싱 및 DNS 조회
URL 파싱
브라우저는 먼저 입력된 URL을 파싱한다.
- 프로토콜 (http/https)
- 도메인 (example.com)
- 경로 (/page)
등을 분리한다.
DNS 조회
DNS(Domain Name System)를 통해 도메인 이름을 IP 주소로 변환한다.
- "google.com" → "142.250.196.142"
DNS 조회 순서:
- 브라우저 캐시 확인
- 최근 방문한 사이트는 이미 캐시에 있을 수 있다
- 운영체제 캐시 확인
- OS의 DNS 캐시나 hosts 파일 확인
- DNS Resolver 질의
- 통신사 DNS 또는 공개 DNS(Google 8.8.8.8, Cloudflare 1.1.1.1 등)에 질의
- 재귀적 질의 (Recursive Query)
- Root Server → TLD Server → Authoritative Server 순으로 질의
- 각 단계에서 "다음 단계 주소"를 알려줌 (Narrow Down)
- 최종적으로 실제 IP 주소를 얻음
예시: www.example.com 조회
Resolver: "www.example.com의 IP 주소는?"
1. Root Server
→ ".com은 이 TLD 서버가 관리해"
2. TLD Server (.com)
→ "example.com은 이 Authoritative 서버가 관리해"
3. Authoritative Server
→ "www.example.com의 IP는 93.184.216.34야!"
- 응답 전달 및 캐싱
- Resolver와 브라우저가 결과를 캐시
- TTL(Time To Live) 시간만큼 유효
2.2 TCP 연결 수립 (3-Way Handshake)
IP 주소를 알아냈으면 서버와 TCP 연결을 맺는다.
3-Way Handshake 과정:
클라이언트 서버
| |
| SYN (Seq=1000) |
|-------------------------->|
| |
| SYN-ACK (Seq=5000, Ack=1001)
|<--------------------------|
| |
| ACK (Seq=1001, Ack=5001) |
|-------------------------->|
| |
| === 연결 수립 완료 === |
단계별 설명:
- SYN: 클라이언트가 연결 요청
- Seq=1000 (초기 시퀀스 번호)
- SYN-ACK: 서버가 연결 승인
- Seq=5000 (서버의 초기 시퀀스 번호)
- Ack=1001 (클라이언트 Seq + 1)
- ACK: 클라이언트가 확인
- Seq=1001 (서버가 기대한 번호)
- Ack=5001 (서버 Seq + 1)
왜 +1을 할까?
Ack 번호는 "다음에 받고 싶은 바이트 번호"를 의미한다.
- "1000번 받았으니 다음은 1001번 보내줘"
- 명확하고 효율적인 통신을 위한 설계
HTTPS의 경우:
TCP 연결 후 TLS/SSL Handshake가 추가로 진행되어 암호화된 연결을 만든다.
2.3 HTTP 요청/응답
요청:
- 메서드 (GET, POST 등)
- 헤더 (Cookie, User-Agent 등)
- 바디 (POST의 경우)
응답:
- 상태 코드 (200, 404, 500 등)
- 헤더
- 실제 콘텐츠 (HTML 문서)
2.4 렌더링 과정
서버로부터 HTML을 받으면 본격적인 렌더링이 시작된다.
a) HTML 파싱 → DOM 트리 생성
렌더링 엔진이 HTML을 위에서 아래로 읽으면서 파싱한다. 이 과정에서 DOM(Document Object Model) 트리를 만든다.
예시:
<html>
<body>
<div class="container">
<h1>Welcome</h1>
<p>Hello</p>
</div>
</body>
</html>
DOM 트리:
Document
└─ html
└─ body
└─ div (class="container")
├─ h1
│ └─ #text: "Welcome"
└─ p
└─ #text: "Hello"
DOM은 JavaScript 객체로 표현된다:
// div 요소의 실제 객체 구조
{
nodeType: 1, // ELEMENT_NODE
nodeName: "DIV",
className: "container",
children: [h1, p],
parentNode: body,
textContent: "Welcome Hello"
}
b) CSS 파싱 → CSSOM 트리 생성
HTML 파싱 중 <link>나 <style> 태그를 만나면 CSS를 파싱한다. 이때 CSSOM(CSS Object Model) 트리를 만든다.
예시:
body { font-size: 16px; }
.container { padding: 20px; }
h1 { color: blue; }
CSSOM 트리:
StyleSheet
└─ Rules
├─ body { font-size: 16px }
├─ .container { padding: 20px }
└─ h1 { color: blue }
c) JavaScript 실행 (Render-Blocking)
HTML 파싱 중 <script> 태그를 만나면 파싱이 중단되고 JavaScript를 다운로드하고 실행한다.
왜 파싱을 멈출까?
<body>
<div id="box">Hello</div>
<script>
document.getElementById('box').style.color = 'red';
</script>
<p>World</p>
</body>
만약 파싱을 계속한다면:
<script>와<p>태그를 동시에 파싱- JavaScript가 실행될 때
<p>가 이미 파싱됨 - JavaScript가 DOM을 변경하면 예상과 다른 결과 발생
따라서 파싱을 멈추고 순차적으로 처리:
<div>파싱<script>만남 → 파싱 중단- JavaScript 실행 (위쪽 DOM만 접근 가능)
- JavaScript 완료 → 파싱 재개
<p>파싱
타임라인:
HTML 파싱 ━━━ ❌중단 ━━━ HTML 파싱
↓
JS 다운로드
↓
JS 실행
성능 문제:
<head>
<script src="jquery.js"></script> <!-- 100KB, 200ms -->
<script src="app.js"></script> <!-- 200KB, 400ms -->
</head>
<body>
<h1>Title</h1>
</body>
사용자는 600ms 동안 빈 화면을 본다. 😢
해결 방법:
1) defer 속성 사용 (추천)
<head>
<script src="app.js" defer></script>
</head>
- 파싱 멈추지 않음
- 다운로드는 병렬로 진행
- DOM 완성 후 실행 (모든 DOM 접근 가능)
HTML 파싱 ━━━━━━━━━━━━━━━ 완료!
┃ ↓
┃ JS 다운로드 ━━━━━━━ JS 실행
2) body 끝에 배치
<body>
<h1>Title</h1>
<p>Content</p>
<script src="app.js"></script>
</body>
- 간단하고 안전함
- HTML이 먼저 파싱되어 화면 빨리 표시
- 다운로드 시작이 늦음 (defer보다 느림)
3) async 속성 (독립적인 스크립트)
<script src="analytics.js" async></script>
- 파싱 멈추지 않음
- 다운로드 완료 시 즉시 실행
- DOM 준비 보장 안 됨
- Google Analytics, 광고 스크립트 등에 적합
d) 렌더 트리 구성
DOM 트리와 CSSOM 트리를 결합해서 렌더 트리(Render Tree)를 만든다.
렌더 트리는 화면에 실제로 표시될 요소들만 포함한다.
제외되는 요소:
<head>,<script>,<meta>등display: none스타일이 적용된 요소
포함되는 요소:
visibility: hidden(공간은 차지함)
렌더 트리 예시:
RenderBody
└─ RenderDiv (class="container")
├─ RenderH1
│ ├─ color: blue
│ └─ Text: "Welcome"
└─ RenderP
└─ Text: "Hello"
각 노드에는 최종 계산된 스타일이 포함된다.
e) 레이아웃 (Layout / Reflow)
렌더 트리의 각 노드가 화면의 어느 위치에, 어떤 크기로 표시될지 계산한다.
계산하는 것:
- 정확한 위치 (x, y 좌표)
- 크기 (width, height)
- 마진, 패딩
- 박스 모델
뷰포트 기준으로 계산:
뷰포트: 1920x1080
div { width: 50% }
→ 960px로 계산
h1 { font-size: 2em }
→ 부모 font-size 기준으로 계산
f) 페인팅 (Painting)
레이아웃이 완료되면 실제로 픽셀을 화면에 그린다.
각 노드를 화면의 실제 픽셀로 변환하는 과정이다.
여러 레이어로 나뉘어 그려질 수 있다:
"레이어"는 포토샵의 레이어처럼 겹쳐진 층을 의미한다.
레이어 3 (상단) [position: fixed 헤더]
레이어 2 [z-index가 높은 팝업]
레이어 1 (하단) [본문 내용]
↓ 합치기
[최종 화면]
왜 레이어를 나눌까?
성능 최적화를 위해서다.
스크롤 시:
- 레이어 분리 안 하면: 전체 다시 그리기 😢
- 레이어 분리하면: 움직이는 레이어만 변경 🚀
레이어가 생성되는 경우:
.element {
position: fixed; /* ✅ 새 레이어 */
transform: ...; /* ✅ 새 레이어 */
opacity: 0.5; /* ✅ 새 레이어 */
will-change: transform; /* ✅ 새 레이어 */
}
g) 합성 (Compositing)
여러 레이어를 올바른 순서로 합쳐서 최종 화면을 만든다.
페인팅:
레이어 1 → [픽셀로 그리기] → 텍스쳐 1
레이어 2 → [픽셀로 그리기] → 텍스쳐 2
레이어 3 → [픽셀로 그리기] → 텍스쳐 3
합성:
텍스쳐 1 위에
텍스쳐 2 얹고
텍스쳐 3 얹기
→ 최종 화면!
GPU를 활용해서 이 과정을 빠르게 처리할 수 있다.
2.5 추가 리소스 로딩
초기 HTML 렌더링 중 이미지, 폰트, 추가 CSS/JS 파일 등을 발견하면 네트워킹 컴포넌트가 이들을 병렬로 다운로드한다.
다운로드가 완료되면:
- 필요한 경우 렌더 트리 업데이트
- 레이아웃과 페인팅 다시 수행
2.6 JavaScript 상호작용
페이지 로드 후에도 JavaScript가 DOM을 조작하거나 스타일을 변경하면:
- 해당 부분의 레이아웃 재계산 (Reflow)
- 페인팅 다시 수행 (Repaint)
성능 최적화가 중요한 이유:
// 나쁜 예: 레이아웃 변경
element.style.width = '300px'; // Reflow 발생 😢
// 좋은 예: transform 사용
element.style.transform = 'scale(1.5)'; // 레이어만 변경 🚀
3. V8 엔진과 Node.js
앞서 JavaScript 엔진으로 V8을 언급했다. V8은 단순한 JavaScript 실행 엔진을 넘어 현대 웹 개발 생태계의 핵심이 되었다.
3.1 JavaScript의 역사적 배경
JavaScript는 원래 브라우저에서만 실행되는 언어였다.
- 1995년 넷스케이프가 개발
- 웹페이지에 동적 기능 추가 목적
- 파일 시스템 접근 불가, 서버 만들기 불가
전통적인 웹 개발:
- 프론트엔드: JavaScript
- 백엔드: PHP, Java, Python, Ruby 등
두 가지 언어를 배워야 했다.
3.2 V8 엔진이란?
V8은 Google이 Chrome을 위해 2008년에 개발한 JavaScript 엔진이다.
JavaScript 엔진이 하는 일:
- 파싱(Parsing): 코드를 읽어서 이해
- 컴파일(Compilation): 기계어로 변환
- 실행(Execution): 실제로 코드 실행
V8의 혁신: JIT 컴파일
전통적인 JavaScript 엔진은 코드를 한 줄씩 해석하며 실행했다 (인터프리터 방식). 느렸다.
V8은 JIT(Just-In-Time) 컴파일을 사용한다:
JavaScript 코드
↓
파싱 → AST
↓
바이트코드 생성
↓
실행하며 프로파일링
↓
자주 실행되는 코드 발견 (핫 코드)
↓
최적화된 기계어로 컴파일
↓
빠른 실행!
왜 빠른가:
- 인터프리터처럼 빠르게 시작
- 컴파일러처럼 빠르게 실행
- 실행 패턴을 보고 적응적으로 최적화
V8 덕분에 Chrome이 빠르게 동작하게 되었고, Chrome의 인기에 큰 역할을 했다.
3.3 Node.js의 탄생
2009년, Ryan Dahl이 생각했다:
"V8 엔진이 이렇게 빠른데, 브라우저 밖에서도 쓸 수 있지 않을까?"
그래서 V8 엔진을 브라우저에서 꺼내 독립적으로 실행할 수 있게 만든 것이 Node.js다.
Node.js 구조:
Node.js
├── V8 엔진 (JavaScript 실행)
├── libuv (비동기 I/O, 이벤트 루프)
├── 기타 C/C++ 라이브러리
└── Node.js API (fs, http, path 등)
V8 엔진:
- JavaScript 실행의 핵심
- Chrome과 동일한 엔진
libuv:
- 파일 시스템, 네트워크 등 I/O 처리
- 이벤트 루프 제공
- C로 작성됨
Node.js API:
- JavaScript로 파일 읽기/쓰기
- HTTP 서버 만들기
- 데이터베이스 연결 등
'Learning Log' 카테고리의 다른 글
| [멋사 클라우드 5기] Day 18 - JavaScript 호이스팅 (0) | 2026.02.15 |
|---|---|
| [멋사 클라우드 5기] Day 17 - Flexbox (0) | 2026.02.13 |
| [멋사 클라우드 5기] Day 15 - MVC & JDBC 구조 톺아보기 (0) | 2026.02.11 |
| [멋사 클라우드 5기] Day 14 - Modeling, JDBC 이해하기 (1) | 2026.02.09 |
| [멋사 클라우드 5기] Day 13 - DML, DDL, DCL, TCL (0) | 2026.02.08 |
