스크롤로 따라가는 전체 흐름
전원이 들어오는 순간부터 화면에 픽셀이 찍히기까지, 아래로 스크롤하며 24단계를 순서대로 따라가 보세요.
Power ON
전원 공급
전원 공급 장치(PSU)가 각 부품에 안정적인 전압을 공급하고, 전압이 준비되면 CPU가 깨어나 첫 명령어를 실행한다.
Power ON
'Power ON'은 단순히 버튼을 누르는 순간이 아니라, ATX 규격에 따라 전원이 안정화되고 CPU가 첫 명령을 실행할 수 있는 상태에 도달하기까지의 정밀한 시퀀스다. 전원 버튼을 누르면 메인보드가 PSU에 PS_ON# 신호(액티브 로우)를 보내고, PSU는 220V/110V 교류(AC)를 정류·강압하여 +12V, +5V, +3.3V, -12V, +5VSB(스탠바이) 등의 직류(DC) 레일을 생성한다. 모든 레일이 규격 허용 오차 내로 안정되면 PSU는 최소 100ms 이상 대기한 뒤 Power Good(PWR_OK) 신호를 어서트하는데, 이 신호가 뜨기 전까지 메인보드는 CPU를 리셋 상태로 붙잡아 둔다. PWR_OK가 어서트되면 클럭 제너레이터가 안정된 기준 클럭을 공급하기 시작하고, 리셋 라인이 해제되면서 CPU는 하드웨어에 고정된 리셋 벡터(reset vector) 주소에서 첫 명령을 페치한다. 이 모든 과정의 목적은 '전기적으로 불안정한 순간에 연산이 시작되어 시스템이 오동작하는 것'을 원천 차단하는 것이다.
- 전원 버튼 → 메인보드가 PS_ON#(액티브 로우)을 PSU에 보냄 → PSU가 메인 레일 출력 시작
- PSU는 AC를 정류·강압해 +12V(모터·CPU·GPU), +5V(로직·USB), +3.3V(메모리·칩셋), +5VSB(대기전력) 레일 생성
- Power Good(PWR_OK)은 +5V 로직 하이 신호로, 모든 레일이 규격 내로 안정되고 100ms 이상 경과해야 어서트됨
- PWR_OK 어서트 전까지 메인보드가 CPU를 리셋 상태로 유지 → 불안정 전압에서의 연산 시작 방지
- 클럭 제너레이터(수정 발진자 기반)가 안정 클럭을 공급하고, 리셋 해제 시 CPU는 리셋 벡터에서 첫 명령 페치
- CMOS/RTC는 코인형 배터리(CR2032)로 전원 차단 시에도 시각·BIOS 설정을 유지
- AC 상실 시 PWR_OK는 레일이 규격을 벗어나기 최소 1ms 전에 디어서트되어 CPU를 안전하게 정지
CPU
CPU(중앙처리장치)는 명령어를 인출(fetch)·해석(decode)·실행(execute)하는 폰 노이만 구조의 심장으로, 그 동작은 전적으로 내부 '레지스터(register)'라는 초고속 저장소들 사이의 데이터 이동으로 이루어진다. 페치 단계에서 프로그램 카운터(PC)가 가리키는 주소가 MAR(메모리 주소 레지스터)로 복사되어 주소 버스로 나가고, 해당 명령어가 데이터 버스를 통해 MDR(메모리 데이터 레지스터)로 들어온 뒤 명령어 레지스터(IR)에 적재되며 PC는 다음 명령어를 가리키도록 증가한다. 디코드 단계에서 제어장치(Control Unit)의 명령어 디코더가 IR의 비트필드를 해석해 '어떤 연산을, 어떤 레지스터·피연산자로' 수행할지 제어 신호(control signal)로 변환한다. 실행 단계에서는 ALU(산술논리장치)가 실제 덧셈·비교·논리연산을 수행하고 그 결과는 누산기(Accumulator)나 범용 레지스터에 저장되며, 연산의 부수 결과(제로·캐리·오버플로·부호)는 상태/플래그 레지스터(FLAGS)에 기록되어 조건 분기의 근거가 된다. 이 모든 흐름은 클럭에 맞춰 동기화되고, 자주 쓰는 데이터·명령어는 L1/L2 캐시가 붙잡아 두며, 현대 CPU는 파이프라인으로 여러 명령을 겹쳐 처리해 처리량을 극대화한다.
- PC(프로그램 카운터)는 다음에 실행할 명령어의 주소를 보관하고, 명령 인출 후 자동 증가함
- MAR→주소 버스로 주소 송출, MDR←데이터 버스로 명령/데이터 수신 — 메모리와의 유일한 창구
- IR(명령어 레지스터)에 담긴 명령을 제어장치의 디코더가 해석해 제어 신호를 생성
- ALU가 산술·논리 연산을 수행하고 결과는 누산기/범용 레지스터에, 부수 결과는 FLAGS에 기록
- FLAGS(상태 레지스터)의 Zero/Carry/Overflow/Sign 비트가 조건 분기(if, 반복)의 판단 근거
- 레지스터가 메모리 계층의 최상단(1클럭)이며, L1(명령/데이터 분리)·L2 캐시가 그 아래에서 지연을 은폐
- 파이프라인(Fetch/Decode/Execute/Memory/Write-back)으로 명령을 겹쳐 처리해 처리량 향상, 슈퍼스칼라는 병렬 실행
Memory / Cache
메모리는 두 축으로 이해해야 한다. 첫째는 '속도-용량-비용'의 트레이드오프로 배열된 물리적 메모리 계층(memory hierarchy)이고, 둘째는 하나의 프로세스가 바라보는 논리적 가상 메모리 레이아웃(virtual memory layout)이다. 계층은 위에서부터 레지스터(1클럭)→L1 캐시(~1ns)→L2(~4ns)→L3(공유, ~10~40ns)→RAM(~100ns)→SSD(수십 µs)→HDD(수 ms) 순으로, 위로 갈수록 빠르지만 비싸고 작으며, 자주 쓰는 데이터를 상위 계층이 붙잡아 지역성(locality)을 활용해 평균 접근 시간을 줄인다. 한편 각 프로세스는 자신만의 연속된 가상 주소 공간을 갖는데, 낮은 주소부터 코드(Text)·데이터(Data)·BSS·힙(Heap, 위로 성장)·메모리 매핑 영역·스택(Stack, 아래로 성장)이 배치되고, 상위 절반은 커널 영역(kernel space)으로 사용자 코드가 접근할 수 없다. 이 가상 주소를 실제 물리 주소로 바꾸는 하드웨어가 MMU(메모리 관리 장치)이며, 페이지 테이블(page table)을 참조해 페이징(paging)을 수행하고, 최근 변환을 TLB(변환 색인 버퍼)에 캐싱해 매번 페이지 테이블을 뒤지는 비용을 줄인다. 이 구조 덕분에 프로세스 간 메모리 격리, 물리 메모리보다 큰 주소 공간, 요구 페이징(demand paging)이 가능해진다.
- 메모리 계층: 레지스터→L1→L2→L3(공유)→RAM→SSD→HDD, 위로 갈수록 빠르고 비싸고 작음
- 지역성(temporal/spatial locality)을 이용해 상위 캐시가 하위 계층 접근을 은폐, 평균 접근 시간 단축
- 프로세스 가상 메모리: Text→Data→BSS→Heap(↑)→mmap→Stack(↓) 순으로 배치
- Heap은 낮은 주소에서 위로, Stack은 높은 주소에서 아래로 성장 — 가운데 빈 공간을 두고 서로를 향해 자람
- BSS는 0으로 초기화되는 전역/정적 변수 영역으로, 실행 파일에는 크기 정보만 담겨 용량을 절약
- 사용자 영역(user space)/커널 영역(kernel space)을 분리해 커널 메모리를 보호
- MMU가 페이지 테이블로 가상→물리 주소를 변환하고, TLB가 최근 변환을 캐싱해 속도를 확보
OS Kernel
커널(Kernel)은 운영체제의 핵심으로, 부팅 후 항상 메모리에 상주하며 하드웨어와 응용 프로그램 사이에서 자원을 중재하는 최상위 특권 소프트웨어다. CPU는 사용자 모드(user mode, ring 3)와 커널 모드(kernel mode, ring 0)라는 두 특권 수준을 하드웨어적으로 구분하며, 응용 프로그램은 파일 입출력이나 메모리 할당처럼 특권이 필요한 작업을 직접 수행할 수 없고 반드시 시스템 콜(system call)을 통해 커널에 요청해야 한다. 시스템 콜은 소프트웨어 인터럽트(trap)나 x86의 SYSCALL/SYSENTER 같은 전용 명령으로 모드 전환(mode switch)을 일으키며, 커널은 시스템 콜 번호를 시스템 콜 테이블에서 조회해 해당 핸들러로 디스패치한 뒤 권한을 검사하고 서비스를 수행한다. 커널은 프로세스 스케줄링, 가상 메모리 관리, 파일 시스템, 장치 드라이버, IPC, 인터럽트 처리 등 여러 서브시스템으로 구성되며, 이 서브시스템들을 어떤 주소 공간에 배치하느냐에 따라 모놀리식(monolithic)과 마이크로커널(microkernel)로 나뉜다. 모놀리식 커널은 모든 서비스를 단일 커널 주소 공간에서 실행해 함수 호출 수준의 빠른 성능을 얻지만 하나의 드라이버 버그가 시스템 전체를 무너뜨릴 수 있고, 마이크로커널은 IPC·기본 스케줄링·기본 메모리 관리만 커널에 두고 드라이버·파일 시스템을 사용자 공간 프로세스로 분리해 안정성과 모듈성을 얻는 대신 메시지 전달 오버헤드를 감수한다. Linux·전통적 Unix는 모놀리식, MINIX·QNX·seL4는 마이크로커널, Windows NT·macOS(XNU)는 두 방식을 절충한 하이브리드(hybrid) 커널이다.
- 커널은 항상 메모리에 상주하는 최상위 특권 소프트웨어로 하드웨어 자원을 중재한다
- CPU가 사용자 모드(ring 3)와 커널 모드(ring 0)를 하드웨어로 구분해 특권 명령을 보호한다
- 응용 프로그램은 시스템 콜로만 커널 서비스에 접근하며, trap/SYSCALL 명령으로 모드 전환이 일어난다
- 시스템 콜 번호를 시스템 콜 테이블에서 조회해 핸들러로 디스패치하고 권한을 검사한다
- 모놀리식 커널: 모든 서비스가 단일 커널 주소 공간 → 빠르지만 결함 전파 위험 (Linux, Unix)
- 마이크로커널: 최소 기능만 커널에, 나머지는 사용자 공간 → 안정·모듈성, IPC 오버헤드 (QNX, seL4)
- 하이브리드 커널은 둘의 절충안 (Windows NT, macOS XNU)
Process
프로세스(Process)는 실행 중인 프로그램의 인스턴스로, 디스크에 있는 수동적 파일(program)과 달리 프로그램 카운터·레지스터·스택 같은 실행 상태를 가진 능동적 실체다. 커널은 각 프로세스를 프로세스 제어 블록(PCB, Process Control Block; Linux에서는 task_struct)이라는 자료구조로 표현하며, 여기에 프로세스 상태·PID·CPU 레지스터·스케줄링 정보·메모리 관리 정보·입출력 상태·계정 정보가 저장된다. 각 프로세스는 독립된 가상 주소 공간(address space)을 가지며 이는 text(컴파일된 코드), data(전역·정적 변수), heap(malloc/free로 동적 할당), stack(지역 변수·함수 프레임)의 네 영역으로 구성되고, heap과 stack은 자유 공간의 양 끝에서 서로를 향해 자란다. 프로세스는 new→ready→running→waiting→terminated의 다섯 상태를 오가며, ready에서 running으로는 스케줄러의 dispatch로, running에서 waiting으로는 I/O 요청 등으로, running에서 ready로는 타임 슬라이스 만료(preemption)로 전이한다. Unix에서 새 프로세스는 fork() 시스템 콜로 부모(parent)를 복제해 자식(child)을 만드는데, fork는 자식에게 0을, 부모에게 자식의 PID를 반환하며 현대 커널은 성능을 위해 Copy-on-Write(COW)로 주소 공간 복사를 지연시킨다. 프로세스들은 서로 격리되어 있으므로 협력하려면 파이프·공유 메모리·메시지 큐·소켓·시그널 같은 IPC 메커니즘이 필요하다.
- 프로세스 = 실행 중인 프로그램 인스턴스(능동적), 프로그램 = 디스크의 수동적 파일
- 커널은 각 프로세스를 PCB(task_struct)로 관리하며 상태·PID·레지스터·메모리·I/O 정보를 담는다
- 주소 공간은 text/data/heap/stack 4영역이며 heap과 stack이 서로를 향해 자란다
- 5가지 상태: new → ready → running → waiting → terminated (dispatch, preemption, I/O로 전이)
- fork()는 부모를 복제해 자식 생성, 자식엔 0·부모엔 자식 PID 반환, COW로 복사 지연
- 프로세스는 서로 격리되어 있어 협력하려면 IPC가 필수다
- 컨텍스트 스위칭 시 PCB에 현재 상태를 저장하고 다음 프로세스 상태를 복원한다
Thread
스레드(Thread)는 프로세스 내부의 실행 흐름 단위로, CPU가 스케줄링하는 최소 단위다. 하나의 프로세스는 여러 스레드를 가질 수 있으며, 이 스레드들은 프로세스의 코드(text)·전역 데이터(data)·힙(heap)·열린 파일 디스크립터를 공유하는 대신, 각자 고유한 스택(stack)·레지스터·프로그램 카운터(PC)·스레드 로컬 저장소(TLS)를 가진다. 커널은 각 스레드를 스레드 제어 블록(TCB, Thread Control Block)으로 관리하며 여기에는 스레드 ID·상태·PC·레지스터·스택 포인터·우선순위·소속 PCB 포인터가 담긴다. 스레드는 구현 위치에 따라 커널이 모르는 사용자 수준 스레드(user-level thread)와 커널이 스케줄링하는 커널 수준 스레드(kernel-level thread)로 나뉘고, 이 둘의 매핑 관계에 따라 N:1(다대일), 1:1(일대일), M:N(다대다) 모델로 분류된다. 컨텍스트 스위칭(context switching)은 실행 중인 스레드의 문맥(레지스터·PC 등)을 그 TCB에 저장하고 다음 스레드의 문맥을 TCB에서 복원하는 과정인데, 같은 프로세스 내 스레드 간 전환은 가상 주소 공간이 바뀌지 않아 TLB 플러시를 피하므로 프로세스 간 전환보다 훨씬 저렴하다. 여러 스레드가 공유 자원에 동시에 접근하면 실행 순서에 따라 결과가 달라지는 경쟁 상태(race condition)가 발생하므로, 임계 구역(critical section)을 뮤텍스(mutex)·세마포어(semaphore) 같은 동기화 기법으로 보호해야 한다.
- 스레드는 프로세스 내 실행 흐름 단위이자 CPU 스케줄링의 최소 단위다
- 공유 자원: 코드(text)·데이터(data)·힙(heap)·열린 파일 / 스레드별 자원: 스택·레지스터·PC·TLS
- 커널은 각 스레드를 TCB(ID·상태·PC·레지스터·스택포인터·우선순위·PCB포인터)로 관리한다
- 사용자 수준 스레드(빠르지만 블로킹 취약) vs 커널 수준 스레드(진짜 병렬 가능)
- 매핑 모델: N:1(다대일), 1:1(일대일, 현대 주류), M:N(다대다 하이브리드)
- 스레드 컨텍스트 스위칭은 주소 공간 유지로 TLB 플러시가 없어 프로세스 전환보다 저렴하다
- 공유 자원 동시 접근 시 race condition 발생 → 뮤텍스/세마포어로 임계 구역 보호
Chrome Browser Process
크롬(Chromium)은 하나의 거대한 프로세스가 아니라 역할별로 분리된 여러 OS 프로세스로 구성된 멀티 프로세스 아키텍처(multi-process architecture)를 채택한다. 최상위에 특권을 가진 브라우저 프로세스(browser process, 흔히 main process)가 있고, 그 아래로 탭 안의 웹 콘텐츠를 실행하는 샌드박스된 렌더러 프로세스(renderer process), 그래픽을 담당하는 GPU 프로세스, 네트워크·오디오·스토리지 등 위험한 작업을 격리한 유틸리티 프로세스(utility process), 그리고 (레거시) 플러그인 프로세스로 나뉜다. 이렇게 나누는 핵심 이유는 안정성(한 탭이 죽어도 다른 탭은 살아있음)과 보안(신뢰할 수 없는 웹 콘텐츠를 최소 권한의 샌드박스에 가둠), 성능 격리다. 프로세스 간 통신은 과거의 레거시 IPC를 대체하는 Mojo라는 IPC 프레임워크로 이뤄지며, scoped/typed 인터페이스로 권한을 좁게 제한한다. 사이트 격리(Site Isolation)가 켜지면 렌더러 프로세스는 한 번에 오직 하나의 사이트(site) 문서만 담고, cross-site iframe은 별도 프로세스(OOPIF)로 분리되어 Spectre 같은 사이드채널 공격으로부터 사이트 간 데이터를 보호한다. 단점은 프로세스마다 V8 등 공통 인프라 사본을 갖게 되어 메모리 오버헤드가 크다는 점이며, 크롬은 기기 사양에 따라 프로세스 수를 제한하고 같은 사이트 탭을 합치는 방식으로 이를 완화한다.
- 브라우저 프로세스는 유일하게 높은 권한을 가지며 UI(주소창·탭·북마크), 네비게이션, 네트워크/파일 접근, 스토리지 정책, 프로세스 생성을 총괄한다
- 렌더러 프로세스는 신뢰할 수 없는 웹 콘텐츠를 실행하므로 강한 샌드박스에 갇혀 있고 파일/네트워크에 직접 접근하지 못한다
- IPC는 레거시 Chromium IPC에서 Mojo로 대체 중이며, 인터페이스 단위로 권한을 좁게 부여(capability-based)한다
- 사이트 격리(Site Isolation)는 렌더러 하나에 하나의 site만 담고 cross-site iframe을 OOPIF 별도 프로세스로 분리해 Spectre류 공격을 방어한다
- 멀티 프로세스의 이점은 안정성(크래시 격리), 보안(샌드박스), 성능 격리이고 대가는 메모리 중복(프로세스마다 공통 인프라 사본)이다
- 크롬은 기기 메모리에 따라 프로세스 개수 상한을 두고, 같은 사이트의 여러 탭을 한 렌더러로 통합해 오버헤드를 줄인다
- GPU·네트워크·오디오 등 위험도가 높은 작업은 유틸리티/GPU 프로세스로 out-of-process 격리한다
Renderer Process
렌더러 프로세스(renderer process)는 탭 하나(정확히는 사이트 인스턴스) 안에서 벌어지는 거의 모든 일을 처리하는 프로세스로, 내부는 여러 스레드로 구성된다. 중심은 메인 스레드(main thread)로, 여기서 HTML/CSS 파싱, DOM 구성, 스타일 계산(recalc), 레이아웃(layout), 히트 테스트, 이벤트 디스패치, 그리고 V8을 통한 JavaScript 실행과 문서 라이프사이클(document lifecycle)이 돌아간다. 스크롤·애니메이션·입력 처리는 컴포지터 스레드(compositor thread)로 분리되어, 메인 스레드가 무거운 JS로 막혀 있어도 부드러운 스크롤이 가능하도록 성능을 격리한다. 컴포지터가 레이어를 타일(tile)로 쪼개면 래스터 스레드(raster/compositor worker thread)들이 각 타일을 실제 픽셀로 래스터화하여 GPU 메모리에 저장한다. 이 외에 Web Worker/Service Worker/OffscreenCanvas를 돌리는 워커 스레드, 오디오·비디오를 다루는 미디어 스레드가 있다. 렌더링 엔진 Blink는 DOM과 웹 표준 API·렌더 파이프라인을 구현하고, V8은 JS와 WebAssembly를 실행하며, 이 모든 것은 신뢰할 수 없는 코드를 실행하므로 강한 샌드박스(sandbox) 안에 갇혀 있다.
- 메인 스레드는 파싱·스타일·레이아웃·페인트 준비·JS 실행·문서 라이프사이클을 모두 처리하는 병목 지점이다
- 컴포지터 스레드는 입력·스크롤·애니메이션·레이어화(layerization)를 담당해 메인 스레드가 막혀도 스크롤이 부드럽다
- 래스터 스레드는 컴포지터가 나눈 타일을 픽셀로 래스터화해 GPU 텍스처에 저장한다
- 워커 스레드는 Web/Service Worker와 OffscreenCanvas를, 미디어 스레드는 오디오·비디오 디코딩을 독립적으로 처리한다
- Blink는 렌더링 엔진(DOM·CSS·웹 API·렌더 파이프라인)이고 V8은 JS/WASM 엔진으로, 둘은 바인딩으로 연결된다
- 렌더러는 신뢰할 수 없는 웹 콘텐츠를 실행하므로 샌드박스로 격리되어 OS 자원에 직접 접근하지 못한다
- 실제 화면 합성·그리기는 렌더러가 아니라 GPU 프로세스(Viz)에서 일어나며 렌더러는 컴포지터 프레임을 제출한다
Network
브라우저가 URL을 화면에 띄우기까지의 네트워크 계층은 DNS → TCP → TLS → HTTP 순으로 쌓인다. 먼저 DNS 리졸버(recursive resolver)가 도메인 이름을 IP로 변환하는데, 캐시에 없으면 루트 네임서버(root) → TLD 네임서버(.com 등) → 권한 네임서버(authoritative)를 재귀적으로 물어 최종 IP를 얻는다. 그다음 TCP 3-way handshake(SYN → SYN-ACK → ACK)로 신뢰성 있는 연결을 맺고, 포트·시퀀스 번호를 협상하며 흐름 제어(flow control)와 혼잡 제어(congestion control)로 전송 속도를 조절한다. HTTPS라면 그 위에서 TLS 핸드셰이크가 일어나 인증서로 서버 신원을 검증하고, 비대칭키(asymmetric)로 대칭 세션키(symmetric)를 안전하게 교환한 뒤 이후 데이터는 대칭키로 빠르게 암호화한다. 마지막으로 HTTP가 요청(메서드·헤더·바디)과 응답(상태코드·헤더·바디)을 주고받으며, 프로토콜은 HTTP/1.1(텍스트, 순차)에서 HTTP/2(바이너리 프레이밍·멀티플렉싱, TCP 위)로, 다시 HTTP/3(QUIC/UDP 위, HoL 블로킹 해소)로 진화했다. 반복 요청은 HTTP 캐시로 절약하고, 지리적으로 분산된 CDN이 콘텐츠를 사용자 가까이에서 제공해 지연(latency)을 줄인다.
- DNS는 계층적 분산 시스템으로, 리졸버가 root→TLD→authoritative를 재귀 질의하며 각 단계 캐시로 대부분의 왕복을 생략한다
- TCP 3-way handshake(SYN/SYN-ACK/ACK)로 연결을 맺고 시퀀스 번호·흐름 제어·혼잡 제어로 신뢰성 있는 순차 전송을 보장한다
- TLS는 TCP 연결 위에서 인증서로 서버를 검증하고 비대칭키로 대칭 세션키를 교환한 뒤, 실제 데이터는 대칭키로 암호화한다
- HTTP 요청은 메서드·URL·헤더·바디, 응답은 상태코드·헤더·바디로 구성되며 헤더가 캐싱·인증·콘텐츠 협상을 제어한다
- HTTP/1.1은 텍스트·순차, HTTP/2는 바이너리 멀티플렉싱(TCP), HTTP/3는 QUIC/UDP 기반으로 HoL 블로킹과 핸드셰이크 왕복을 줄인다
- HTTP 캐시(Cache-Control·ETag 등)는 재요청을 줄이고, CDN은 엣지에서 콘텐츠를 제공해 물리적 거리로 인한 지연을 줄인다
- QUIC은 전송+암호화 핸드셰이크를 1-RTT로 합치고, 스트림별 독립 전송으로 패킷 손실이 다른 스트림을 막지 않으며, 연결 마이그레이션을 지원한다
HTML Parser
HTML 파서는 서버가 보낸 바이트 스트림을 브라우저가 다룰 수 있는 DOM 트리로 변환하는 컴포넌트로, WHATWG 스펙상 크게 두 단계로 나뉜다. 먼저 바이트→문자 디코딩(byte-to-character decoding) 단계에서 Content-Type의 charset이나 BOM, meta 태그를 근거로 인코딩(주로 UTF-8)을 결정해 바이트를 유니코드 문자로 바꾼다. 그다음 토크나이저(tokenizer)가 문자 스트림을 상태 기계(state machine)로 훑어 시작 태그·종료 태그·텍스트·주석 같은 토큰(token)을 뽑아내고, 트리 구성(tree construction) 단계가 그 토큰들을 받아 삽입 모드(insertion mode) 규칙에 따라 노드를 만들어 DOM 트리에 붙인다. 이 과정에서 `<script>`를 만나면(async/defer가 아닌 한) 스크립트가 DOM을 바꿀 수 있으므로 트리 구성이 멈추고 스크립트를 즉시 실행한다. 이 블로킹으로 인한 낭비를 줄이기 위해 브라우저는 프리로드 스캐너(preload scanner, Firefox는 speculative parser)를 따로 돌려, 파서가 막혀 있는 동안에도 뒤쪽 HTML을 미리 훑어 img·link·script 등 리소스를 병렬로 선다운로드한다. defer는 파싱을 막지 않고 문서 파싱이 끝난 뒤 순서대로 실행하며, async는 다운로드가 끝나는 즉시 순서 무관하게 실행되어 파싱을 중단시킬 수 있다.
- 파싱은 바이트→문자 디코딩 → 토크나이저(토큰화) → 트리 구성 → DOM 생성의 파이프라인으로 진행된다
- 토크나이저는 상태 기계로 문자 스트림을 훑어 시작/종료 태그·텍스트·주석 토큰을 생성한다
- 트리 구성 단계는 insertion mode 규칙에 따라 토큰을 노드로 만들고 DOM 트리에 삽입하며 오류 복구(error recovery)도 수행한다
- 동기 `<script>`는 DOM을 변경할 수 있어 파서를 블로킹하고 즉시 실행되며, CSSOM이 필요하면 CSS 다운로드까지 기다린다
- 프리로드 스캐너(speculative parsing)는 파서가 막힌 동안 앞쪽 HTML을 미리 훑어 리소스를 병렬 프리페치한다
- defer는 파싱을 막지 않고 파싱 완료 후 순서대로, async는 다운로드 완료 즉시 순서 무관하게 실행된다
- document.write는 파서 스트림에 바이트를 주입해 프리로드 스캐너의 추측 작업을 무효화하므로 성능에 해롭다
DOM
DOM(Document Object Model)은 HTML/XML 문서를 메모리 상의 논리적 트리(tree)로 표현한 것으로, 각 가지가 노드(node)에서 끝나고 각 노드는 객체다. 트리의 루트는 Document 노드이며 전역 변수 document로 노출되고, 그 아래로 요소(Element), 텍스트(Text), 주석(Comment), 속성(Attribute) 등 여러 노드 타입이 부모/자식/형제 관계로 연결된다. 각 노드 타입은 nodeType 숫자로 구분되는데 Element=1, Text=3, Comment=8, Document=9 등이다. JavaScript는 querySelector, getElementById, createElement, appendChild 같은 DOM API로 이 트리를 조회·수정하고, addEventListener로 이벤트를 등록해 사용자 상호작용을 처리한다. 컬렉션에는 두 종류가 있어, HTMLCollection과 getElementsByTagName류가 반환하는 live 컬렉션은 문서가 바뀌면 자동으로 갱신되고, querySelectorAll이 반환하는 static NodeList는 조회 시점의 스냅샷으로 고정된다. Shadow DOM은 커스텀 엘리먼트 내부에 캡슐화된 별도 DOM 서브트리(ShadowRoot)를 붙여 스타일과 마크업을 외부로부터 격리하는 웹 컴포넌트 기술이다.
- DOM은 문서를 노드 객체의 트리로 표현하며 루트는 Document 노드(전역 document)다
- 노드 타입은 nodeType 숫자로 구분된다: Element=1, Text=3, Comment=8, Document=9 등
- 노드는 부모/자식/형제(parentNode·childNodes·nextSibling 등) 관계로 연결된 계층 구조를 이룬다
- querySelector·getElementById·createElement·appendChild 등 DOM API로 트리를 조회·조작한다
- 이벤트는 addEventListener로 등록되고 capture→target→bubble 단계로 전파된다
- live 컬렉션(HTMLCollection, getElementsBy*)은 문서 변경에 자동 반영, static NodeList(querySelectorAll)는 스냅샷으로 고정된다
- Shadow DOM(ShadowRoot)은 캡슐화된 서브트리로 스타일·구조를 외부와 격리하는 웹 컴포넌트 기반이다
CSSOM
CSSOM(CSS Object Model)은 브라우저가 모든 CSS 소스(외부 stylesheet, <style> 블록, 인라인 style, User-Agent 기본 스타일시트)를 bytes → characters → tokens → nodes 순서로 파싱해 만든 트리 형태의 객체 모델로, DOM과 별개로 존재하지만 최종적으로 결합되어 각 요소의 스타일을 계산하는 데 쓰인다. CSSOM은 부분적으로 적용할 수 없는데, 뒤에 나오는 규칙이 앞의 규칙을 덮어쓸 수 있어(cascade) CSSOM이 완성되기 전까지는 어떤 요소도 안전하게 렌더링할 수 없기 때문에 CSS는 기본적으로 렌더링 차단(render-blocking) 자원이다. 각 요소의 최종 스타일을 결정할 때 브라우저는 캐스케이드 원점/레이어(origin & importance) → 명시도(specificity) → 소스 순서(source order) 순으로 충돌을 해소하고, 상속(inheritance)으로 부모의 상속 가능 속성을 물려받은 뒤, 상대값(em, %, currentColor 등)을 절대값으로 환산하는 값 처리 단계(specified → computed → used → actual value)를 거친다. 명시도는 (인라인, ID, 클래스/속성/의사클래스, 타입/의사요소)의 4자리 가중치로 계산되며 !important와 CSS @layer(cascade layer)는 명시도를 넘어서는 우선순위 계층을 만든다. 최종 계산된 스타일은 getComputedStyle(el)로 조회할 수 있으며, 이것이 렌더 트리에 부착되는 실제 스타일이다.
- CSSOM은 bytes→characters→tokens→nodes→CSSOM 트리로 파싱되며 DOM과 병렬로 구성된다
- CSS는 render-blocking: CSSOM이 완성되기 전에는 첫 페인트가 지연된다 (미디어 쿼리로 비차단화 가능)
- 캐스케이드 우선순위: 원점/importance/@layer > 명시도(specificity) > 소스 순서
- 명시도는 (inline, ID, class·attr·pseudo-class, type·pseudo-element) 4자리로 계산된다
- 상속(inheritance): color, font 등은 부모에서 물려받고, inherit/initial/unset/revert 키워드로 제어
- 값 처리 파이프라인: specified value → computed value → used value → actual value
- getComputedStyle()은 캐스케이드·상속·기본값이 모두 반영된 최종 값을 반환한다
Render Tree
렌더 트리(Render Tree)는 DOM 트리와 CSSOM 트리를 결합해 만든, '실제로 화면에 그려질 내용'만을 담은 트리로, 각 노드에는 계산된 스타일(computed style)이 부착된다. 구성 과정은 DOM 루트부터 순회하며, 렌더링 출력에 반영되지 않는 노드(<script>, <meta>, <head> 등)와 display:none으로 숨겨진 노드를 제외하고, 남은 각 가시 노드에 매칭되는 CSSOM 규칙을 찾아 계산된 스타일과 함께 방출(emit)하는 방식이다. 여기서 핵심 구분은 display:none은 박스 자체가 생성되지 않아 렌더 트리에서 완전히 빠지지만, visibility:hidden은 보이지 않아도 공간을 차지하는 빈 박스로 렌더 트리에 남는다는 점이다. 텍스트를 감싸는 인라인/블록 컨텍스트를 맞추기 위해 브라우저가 자동으로 만드는 익명 박스(anonymous box)와, DOM에는 없지만 content로 생성되는 가상 요소(::before/::after) 및 ::marker 같은 노드도 렌더 트리에 포함된다. 이렇게 완성된 렌더 트리(브라우저별로 render tree/frame tree/box tree/fragment tree 등으로 불림)가 이후 레이아웃(위치·크기)과 페인트(픽셀)의 입력이 된다.
- 렌더 트리 = DOM(무엇을) + CSSOM(어떻게) 결합, 계산된 스타일이 각 노드에 부착
- 가시 노드만 포함: <head>, <meta>, <script>처럼 렌더링에 안 나오는 노드는 제외
- display:none → 박스 미생성, 렌더 트리에서 완전 제외 (레이아웃 참여 안 함)
- visibility:hidden → 렌더 트리에 포함, 공간은 차지하되 픽셀만 안 그림
- 익명 박스(anonymous box): 인라인/블록 혼재 시 브라우저가 자동 생성하는 이름 없는 박스
- 가상 요소 ::before/::after/::marker는 DOM에 없어도 content 규칙으로 렌더 트리에 추가됨
- DOM 노드와 렌더 트리 노드는 1:1이 아니다 (제외되거나, 하나가 여러 박스로 쪼개짐)
Layout (Reflow)
레이아웃(Layout), 또는 리플로우(Reflow)는 렌더 트리의 각 노드에 대해 뷰포트(viewport) 안에서의 정확한 위치와 크기를 기하학적으로 계산하는 단계로, 결과는 상대값(%, em, vw 등)까지 모두 픽셀 좌표로 확정된다. 계산의 기본 단위는 박스 모델(box model)로, 안쪽부터 content → padding → border → margin 순의 영역과 box-sizing(content-box/border-box)에 따라 크기가 결정된다. 각 요소는 자신이 속한 포매팅 컨텍스트(formatting context)—블록(BFC), 인라인(IFC), 플렉스, 그리드—의 규칙에 따라 배치되며, 이 컨텍스트가 자식들의 흐름·정렬·마진 병합 여부를 지배한다. 레이아웃은 렌더 트리 크기에 비례하는 비용이 큰 작업이라 브라우저는 변경된 부분만 다시 계산하는 증분 레이아웃(incremental/dirty-bit layout)으로 최적화한다. 하지만 JS가 offsetTop, getBoundingClientRect(), getComputedStyle() 등 레이아웃 의존 값을 스타일 변경 직후 읽으면 브라우저가 큐에 쌓인 변경을 즉시 반영하려 강제 동기 레이아웃(forced synchronous layout)을 유발하고, 이를 읽기/쓰기가 번갈아 반복되면 레이아웃 스래싱(layout thrashing)이 되어 프레임을 떨어뜨린다.
- 레이아웃/리플로우는 각 노드의 위치·크기를 뷰포트 기준 픽셀로 확정하는 단계
- 박스 모델: content → padding → border → margin, box-sizing이 크기 해석을 바꿈
- 포매팅 컨텍스트가 배치 규칙 결정: BFC(블록), IFC(인라인), Flex, Grid
- 리플로우 트리거: DOM 추가/삭제, 크기·폰트·위치 변경, 창 리사이즈, 콘텐츠 변경 등
- 증분 레이아웃(dirty bit): 변경된 서브트리만 다시 계산해 비용 절감
- 강제 동기 레이아웃: 레이아웃 의존 속성(offsetWidth, getBoundingClientRect 등)을 읽으면 즉시 재계산
- 레이아웃 스래싱: 읽기↔쓰기 반복으로 한 프레임에 리플로우가 여러 번 → 읽기 batch로 해결(FastDOM 등)
Paint
페인트(Paint)는 레이아웃으로 확정된 각 박스를 실제 픽셀로 바꾸기 위한 '그리기 명령'을 만드는 단계로, 여기서 바로 픽셀이 채워지는 것이 아니라 배경·테두리·텍스트·그림자 등을 어떻게 그릴지 기록한 페인트 레코드(paint record) 즉 디스플레이 리스트(display list)를 생성한다. 이 명령들은 반드시 올바른 순서로 쌓여야 하는데, 그 순서는 스태킹 컨텍스트(stacking context)와 z-index로 결정되며 배경/보더 → 음수 z-index → 블록 → float → 인라인 → 양수 z-index 순의 페인트 순서를 따른다. 실제로 픽셀을 채우는 작업은 래스터화(rasterization)라 하며, 디스플레이 리스트를 비트맵으로 변환하는 이 과정은 종종 GPU에서 뒤이은 컴포지트 단계와 함께 수행된다. 어떤 요소의 시각적 속성(색, 배경, 그림자 등 기하와 무관한 속성)만 바뀌면 레이아웃 없이 해당 영역만 다시 그리는 리페인트(repaint)가 일어나며, 이때 브라우저는 화면을 타일 단위로 잘라 바뀐 타일만 다시 래스터화해 비용을 줄인다.
- 페인트는 픽셀을 바로 칠하지 않고 '그리기 명령'(paint record / display list)을 기록하는 단계
- 디스플레이 리스트: 배경·보더·텍스트·그림자 등을 그리는 순서가 있는 명령 목록
- 페인트 순서는 스태킹 컨텍스트와 z-index가 결정 (겹침 순서)
- 스태킹 컨텍스트는 z-index만이 아니라 opacity<1, transform, filter, will-change 등으로도 생성
- 래스터화(rasterization): 디스플레이 리스트를 실제 비트맵 픽셀로 변환하는 과정
- 레이어: 독립적으로 그려질 수 있는 콘텐츠 묶음. 리페인트 범위를 격리
- 리페인트(repaint): 기하 변경 없는 시각 속성 변경 시 해당 타일만 다시 래스터화
Composite
컴포지트(Composite)는 페인트로 래스터화된 여러 레이어를 최종 화면 이미지로 합성하는 마지막 단계로, 이 작업은 메인 스레드가 아닌 별도의 컴포지터 스레드(compositor thread)에서 수행되어 메인 스레드가 바쁜(JS 실행 등) 동안에도 스크롤과 애니메이션이 부드럽게 유지된다. 브라우저는 페인트 결과인 디스플레이 리스트를 레이어 트리(layer tree)로 나누고(layerize), 각 레이어를 타일(tile) 단위로 GPU에서 래스터화한 뒤, 위치·클립·시각효과를 담은 프로퍼티 트리(transform, clip, effect, scroll)를 적용해 GPU가 텍스처들을 겹쳐 그린다. transform과 opacity는 레이아웃·페인트를 건드리지 않고 이 컴포지터 단계에서만 처리되기 때문에 애니메이션을 메인 스레드에서 완전히 오프로드해 60fps(프레임당 약 16.6ms)를 안정적으로 달성할 수 있다. 개발자는 will-change: transform 등으로 요소를 미리 레이어로 승격(layer promotion)해 힌트를 줄 수 있으나, 레이어마다 GPU 메모리가 들기 때문에 과도한 승격은 오히려 성능을 해친다.
- 컴포지트는 여러 레이어를 GPU에서 겹쳐 최종 화면을 만드는 마지막 단계
- 컴포지터 스레드는 메인 스레드와 분리 → 메인이 바빠도 스크롤·애니메이션 유지
- 레이어 트리로 분할(layerize) 후 각 레이어를 타일 단위로 GPU 래스터화
- 프로퍼티 트리 4종: transform, clip, effect(opacity/filter/mask), scroll
- transform·opacity는 layout·paint를 건너뛰고 컴포지트만 → 가장 저렴, 60fps 달성
- will-change / 레이어 승격(promotion)으로 미리 레이어화 힌트 제공
- 레이어 남용은 GPU 메모리 폭증(layer explosion)으로 역효과 → 필요한 곳에만
V8 Engine
V8은 구글이 만든 오픈소스 JavaScript/WebAssembly 엔진으로, Chrome과 Node.js의 실행 코어다. 소스코드는 먼저 스캐너(scanner)가 문자를 토큰(token)으로 분해하고 파서(parser)가 이를 AST(추상 구문 트리)로 만든 뒤, Ignition 인터프리터가 AST를 바이트코드(bytecode)로 컴파일해 즉시 실행한다. 실행 도중 인라인 캐시(inline cache)와 프로파일링으로 '뜨거운(hot)' 함수와 타입 정보를 수집하고, 충분히 자주 실행되면 TurboFan(및 중간 계층 Maglev/Sparkplug)이 이를 근거로 투기적(speculative) 최적화 기계어를 JIT 컴파일한다. 최적화는 '이 객체는 항상 이 히든 클래스(hidden class)'라는 가정에 의존하므로, 가정이 깨지면 최적화 해제(deoptimization, deopt)가 일어나 다시 바이트코드 인터프리터로 되돌아간다. 메모리는 세대별(generational) 가비지 컬렉터가 힙(heap)을 관리하며, 짧게 사는 객체는 Scavenge로, 오래 사는 객체는 Mark-Sweep-Compact로 회수한다.
- 실행 파이프라인: 소스 → 스캐너(토큰) → 파서(AST) → Ignition 바이트코드 → 실행 → 프로파일 → TurboFan 최적화 기계어(JIT)
- V8은 순수 인터프리터가 아니라 인터프리터+JIT의 다계층(tiered) 구조: Ignition(인터프리터) → Sparkplug(베이스라인) → Maglev(중간 최적화) → TurboFan(최고 최적화)
- 히든 클래스(hidden class, 내부 명칭 Map)로 객체 구조를 공유·비교하고, 인라인 캐시(IC)로 반복되는 프로퍼티 접근을 캐싱해 속도를 높인다
- TurboFan의 최적화는 투기적이라 가정이 깨지면 deopt로 인터프리터에 되돌아간다(예: 객체 shape 변경, 타입 변경)
- 가비지 컬렉션은 세대별: young generation은 Scavenge(Cheney 반공간 복사), old generation은 Mark-Sweep-Compact
- young에서 GC를 견딘 객체는 old generation으로 승격(promotion)되며, Orinoco 프로젝트로 대부분 병렬·동시(concurrent) 처리해 멈춤(jank)을 줄인다
- 힙(heap)은 객체·클로저 저장, 스택(stack)은 함수 호출 프레임과 원시값 참조를 저장하는 별개 영역
Call Stack
콜스택(call stack)은 인터프리터가 '지금 어느 함수를 실행 중이고, 그 함수를 어디서 호출했는가'를 추적하는 LIFO(Last-In-First-Out) 자료구조다. 함수를 호출하면 그 함수의 실행 컨텍스트(execution context)를 담은 스택 프레임(stack frame)이 스택 맨 위에 push되고, 함수가 return하면 그 프레임이 pop되어 호출한 위치로 제어가 돌아간다. JavaScript는 단일 스레드(single-threaded)라 콜스택이 하나뿐이며, 한 순간에 오직 하나의 프레임만 실행한다 — 이것이 동기 코드가 순차적으로 실행되는 이유다. 각 실행 컨텍스트는 그 함수의 지역 변수 환경(variable environment), 스코프 체인(scope chain, 렉시컬 환경 참조), this 바인딩(this binding)을 함께 보관한다. 재귀가 종료 조건 없이 계속되거나 너무 깊어지면 스택 프레임이 한계를 넘어 RangeError: Maximum call stack size exceeded(스택 오버플로우, stack overflow)가 발생한다.
- 콜스택은 LIFO 구조: 함수 호출 시 프레임 push, return 시 pop
- JavaScript는 단일 스레드 → 콜스택 1개, 한 번에 한 프레임만 실행(동기 실행의 근거)
- 스택 프레임 = 실행 컨텍스트: 변수 환경, 스코프 체인, this 바인딩을 담는다
- 스크립트 로드 시 전역 실행 컨텍스트(Global Execution Context)가 스택 바닥에 먼저 놓인다
- this 바인딩은 호출 방식(메서드/일반/화살표/call·apply·bind/new)에 따라 컨텍스트 생성 시점에 결정된다
- 스코프 체인은 렉시컬(정적) 구조라 '어디서 선언됐는가'로 결정되지, 어디서 호출됐는가가 아니다
- 종료 없는/과도한 재귀는 스택 오버플로우(RangeError)로 이어진다
Event Loop
이벤트 루프(event loop)는 단일 스레드인 JavaScript가 비동기 작업을 논블로킹으로 처리하게 해주는 조정자다. 콜스택이 비면, 이벤트 루프는 태스크 큐(task queue, 매크로태스크)에서 태스크를 '하나' 꺼내 콜스택에 올려 실행하고, 그 태스크가 끝나 콜스택이 다시 비면 마이크로태스크 큐(microtask queue)를 '완전히' 비운 뒤, 필요하면 렌더링을 수행하고 다시 루프를 돈다. 여기서 콜스택은 동기 실행을, Web API(브라우저 제공: DOM 이벤트, 타이머, fetch 등)는 비동기 작업을 백그라운드에서 처리한 뒤 콜백을 각 큐로 넣어주는 역할을 한다. 매크로태스크에는 setTimeout/setInterval, 사용자·네트워크 이벤트, 초기 스크립트 실행이 있고, 마이크로태스크에는 Promise 반응(.then/catch/finally), queueMicrotask(), MutationObserver 콜백이 있다. 핵심 규칙은 '태스크 하나 → 마이크로태스크 전부 → (필요 시)렌더링'의 반복이며, 마이크로태스크가 항상 다음 매크로태스크보다 먼저, 그리고 렌더링보다 먼저 실행된다는 점이다.
- 이벤트 루프 1회전: 매크로태스크 1개 실행 → 마이크로태스크 큐 전부 비움 → (필요 시)렌더링 → 반복
- 마이크로태스크는 매크로태스크보다 우선순위가 높고, 콜스택이 빌 때마다 '전부' 소진된다(중간에 추가돼도 같은 회전에서 실행)
- 매크로태스크 소스: setTimeout/setInterval, DOM·네트워크 이벤트, 초기 스크립트, MessageChannel 등
- 마이크로태스크 소스: Promise(.then/catch/finally), queueMicrotask, MutationObserver, async/await의 await 이후
- Web API(브라우저 제공: 타이머·DOM·fetch·geolocation 등)는 콜스택 밖에서 비동기 작업을 처리하고 콜백을 큐에 넣는다
- 렌더링(스타일·레이아웃·페인트)은 마이크로태스크가 다 끝난 뒤, 다음 매크로태스크 전에 일어나며 rAF 콜백은 그 렌더링 직전에 실행된다
- 무한히 마이크로태스크를 추가하면 렌더링·태스크가 굶어(starvation) UI가 멈출 수 있다
React Render Phase
렌더 단계(Render Phase)는 React가 상태나 props 변경을 감지한 뒤 각 컴포넌트 함수를 호출해 새 UI의 청사진을 계산하는 단계다. JSX는 빌드 타임에 React.createElement(type, props, ...children) 호출로 변환되고, 이 호출들은 실제 DOM이 아니라 순수 JavaScript 객체 트리인 Virtual DOM(엘리먼트 트리)을 만든다. React는 이 새 트리를 이전 렌더 결과 트리와 비교(diffing)하는 재조정(reconciliation)을 수행하는데, 일반적인 트리 비교는 O(n^3)이지만 React는 두 가지 휴리스틱 가정(서로 다른 타입은 다른 트리를 만든다, key로 안정적인 자식을 힌트한다)에 기반해 O(n) 알고리즘으로 근사한다. 타입이 다르면 기존 서브트리를 통째로 파기하고 새로 만들며(상태 전부 손실), 같은 타입이면 DOM 노드를 재사용하고 바뀐 속성만 갱신한다. 리스트 자식은 key를 통해 위치가 아닌 정체성(identity)으로 매칭해 이동/삽입/삭제를 효율적으로 처리한다. 이 단계는 반드시 순수(pure)해야 하며, 부작용 없이 중단·재개·폐기될 수 있어야 하므로 렌더 중 DOM 조작·구독·타이머 등은 금지된다.
- JSX는 React.createElement 호출로 컴파일되어 실제 DOM이 아닌 순수 JS 객체(Virtual DOM 엘리먼트)를 생성한다
- 재조정(reconciliation)은 새 엘리먼트 트리와 이전 트리를 diffing해 최소 변경 집합을 계산하는 과정이다
- 일반 트리 diff는 O(n^3)이지만 React는 휴리스틱으로 O(n)에 근사한다(가정 2개: 타입 다르면 다른 트리, key로 안정적 자식 힌트)
- 타입 비교: <div>→<span>처럼 타입이 바뀌면 노드 파기 후 재생성(자식과 상태 전부 손실), 같은 타입이면 노드 재사용+변경 속성만 패치
- key는 형제 간에만 유일하면 되며, 배열 index를 key로 쓰면 재정렬 시 상태 꼬임·불필요한 언마운트가 발생한다
- 렌더 단계는 순수해야 한다: 같은 입력→같은 출력, 부작용 금지, 그래서 중단/재개/폐기가 안전하다
- 이 단계는 계산만 할 뿐 실제 DOM은 건드리지 않는다(실제 반영은 커밋 단계)
Fiber
Fiber는 React 16부터 도입된 재조정 엔진의 핵심 자료구조이자 실행 모델로, 각 컴포넌트 인스턴스에 대응하는 '작업 단위(unit of work)'를 나타내는 JavaScript 객체다. 기존 스택 기반 재귀 재조정은 한 번 시작하면 끝까지 동기적으로 실행되어 중단할 수 없었지만, Fiber는 트리를 연결 리스트(child/sibling/return 포인터)로 재구성해 작업을 잘게 쪼개고 중단·재개·우선순위 재조정을 가능하게 했다. React는 화면에 보이는 current 트리와 백그라운드에서 만드는 work-in-progress 트리를 동시에 유지하는 더블 버퍼링(double buffering)을 사용하며, 커밋 시점에 두 트리의 포인터를 스왑한다. 각 업데이트에는 우선순위를 나타내는 lane(32비트 비트마스크)이 부여되고, 스케줄러(Scheduler)가 브라우저의 유휴 시간을 활용해 타임 슬라이싱으로 작업을 배분하며 급한 작업(예: 입력)이 오면 낮은 우선순위 렌더를 중단한다. 재조정은 각 Fiber에서 하향(beginWork)과 상향(completeWork) 두 국면으로 진행되며, 그 결과 부작용이 있는 노드들이 effect list에 모여 커밋 단계로 전달된다.
- Fiber 노드 = 하나의 작업 단위. type, key, stateNode, 그리고 child/sibling/return 포인터와 pendingProps/memoizedState/flags 등을 담는다
- 스택 재귀를 연결 리스트 순회로 바꿔 렌더 작업을 중단(pause)·재개(resume)·폐기(abort)할 수 있게 만든 것이 Fiber의 본질
- 더블 버퍼링: current 트리(화면 반영본) vs work-in-progress 트리(작업본), alternate 포인터로 상호 연결, 커밋 때 스왑
- lanes: 각 비트가 우선순위 레벨인 32비트 정수 비트마스크. 여러 업데이트의 우선순위를 효율적으로 병합/비교
- Scheduler가 requestIdleCallback 유사 메커니즘(실제로는 MessageChannel)으로 타임 슬라이싱, 프레임을 양보(yield)
- beginWork(하향): 컴포넌트 render 호출 후 자식 Fiber 생성/재조정. completeWork(상향): 호스트 인스턴스 준비 및 effect 수집
- effect list(현대엔 flags/subtreeFlags 기반)로 부작용이 있는 Fiber만 추려 커밋 단계에 전달
Commit Phase
커밋 단계(Commit Phase)는 렌더 단계가 계산해 낸 work-in-progress 트리의 변경 사항을 실제 DOM에 반영하는 단계로, 중단 불가능한 동기(synchronous) 실행이 특징이다. 내부적으로 세 개의 하위 단계로 나뉜다: before mutation(변이 전, getSnapshotBeforeUpdate 호출 등 DOM 변이 직전 상태를 읽는 단계), mutation(실제 DOM 삽입·삭제·속성 갱신 및 이전 ref detach, useLayoutEffect의 cleanup 실행), layout(DOM 변이 완료 후 useLayoutEffect의 create 실행 및 새 ref attach). 이 세 단계는 브라우저 페인트 이전에 동기적으로 끝나므로, useLayoutEffect에서 DOM을 측정하고 다시 쓰면 사용자는 깜빡임 없이 최종 결과만 본다. 반면 useEffect(passive effect)는 커밋과 페인트가 끝난 뒤 별도로 비동기 스케줄되어 실행되므로 페인트를 막지 않는다. 커밋이 끝나면 work-in-progress 트리가 current 트리가 되며(포인터 스왑), 이 시점 이후에야 브라우저가 화면을 다시 그릴 기회를 얻는다.
- 커밋 단계는 동기적이고 중단 불가능하다 — 렌더 단계와 달리 절대 쪼개지거나 폐기되지 않는다
- 3개 하위 단계: before mutation → mutation → layout 순서로 실행된다
- before mutation: getSnapshotBeforeUpdate 호출 등 DOM을 변경하기 직전의 스냅샷을 읽는다
- mutation: 실제 DOM에 노드 삽입/삭제/갱신, 이전 ref를 null로 detach, useLayoutEffect의 cleanup(destroy) 실행
- layout: DOM 반영이 끝난 뒤 useLayoutEffect의 create 콜백을 동기 실행하고 새 ref를 attach — 페인트 전이라 측정+재조정이 안전
- useLayoutEffect는 페인트를 막고 동기 실행(깜빡임 방지), useEffect(passive)는 페인트 후 비동기 실행(성능 우선)
- 커밋 완료 시 work-in-progress 트리가 current 트리로 스왑되고, 이후 브라우저가 페인트한다
Browser Paint
브라우저 페인트는 React가 커밋으로 DOM을 바꾼 뒤 브라우저가 픽셀을 갱신하기 위해 렌더링 파이프라인을 재실행하는 과정이다. 파이프라인은 JavaScript → 스타일 계산(Style) → 레이아웃(Layout, = 리플로우 reflow) → 페인트(Paint, = 리페인트 repaint) → 합성(Composite)의 다섯 단계로 이루어진다. 변경한 CSS 속성의 성격에 따라 실행되는 단계가 달라지는데, width·height·top 같은 기하 속성을 바꾸면 레이아웃부터 전체가 다시 돈다(리플로우). color·background-image·box-shadow 같은 페인트 전용 속성은 레이아웃을 건너뛰고 페인트→합성만 실행되며, transform·opacity처럼 합성만 필요한 속성은 레이아웃과 페인트를 모두 건너뛰어 가장 저렴하다. 레이아웃 스래싱(layout thrashing)은 JS에서 레이아웃 값을 읽고(offsetHeight 등) 곧바로 쓰기를 반복해 프레임 내에서 강제 동기 레이아웃(forced synchronous layout)을 여러 번 유발하는 안티패턴이다. 브라우저는 이런 비용을 줄이기 위해 여러 DOM 변경을 모아 한 프레임에 배칭(batching)해서 파이프라인을 한 번만 재실행하려 한다.
- 렌더링 파이프라인 5단계: JavaScript → Style → Layout(리플로우) → Paint(리페인트) → Composite(합성)
- 리플로우(reflow) = 레이아웃 재계산: 요소의 크기/위치가 바뀌면 발생, 한 요소 변경이 트리 전체에 파급될 수 있어 가장 비쌈
- 리페인트(repaint) = 픽셀 다시 칠하기: 레이아웃은 그대로고 색/배경/그림자 등 시각만 바뀔 때. 레이아웃 단계를 건너뜀
- 합성(composite)만 하는 경로: transform·opacity는 레이아웃·페인트 없이 GPU 레이어만 재합성 → 애니메이션에 이상적
- 리플로우는 반드시 리페인트를 동반하지만, 리페인트가 항상 리플로우를 유발하진 않는다
- 레이아웃 스래싱: 읽기(offsetTop/getComputedStyle)와 쓰기를 번갈아 하면 강제 동기 레이아웃이 반복 발생 → 읽기/쓰기를 분리·배칭해야 한다
- 브라우저는 여러 DOM 변경을 모아 한 프레임에 배칭하여 파이프라인 재실행을 최소화한다
Screen Output
화면 출력은 브라우저가 합성한 최종 레이어들을 GPU가 실제 픽셀 프레임으로 합쳐 디스플레이에 내보내는 여정의 마지막 단계다. 컴포지터는 페인트로 만들어진 각 레이어를 텍스처로 GPU에 업로드하고, GPU는 레이어 계층을 순회하며 프레임 버퍼(frame buffer)에 그려 하나의 완성된 이미지를 만든다. 이 프레임은 곧바로 화면에 뿌려지지 않고, 디스플레이의 주사율(refresh rate, 예: 60Hz는 초당 60번)에 맞춰 수직 동기화(VSync) 신호에 동기화되어 스왑(swap)된다. VSync는 모니터가 한 프레임을 위에서 아래로 스캔하는 도중에 버퍼가 교체되어 두 프레임이 섞여 보이는 테어링(tearing)을 방지하며, 이를 위해 최소 두 개(더블 버퍼링) 또는 세 개(트리플 버퍼링)의 버퍼를 번갈아 쓴다. 최종적으로 프레임 버퍼의 각 픽셀 값이 디스플레이의 물리 픽셀(RGB 서브픽셀)로 발광하며 사용자 눈에 도달한다. 즉 전체 여정은 '상태 변경 → React 렌더(Fiber diff) → React 커밋(DOM 반영) → 브라우저 파이프라인(Style/Layout/Paint) → 합성 → GPU 프레임 버퍼 → VSync 스왑 → 픽셀 발광'으로 요약된다.
- 컴포지터가 페인트된 레이어를 텍스처로 GPU에 올리고, GPU가 레이어 계층을 순회하며 프레임 버퍼에 합성한다
- 프레임 버퍼(frame buffer): 한 프레임의 모든 픽셀 색값을 담는 GPU 메모리 영역. 완성되면 디스플레이로 스캔아웃된다
- 주사율(refresh rate): 디스플레이가 초당 화면을 갱신하는 횟수(60Hz=16.7ms/frame, 120Hz=8.3ms). 애니메이션 예산의 기준
- VSync(수직 동기화): 모니터의 수직 블랭킹 구간에 맞춰 버퍼를 스왑, 스캔 중 교체로 생기는 테어링을 방지
- 더블/트리플 버퍼링: 프론트 버퍼(표시용)와 백 버퍼(그리기용)를 분리·순환해 렌더와 표시를 디커플링
- 픽셀: 프레임 버퍼의 값이 디스플레이의 물리 RGB 서브픽셀 발광으로 변환되어 최종 이미지가 된다
- 전체 여정: 상태 변경→React 렌더/커밋→브라우저 파이프라인→합성→GPU 프레임 버퍼→VSync 스왑→화면