Skip to content
Published on

브라우저는 어떻게 동작하는가: URL 입력부터 픽셀 렌더링까지 — 프론트엔드 면접 필수 지식

Authors

소개

"브라우저 주소창에 URL을 입력하면 무슨 일이 일어나나요?"

이 질문은 프론트엔드 면접에서 가장 자주 등장하는 질문 중 하나이며, 브라우저의 동작 원리에 대한 깊은 이해를 확인하는 핵심 질문입니다. 단순히 "HTML을 받아서 화면에 보여준다"는 답변으로는 부족합니다.

이 글에서는 DNS 조회부터 TCP 연결, HTML/CSS 파싱, DOM/CSSOM 구축, 렌더 트리 생성, 레이아웃, 페인트, 합성, 그리고 이벤트 루프까지 브라우저 동작의 모든 단계를 체계적으로 다룹니다.


1. URL 입력부터 화면 표시까지 (면접 완벽 답변)

1.1 전체 흐름 요약

사용자: URL 입력 + Enter
    |
    v
1. URL 파싱 (프로토콜, 도메인, 경로 분리)
    |
    v
2. DNS 조회 (도메인 → IP 주소)
    |
    v
3. TCP 연결 (3-way handshake)
    |
    v
4. TLS 핸드셰이크 (HTTPS인 경우)
    |
    v
5. HTTP 요청/응답
    |
    v
6. HTML 파싱 → DOM 트리 구축
    |
    v
7. CSS 파싱 → CSSOM 트리 구축
    |
    v
8. DOM + CSSOM → 렌더 트리 생성
    |
    v
9. 레이아웃 (각 요소의 위치/크기 계산)
    |
    v
10. 페인트 (픽셀로 그리기)
    |
    v
11. 합성 (레이어 합치기, GPU)
    |
    v
화면에 표시!

2. 네트워크 단계

2.1 DNS 조회 (Domain Name System)

브라우저가 도메인 이름을 IP 주소로 변환하는 과정입니다.

조회 순서:
1. 브라우저 DNS 캐시 확인
2. OS DNS 캐시 확인
3. 로컬 hosts 파일 확인
4. DNS Resolver (ISP) 조회
5. Root DNS 서버
6. TLD DNS 서버 (.com, .kr)
7. 권한 있는 DNS 서버 (실제 IP 반환)
예시: www.example.com 조회
    Browser Cache → miss
    OS Cache → miss
    Router Cache → miss
    ISP DNS Resolver → miss
    Root Server".com은 여기에 물어봐"
    .com TLD Server"example.com은 여기에 물어봐"
    example.com NS"IP는 93.184.216.34"

최적화 기법:

  • dns-prefetch: 미리 DNS를 해석합니다
<link rel="dns-prefetch" href="//api.example.com" />
<link rel="preconnect" href="https://cdn.example.com" />

2.2 TCP 3-Way Handshake

Client                    Server
  |                         |
  |--- SYN (seq=x) ------->|
  |                         |
  |<-- SYN-ACK (seq=y, -----|
  |    ack=x+1)             |
  |                         |
  |--- ACK (ack=y+1) ----->|
  |                         |
  연결 완료!

2.3 TLS 핸드셰이크

HTTPS 연결 시 추가되는 암호화 과정입니다.

Client                           Server
  |                                |
  |-- ClientHello (TLS 버전,     -->|
  |   암호 스위트 목록)             |
  |                                |
  |<-- ServerHello, 인증서,      ---|
  |    서버 키 교환                 |
  |                                |
  |-- 클라이언트 키 교환,        -->|
  |   ChangeCipherSpec,            |
  |   Finished                     |
  |                                |
  |<-- ChangeCipherSpec,         ---|
  |    Finished                    |
  |                                |
  암호화된 통신 시작!

2.4 HTTP 요청/응답

GET / HTTP/2
Host: www.example.com
Accept: text/html,application/xhtml+xml
Accept-Encoding: gzip, br
Connection: keep-alive
HTTP/2 200 OK
Content-Type: text/html; charset=UTF-8
Content-Encoding: gzip
Cache-Control: max-age=3600
Content-Length: 12345

<!DOCTYPE html>
<html>...

3. HTML 파싱

3.1 토크나이저와 트리 구축

HTML 파서는 바이트 스트림을 토큰으로 변환하고, 토큰을 DOM 노드로 구축합니다.

바이트 → 문자 → 토큰 → 노드 → DOM 트리

예시 HTML:
<html>
  <head>
    <title>My Page</title>
  </head>
  <body>
    <h1>Hello</h1>
    <p>World</p>
  </body>
</html>

DOM 트리:
Document
  └─ html
       ├─ head
       │    └─ title
       │         └─ "My Page"
       └─ body
            ├─ h1
            │    └─ "Hello"
            └─ p
                 └─ "World"

3.2 스크립트 차단 (Parser Blocking)

기본적으로 script 태그를 만나면 HTML 파싱이 중단됩니다.

<!-- 파서 차단: HTML 파싱이 중단되고, 스크립트 다운로드 + 실행 후 재개 -->
<script src="app.js"></script>

<!-- defer: HTML 파싱과 병렬 다운로드, DOM 완성 후 실행 -->
<script defer src="app.js"></script>

<!-- async: HTML 파싱과 병렬 다운로드, 다운로드 완료 시 즉시 실행 -->
<script async src="analytics.js"></script>

defer vs async 비교:

일반 script:
HTML: ───▓▓▓▓▓▓▓▓▓▓|████|▓▓▓▓▓▓▓
JS:                  |다운|실행|

defer:
HTML: ───▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓|실행|
JS:      |████ 다운로드 ████|

async:
HTML: ───▓▓▓▓▓▓▓▓|실행|▓▓▓▓▓▓▓▓
JS:      |████ 다운|
  • defer: 순서 보장, DOMContentLoaded 전에 실행
  • async: 순서 보장 안됨, 다운로드 완료 즉시 실행

3.3 Preload Scanner

HTML 파서가 차단되어도 프리로드 스캐너는 계속 작동하여 리소스를 미리 발견합니다.

<!-- preload: 중요 리소스 사전 로딩 -->
<link rel="preload" href="critical.css" as="style" />
<link rel="preload" href="hero.webp" as="image" />
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin />

<!-- prefetch: 다음 페이지에 필요한 리소스 미리 가져오기 -->
<link rel="prefetch" href="/next-page.html" />

4. CSS 파싱

4.1 CSSOM 구축

CSS도 HTML과 유사한 과정을 거칩니다.

바이트 → 문자 → 토큰 → 노드 → CSSOM 트리

CSS:
body { font-size: 16px; }
h1 { color: blue; font-size: 2em; }
p { color: #333; }

CSSOM 트리:
body (font-size: 16px)
  ├─ h1 (color: blue, font-size: 32px)
  └─ p (color: #333)

4.2 CSS는 렌더 차단 리소스

CSS가 로드되기 전에는 렌더 트리를 구축할 수 없으므로 CSS는 렌더 차단 리소스입니다.

<!-- 렌더 차단: 이 CSS가 로드/파싱될 때까지 렌더링 불가 -->
<link rel="stylesheet" href="styles.css" />

<!-- 조건부: 인쇄용은 렌더 차단하지 않음 -->
<link rel="stylesheet" href="print.css" media="print" />

<!-- 조건부: 특정 뷰포트에서만 렌더 차단 -->
<link rel="stylesheet" href="mobile.css" media="(max-width: 768px)" />

4.3 Specificity (명시도) 계산

!important > 인라인 스타일 > ID > 클래스/속성/의사클래스 > 태그/의사요소

계산법: (a, b, c)
- a: ID 선택자 수
- b: 클래스, 속성, 의사클래스 수
- c: 태그, 의사요소 수

예시:
#header .nav a          (1, 1, 1)
.nav .item.active       (0, 3, 0)
nav ul li a             (0, 0, 4)
#main #content p.text   (2, 1, 1)

4.4 캐스케이드 규칙

우선순위 (높은 순):
1. !important가 붙은 사용자 에이전트 스타일
2. !important가 붙은 사용자 스타일
3. !important가 붙은 작성자 스타일
4. 작성자 스타일 (명시도 순)
5. 사용자 스타일
6. 사용자 에이전트 스타일 (브라우저 기본)

5. 렌더 트리

5.1 DOM + CSSOM = 렌더 트리

DOM 트리:               CSSOM:
html                    body { font: 16px }
├─ head                 h1 { color: blue }
│  └─ title             p { color: #333 }
├─ body                 .hidden { display: none }
│  ├─ h1
│  ├─ p
│  └─ div.hidden
│  └─ span (visibility: hidden)

렌더 트리 (보이는 요소만):
html
└─ body (font: 16px)
   ├─ h1 (color: blue)
   ├─ p (color: #333)
   └─ span (visibility: hidden) ← 포함! 공간 차지
   [div.hidden은 제외 - display: none]

5.2 display: none vs visibility: hidden

속성렌더 트리 포함공간 차지리플로우 발생
display: none아니오아니오추가/제거 시 발생
visibility: hidden아니오
opacity: 0아니오

5.3 렌더 트리 구축 과정

  1. DOM 트리의 루트부터 순회
  2. 보이지 않는 노드 건너뜀 (script, meta, display: none)
  3. 각 보이는 노드에 CSSOM 규칙 매칭
  4. 계산된 스타일과 함께 노드를 렌더 트리에 추가

6. 레이아웃 (Reflow)

6.1 Box Model

모든 요소는 Box Model로 표현됩니다.

┌─────────────────────────────────────┐
│              margin                 │
│  ┌───────────────────────────────┐  │
│  │           border              │  │
│  │  ┌───────────────────────┐    │  │
│  │  │       padding         │    │  │
│  │  │  ┌─────────────────┐  │    │  │
│  │  │  │    content       │  │    │  │
│  │  │    (width x height)│  │    │  │
│  │  │  └─────────────────┘  │    │  │
│  │  └───────────────────────┘    │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

box-sizing: content-box (기본)
  width = content width만
  실제 너비 = width + padding + border

box-sizing: border-box (권장)
  width = content + padding + border
  실제 너비 = width

6.2 Block vs Inline

Block 요소 (div, p, h1...):
┌──────────────────────────────┐
Block 1 (전체 너비 차지)├──────────────────────────────┤
Block 2├──────────────────────────────┤
Block 3└──────────────────────────────┘

Inline 요소 (span, a, strong...):
[inline1][inline2][inline3]
[inline4가 길어서 다음 줄로...]

Inline-block:
[inline-block1]  [inline-block2]
(블록처럼 너비/높이 설정 가능, 인라인처럼 나란히)

6.3 레이아웃 과정

  1. 루트에서 시작: 뷰포트 크기 결정
  2. 블록 레이아웃: 위에서 아래로 블록 배치
  3. 인라인 레이아웃: 왼쪽에서 오른쪽으로 인라인 배치
  4. Float 처리: 텍스트 흐름 조정
  5. 위치 계산: relative, absolute, fixed, sticky 처리
  6. 크기 확정: 모든 요소의 최종 위치와 크기 계산

7. 페인트와 합성

7.1 페인트 (Paint)

레이아웃이 완료되면 각 요소를 실제 픽셀로 그립니다.

페인트 순서 (z-order):
1. 배경 색상
2. 배경 이미지
3. 보더
4. 자식 요소
5. 아웃라인

페인트는 여러 레이어에서 수행됩니다.

7.2 레이어 생성 조건

다음 조건에서 새 합성 레이어가 생성됩니다:

  • transform: translate3d() 또는 translateZ()
  • will-change: transform 또는 will-change: opacity
  • position: fixed
  • CSS 애니메이션/트랜지션이 적용된 opacity, transform
  • 비디오, 캔버스 요소
  • filter 속성 사용
/* 합성 레이어 강제 생성 */
.layer {
  will-change: transform;
  /* 또는 */
  transform: translateZ(0);
}

7.3 합성 (Compositing)

GPU가 각 레이어를 최종적으로 합치는 단계입니다.

합성 단계:
1. 레이어 나누기 (Layer Tree 생성)
2. 각 레이어를 타일(Tile)로 분할
3. 타일을 래스터화 (GPU에서 비트맵으로 변환)
4. 드로 쿼드 생성 (화면 위치 정보)
5. 컴포지터 프레임 생성 (최종 합성)
6. GPU에서 화면에 표시

렌더링 경로 비교:
JS/CSS 변경
StyleLayoutPaintComposite  (전체 파이프라인)
StylePaintComposite           (레이아웃 스킵)
StyleComposite                   (가장 빠름!)

7.4 GPU 가속 속성

/* 합성만 일어나는 속성 (가장 빠름) */
.fast {
  transform: translate(10px, 20px);  /* 위치 이동 */
  transform: scale(1.2);            /* 크기 변경 */
  transform: rotate(45deg);         /* 회전 */
  opacity: 0.5;                     /* 투명도 */
}

/* 페인트 발생 (중간) */
.medium {
  color: red;
  background-color: blue;
  box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}

/* 레이아웃 발생 (가장 느림) */
.slow {
  width: 200px;
  height: 100px;
  margin: 10px;
  padding: 20px;
  font-size: 18px;
}

8. Critical Rendering Path 최적화

8.1 렌더 차단 리소스 제거

<!-- CSS: Critical CSS 인라인화 -->
<style>
  /* 첫 화면에 필요한 최소 CSS만 인라인 */
  body { margin: 0; font-family: system-ui; }
  .hero { background: #3b82f6; color: white; padding: 2rem; }
</style>

<!-- 나머지 CSS는 비동기 로딩 -->
<link rel="preload" href="styles.css" as="style"
      onload="this.onload=null;this.rel='stylesheet'" />
<noscript><link rel="stylesheet" href="styles.css" /></noscript>

<!-- JS: 파서 차단 방지 -->
<script defer src="app.js"></script>

8.2 리소스 힌트

<!-- DNS 사전 조회 -->
<link rel="dns-prefetch" href="//api.example.com" />

<!-- 사전 연결 (DNS + TCP + TLS) -->
<link rel="preconnect" href="https://fonts.googleapis.com" />

<!-- 중요 리소스 사전 로딩 -->
<link rel="preload" href="hero.webp" as="image" />
<link rel="preload" href="critical.js" as="script" />

<!-- 다음 네비게이션 리소스 미리 가져오기 -->
<link rel="prefetch" href="/about" />

<!-- 다음 페이지 전체 미리 렌더링 -->
<link rel="prerender" href="/likely-next-page" />

8.3 최적화 체크리스트

  1. CSS를 head에 배치: 렌더 차단을 최소화
  2. Critical CSS 인라인: 첫 화면에 필요한 CSS만
  3. JS에 defer/async 사용: 파서 차단 방지
  4. 리소스 압축: gzip/brotli 활용
  5. 이미지 최적화: WebP/AVIF, lazy loading, srcset
  6. 폰트 최적화: font-display: swap, preload
  7. HTTP/2 사용: 멀티플렉싱으로 병렬 요청

9. 리플로우 vs 리페인트

9.1 리플로우 (Reflow / Layout)

레이아웃을 재계산하는 비용이 큰 작업입니다.

리플로우 트리거:

  • 요소 추가/제거
  • 요소 크기 변경 (width, height, padding, margin, border)
  • 폰트 크기 변경
  • 텍스트 내용 변경
  • 윈도우 리사이즈
  • 계산된 스타일 읽기 (offsetWidth, getComputedStyle 등)

9.2 리페인트 (Repaint)

시각적 속성만 변경되는 상대적으로 저렴한 작업입니다.

리페인트만 트리거 (리플로우 없음):

  • color, background-color
  • visibility
  • box-shadow
  • outline

9.3 DOM 읽기/쓰기 배칭

// 나쁜 예: 읽기-쓰기가 교차하여 강제 리플로우 발생
const items = document.querySelectorAll('.item');
items.forEach(item => {
  const width = item.offsetWidth;  // 읽기 → 강제 리플로우!
  item.style.width = width + 10 + 'px';  // 쓰기
});

// 좋은 예: 읽기를 먼저, 쓰기를 나중에 (배칭)
const widths = [];
items.forEach(item => {
  widths.push(item.offsetWidth);  // 모든 읽기를 먼저
});
items.forEach((item, i) => {
  item.style.width = widths[i] + 10 + 'px';  // 모든 쓰기를 나중에
});

9.4 Virtual DOM의 존재 이유

직접 DOM 조작:
변경 1 → 리플로우 → 리페인트
변경 2 → 리플로우 → 리페인트
변경 3 → 리플로우 → 리페인트

Virtual DOM (React):
변경 1, 2, 3을 메모리에서 계산 (diff)
→ 최소한의 실제 DOM 변경
→ 리플로우 1회 → 리페인트 1

10. 이벤트 루프와 태스크 큐

10.1 JavaScript 런타임 구조

┌─────────────────────────┐
Call Stack  (함수 실행 컨텍스트)└───────────┬─────────────┘
┌───────────▼─────────────┐
Event Loop  (스택이 비면 큐 확인)└───┬─────────────────┬───┘
    │                 │
┌───▼───┐       ┌────▼────┐
│Micro  │       │ Macro│Task   │       │ Task│Queue  │       │ Queue└───────┘       └─────────┘

10.2 Macrotask vs Microtask

console.log('1: 동기');

setTimeout(() => {
  console.log('2: Macrotask (setTimeout)');
}, 0);

Promise.resolve().then(() => {
  console.log('3: Microtask (Promise)');
});

queueMicrotask(() => {
  console.log('4: Microtask (queueMicrotask)');
});

console.log('5: 동기');

// 출력 순서:
// 1: 동기
// 5: 동기
// 3: Microtask (Promise)
// 4: Microtask (queueMicrotask)
// 2: Macrotask (setTimeout)

Macrotask (Task Queue):

  • setTimeout, setInterval
  • I/O 콜백
  • UI 렌더링
  • requestAnimationFrame

Microtask (Microtask Queue):

  • Promise.then/catch/finally
  • queueMicrotask
  • MutationObserver

10.3 이벤트 루프 실행 순서

1. Call Stack의 모든 동기 코드 실행
2. Call Stack이 비면:
   a. Microtask Queue의 모든 태스크 실행 (큐가 빌 때까지)
   b. 렌더링이 필요하면 렌더링 수행
   c. Macrotask Queue에서 하나의 태스크를 가져와 실행
3. 2번으로 돌아가 반복

10.4 requestAnimationFrame (rAF)

// rAF: 다음 리페인트 직전에 콜백 실행
// 보통 60fps = 약 16.67ms 간격

function animate() {
  // 애니메이션 로직
  element.style.transform = `translateX(${position}px)`;
  position += 2;

  if (position < 500) {
    requestAnimationFrame(animate);
  }
}

requestAnimationFrame(animate);

rAF vs setTimeout:

// 나쁜 예: setTimeout으로 애니메이션
// 프레임 누락, 불안정한 간격
setInterval(() => {
  moveElement();
}, 16);

// 좋은 예: rAF 사용
// 브라우저 리페인트와 동기화, 부드러운 60fps
function animate() {
  moveElement();
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

10.5 requestIdleCallback

// 브라우저가 유휴 상태일 때 실행
// 중요하지 않은 작업에 사용

requestIdleCallback((deadline) => {
  // deadline.timeRemaining()으로 남은 시간 확인
  while (deadline.timeRemaining() > 0 && tasks.length > 0) {
    performTask(tasks.shift());
  }

  if (tasks.length > 0) {
    requestIdleCallback(processRemainingTasks);
  }
});

11. Web Workers와 OffscreenCanvas

11.1 Web Worker

메인 스레드를 차단하지 않고 백그라운드에서 JavaScript를 실행합니다.

// main.js
const worker = new Worker('worker.js');

worker.postMessage({ type: 'HEAVY_CALC', data: bigArray });

worker.onmessage = (event) => {
  console.log('결과:', event.data.result);
};
// worker.js
self.onmessage = (event) => {
  if (event.data.type === 'HEAVY_CALC') {
    const result = heavyComputation(event.data.data);
    self.postMessage({ result });
  }
};

function heavyComputation(data) {
  // CPU 집약적 작업 (정렬, 암호화, 이미지 처리 등)
  return data.sort((a, b) => a - b);
}

11.2 Worker 종류

// 1. Dedicated Worker: 1:1 관계
const dedicated = new Worker('worker.js');

// 2. Shared Worker: 여러 탭/iframe에서 공유
const shared = new SharedWorker('shared.js');
shared.port.start();
shared.port.postMessage('hello');

// 3. Service Worker: 네트워크 프록시, 오프라인 지원
navigator.serviceWorker.register('/sw.js');

11.3 OffscreenCanvas

Worker에서 캔버스 렌더링을 수행합니다.

// main.js
const canvas = document.getElementById('canvas');
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('render-worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);
// render-worker.js
self.onmessage = (event) => {
  const canvas = event.data.canvas;
  const ctx = canvas.getContext('2d');

  function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    // 복잡한 렌더링 로직...
    ctx.fillStyle = '#3b82f6';
    ctx.fillRect(x, y, 50, 50);
    requestAnimationFrame(draw);
  }
  draw();
};

12. DevTools Performance 패널 가이드

12.1 Performance 탭 사용법

1. DevTools 열기 (F12 또는 Cmd+Option+I)
2. Performance 탭 선택
3. 녹화 시작 (Ctrl+E)
4. 페이지 조작
5. 녹화 중지

결과 해석:
- FPS 차트: 녹색이 60fps, 빨간색이 프레임 드롭
- CPU 차트: 색상별 작업 종류
  - 노란색: JavaScript 실행
  - 보라색: 레이아웃 (리플로우)
  - 녹색: 페인트
  - 회색: 기타

12.2 주요 메트릭

Core Web Vitals:
- LCP (Largest Contentful Paint): 2.5초 이내 → 좋음
- INP (Interaction to Next Paint): 200ms 이내 → 좋음
- CLS (Cumulative Layout Shift): 0.1 이하 → 좋음

추가 메트릭:
- FCP (First Contentful Paint): 첫 콘텐츠 표시
- TTFB (Time to First Byte): 서버 응답 시간
- TBT (Total Blocking Time): 메인 스레드 차단 시간

12.3 일반적인 성능 문제 패턴

1.태스크 (Long Task):
   - 50ms 이상 메인 스레드 차단
   - 해결: 코드 분할, Web Worker 활용

2. 레이아웃 트래싱 (Layout Thrashing):
   - 읽기/쓰기 교차로 강제 리플로우 반복
   - 해결: 읽기/쓰기 배칭

3. 과도한 페인트:
   - 불필요한 영역 재페인트
   - 해결: will-change, transform 사용

4.DOM 트리:
   - 1500+ 노드는 성능 저하
   - 해결: 가상화 (react-virtualized, tanstack-virtual)

13. 면접 질문 및 퀴즈

면접 질문 10선

Q1: 브라우저 주소창에 URL을 입력하면 어떤 일이 일어나나요?

URL 파싱 후 DNS 조회로 IP를 얻고, TCP 3-way handshake로 연결합니다. HTTPS라면 TLS 핸드셰이크도 수행합니다. HTTP 요청으로 HTML을 받으면 파서가 DOM 트리를 구축하고, CSS를 파싱하여 CSSOM을 만듭니다. DOM과 CSSOM을 결합하여 렌더 트리를 생성하고, 레이아웃으로 위치/크기를 계산하고, 페인트로 픽셀을 그리고, 합성으로 GPU에서 최종 화면을 표시합니다.

Q2: Critical Rendering Path를 설명하고 최적화 방법은?

CRP는 HTML 수신부터 첫 화면 렌더링까지의 경로입니다. CSS는 렌더 차단 리소스이므로 Critical CSS를 인라인하고, JS는 defer/async로 파서 차단을 방지합니다. preload로 중요 리소스를 미리 로드하고, 불필요한 CSS/JS를 제거합니다.

Q3: 리플로우와 리페인트의 차이는?

리플로우는 레이아웃 재계산(위치, 크기 변경)으로 비용이 큽니다. 리페인트는 시각적 속성(색상, 그림자)만 변경하는 상대적으로 저렴한 작업입니다. 리플로우는 항상 리페인트를 동반하지만, 리페인트는 리플로우 없이 발생할 수 있습니다.

Q4: display: none과 visibility: hidden의 차이는?

display: none은 렌더 트리에서 완전히 제거되어 공간을 차지하지 않습니다. 토글 시 리플로우가 발생합니다. visibility: hidden은 렌더 트리에 남아 공간을 차지하며, 리페인트만 발생합니다.

Q5: 이벤트 루프에서 Microtask와 Macrotask의 실행 순서는?

동기 코드 실행 후 Call Stack이 비면, Microtask Queue의 모든 태스크를 먼저 처리합니다. Microtask Queue가 비면 렌더링이 필요하면 수행하고, Macrotask Queue에서 하나의 태스크를 실행합니다. 이를 반복합니다.

Q6: requestAnimationFrame의 역할과 setTimeout과의 차이는?

rAF는 다음 리페인트 직전에 콜백을 실행하여 60fps 애니메이션을 보장합니다. setTimeout은 정확한 타이밍을 보장하지 않고, 브라우저 리프레시 레이트와 동기화되지 않아 프레임 누락이 발생할 수 있습니다.

Q7: defer와 async 스크립트의 차이는?

둘 다 HTML 파싱과 병렬로 스크립트를 다운로드합니다. defer는 DOM 완성 후 스크립트 순서대로 실행하며, DOMContentLoaded 전에 실행됩니다. async는 다운로드 완료 즉시 실행하며 순서를 보장하지 않습니다.

Q8: GPU 가속이 적용되는 CSS 속성은?

transform, opacity, filter가 대표적입니다. 이 속성들은 합성(Composite) 단계에서만 처리되어 레이아웃이나 페인트를 건너뜁니다. will-change로 브라우저에 힌트를 줄 수 있습니다.

Q9: Web Worker의 용도와 제한사항은?

메인 스레드를 차단하지 않고 CPU 집약적 작업(정렬, 암호화, 이미지 처리)을 수행합니다. 제한사항으로 DOM에 직접 접근할 수 없으며, 메인 스레드와 postMessage로만 통신합니다.

Q10: Virtual DOM이 필요한 이유를 렌더링 파이프라인 관점에서 설명하세요.

직접 DOM을 여러 번 조작하면 각 변경마다 리플로우/리페인트가 발생합니다. Virtual DOM은 메모리에서 변경 사항을 비교(diff)한 후 최소한의 실제 DOM 업데이트를 수행하여 리플로우 횟수를 줄입니다.

퀴즈 5문제

Q1: 다음 코드의 콘솔 출력 순서는?
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');

정답: A, D, C, B

동기 코드(A, D)가 먼저 실행되고, Microtask인 Promise(C)가 다음, Macrotask인 setTimeout(B)이 마지막입니다.

Q2: CSS에서 어떤 속성 변경이 합성(Composite)만 일어나는가?

정답: transformopacity입니다. 이 속성들은 GPU 합성 레이어에서 처리되어 Layout과 Paint 단계를 건너뜁니다. filter도 합성 레이어에서 처리됩니다.

Q3: preload와 prefetch의 차이는?

정답: preload는 현재 페이지에서 곧 필요한 중요 리소스를 높은 우선순위로 미리 로드합니다. prefetch는 다음 네비게이션에 필요할 것으로 예상되는 리소스를 낮은 우선순위로 미리 가져옵니다.

Q4: 레이아웃 트래싱(Layout Thrashing)이란?

정답: DOM 읽기(offsetWidth 등)와 쓰기(style 변경)를 교차하면, 브라우저가 정확한 값을 반환하기 위해 매번 강제 리플로우를 수행하는 현상입니다. 해결법은 읽기를 먼저 모아서 하고, 쓰기를 나중에 모아서 하는 배칭입니다.

Q5: display: none, visibility: hidden, opacity: 0의 차이를 렌더링 관점에서 설명하세요.

정답:

  • display: none: 렌더 트리에서 완전히 제거, 공간 없음, 토글 시 리플로우 발생
  • visibility: hidden: 렌더 트리에 존재, 공간 차지, 리페인트만 발생
  • opacity: 0: 렌더 트리에 존재, 공간 차지, 합성 레이어에서 처리 가능, 이벤트도 받음

참고 자료

  1. Google - How Browsers Work
  2. MDN - Critical Rendering Path
  3. Chrome - Inside Look at Modern Web Browser
  4. web.dev - Rendering Performance
  5. MDN - Event Loop
  6. web.dev - Optimize LCP
  7. Chrome - Compositing
  8. MDN - Web Workers API
  9. web.dev - requestAnimationFrame
  10. Chrome DevTools - Performance
  11. web.dev - Core Web Vitals
  12. Philip Roberts - Event Loop Talk
  13. CSS Triggers