브라우저 동작 원리 - 화면이 그려지기까지

2024. 12. 6. 12:49·FrontEnd/web

"주소창에 URL 한 줄 쳤을 뿐인데, 브라우저 안에서는 생각보다 많은 일이 벌어진다."

  • 렌더링 파이프라인이 어떻게 흐르는지
  • CSS/JS가 렌더링을 어떻게 막거나 도와주는지
  • DOM / CSSOM / Render Tree / Layout / Paint / Composite 관계
  • 레이아웃 스래싱에서 왜 성능이 깨지는지

결론 먼저

먼저, 큰 그림만 잡고 가자.

  • 브라우저는 HTML, CSS, JS를 받아서 다음 순서로 화면을 만든다.
    • HTML 파싱 → DOM 트리
    • CSS 파싱 → CSSOM 트리
    • DOM + CSSOM → Render Tree
    • Render Tree 기반으로 Layout → Paint → Composite
  • CSS는 렌더‑블로킹이 될 수 있다.
    • CSSOM이 완성되어야 Render Tree를 만들 수 있기 때문이다.
  • JS는 파서‑블로킹이 될 수 있다.
    • 기본 <script>는 HTML 파싱을 잠시 멈추게 만든다.
    • defer/async/type="module"로 어느 정도 제어 가능하다.
  • 성능 이슈에서 자주 나오는 게 레이아웃 스래싱(Layout Thrashing)이다.
    • "읽기(레이아웃 계산) → 쓰기(스타일 변경)"가 프레임 안에서 섞여 반복될 때 터진다.
    • 읽기 묶고 쓰기 묶는 패턴으로 바꾸는 게 핵심.

이제 위 내용들을 조금 더 그림 + 코드와 함께 풀어본다.


1. 렌더링 파이프라인

먼저 브라우저가 화면을 그리는 전체 흐름부터 보자

(네트워크) ➞ HTML / CSS / JS 다운로드
                       │
                       ├─ HTML 파싱 ──▶ DOM 트리
                       ├─ CSS  파싱 ──▶ CSSOM 트리
                       └─ JS   실행 ──▶ (파서 차단 여부 주의)

DOM + CSSOM ➞ Render Tree ──▶ Layout ──▶ Paint ──▶ Composite ─▶ 화면
               (보이는 것만)    (크기/좌표)  (픽셀로 그림) (레이어 합성)

위 과정을 그림으로 보면 이런 느낌이다.

각 단계는 대략 이런 역할을 한다.

  • DOM
    • HTML을 파싱해서 만든 문서 구조 트리
    • 태그들이 노드(Node)로 이어진 계층 구조
  • CSSOM
    • CSS를 파싱해서 만든 스타일 규칙 트리
    • 어떤 선택자에 어떤 스타일이 적용되는지 정보 포함
  • Render Tree
    • DOM + CSSOM을 결합해서 만든 화면에 보이는 노드만 모은 트리
    • display: none 요소는 여기서 제외된다.
    • visibility: hidden은 자리는 차지하지만 안 보이는 상태.
  • Layout (=Reflow)
    • Render Tree 기준으로 각 요소의 크기와 위치(좌표)를 계산한다.
  • Paint
    • Layout 결과를 바탕으로 요소들을 픽셀로 그리는 단계
  • Composite
    • 여러 레이어를 GPU에서 합성해서 최종 화면을 만든다.

실무에서는 이 전체 과정을 줄여서 그냥 "렌더링 파이프라인"이라고 부르는 경우가 많다.


2. 쉬운 용어 정리

용어가 헷갈리기 쉬운 부분들을 먼저 정리해 보자.

렌더‑블로킹(Render‑blocking)

  • 브라우저가 화면을 그리기 전에 반드시 준비되어야 하는 리소스
  • 준비되기 전까지 렌더링을 멈춰야 한다.
  • 대표적인 예가 CSS다.
    • CSSOM이 완성되어야 Render Tree를 만들 수 있기 때문.

파서‑블로킹(Parser‑blocking)

  • HTML을 읽는 파서가 잠시 멈추는 상황
  • 기본 <script> 태그를 만나면 이렇게 된다.
    • HTML 파싱 중단
    • 스크립트 다운로드 및 실행
    • 실행이 끝나면 파싱 재개

메인 파서 vs Preload Scanner

  • 메인 파서(Main Parser)
    • HTML을 읽어서 DOM을 만드는 본 담당자
  • Preload Scanner(= Speculative Parser)
    • 메인 파서가 멈춰 있어도
    • 내부에서 링크 / 스크립트 / 이미지 같은 리소스를 미리 찾아서 다운로드를 시작한다.
    • 덕분에 체감 로딩 속도가 빨라진다.

3. JavaScript와 렌더링

이제 자바스크립트가 이 파이프라인에 어떻게 영향을 주는지 보자.

스크립트 로딩 옵션 정리

속성 파싱과 병행 여부 실행 시점 파서 차단 여부 특징

기본 <script> 경우에 따라 다름 도착 즉시 실행 ✅ 블로킹 HTML 파싱을 잠시 멈춘다
defer ✅ 병행 DOMContentLoaded 직전, 순서 보장 ❌ 비차단 일반적으로 권장 옵션
async ✅ 병행 도착 즉시 실행 ❌ 비차단 실행 순서가 보장되지 않는다
type="module" ✅ 의존성 병렬 기본적으로 defer처럼 동작 ❌ 비차단 모듈 스코프, 중복 로드 방지

실무에서 가장 무난한 조합은 이 두 가지다.

<script src="/app.js" defer></script>
<script type="module" src="/app.mjs"></script>

생각 없이 <script>만 잔뜩 쓰면,
파서‑블로킹 때문에 초기 렌더링이 불필요하게 늦어질 수 있다는 걸 항상 염두에 두는 게 좋다.


4. 브라우저를 구성하는 주요 컴포넌트

조금 더 위에서 보면, 브라우저는 여러 컴포넌트가 모여 있는 하나의 프로그램이다.

각 박스가 하는 일은 대략 이렇다.

컴포넌트 하는 일
User Interface 주소창, 뒤로/앞으로, 북마크 등 브라우저 UI 전체
Browser Engine UI와 렌더링 엔진 사이를 조율하는 중간 관리자
Rendering Engine HTML/CSS 파싱 → 스타일 계산 → Layout/Paint/Composite
Networking HTTP/3, 캐시, 요청 우선순위 관리
JavaScript Engine JS 파싱/컴파일/실행
UI Backend / Graphics 폰트 렌더링, 그리기 API(플랫폼 추상화)
Storage Cookies, localStorage, IndexedDB, Cache API 등

그리고 중요한 포인트 하나

브라우저에서 JS는 렌더링 엔진/네트워크 엔진을 직접 만지지 않는다.
DOM / CSSOM / BOM 같은 Web API를 통해서만 페이지를 조작한다.

우리가 만지는 건 결국 Web API를 통해 노출된 인터페이스일 뿐,
브라우저 내부 모듈(렌더링 엔진, 네트워크 스택)을 바로 건드리는 건 아니다.


5. 트리(Tree)와 노드(Node)

렌더링 파이프라인에서 자꾸 DOM, CSSOM, Render Tree 같은 말이 나오는데,
결국 이 모든 것들은 트리 구조다.

트리와 노드

  • 트리(Tree)
    • 부모 → 자식으로 이어진 계층 구조 자료구조
  • 노드(Node)
    • 트리를 이루는 한 요소
    • 값과 부모/자식 관계를 가진다.

DOM 트리 예시

DOM 트리:
    └── div (id="container")
         ├── p (text: "Hello World!")
         └── a (href="https://example.com", text: "Link")

CSSOM 트리 예시

CSSOM 트리:
    ├── p (선택자)
    │    ├── color: red
    │    └── font-size: 16px
    └── div (선택자)
         └── background-color: blue

정리하면

DOM = 구조/내용, CSSOM = 스타일 규칙.
이 둘을 합쳐서 Render Tree를 만들고,
Render Tree를 기준으로 Layout → Paint가 진행된다.


6. 레이아웃 스래싱(Layout Thrashing)

이제 성능 얘기를 조금만 하자

레이아웃 스래싱이란?

  • 한 프레임 안에서 레이아웃 계산(읽기) 직후에
  • 바로 스타일 변경(쓰기)을 섞어서 반복하는 패턴

이렇게 되면 브라우저는

  • "읽기 → 레이아웃 재계산"
  • "쓰기(스타일 변경) → 다시 레이아웃/페인트 필요"

이 과정을 계속 반복하게 되고, 결국 프레임 안에서 레이아웃/페인트가 여러 번 강제로 일어나는 상황이 된다.

안 좋은 예 (읽고→바로 쓰고→다시 읽고… 반복)

const el = document.querySelector('.box');

// 강제 레이아웃 읽기
const w = el.offsetWidth;

// 곧바로 쓰기(스타일 변경)
el.style.width = (w + 10) + 'px';

// 다시 읽기 → 또 레이아웃 계산
const h = el.offsetHeight;
el.style.height = (h + 10) + 'px';

위 코드는 읽기와 쓰기가 섞여 있어서 레이아웃 스래싱을 유발할 수 있다.

좋은 패턴 (읽기 묶고 → 쓰기 묶기)

const el = document.querySelector('.box');

// 1) 읽기만 먼저
const w = el.offsetWidth;
const h = el.offsetHeight;

// 2) 쓰기는 한 번에 (가능하면 다음 프레임에)
requestAnimationFrame(() => {
  el.style.width = (w + 10) + 'px';
  el.style.height = (h + 10) + 'px';
});

실무에서 기억해 두면 좋은거!

  • "읽기 → 쓰기" 순서를 지키고, 프레임 안에서 섞지 않기
  • 자잘한 스타일 변경은 클래스 토글로 묶어서 한 번에 적용하기
  • 애니메이션은 가능하면 transform/opacity 위주로 설계하기
    • 이 둘은 합성(Composite) 단계에서 처리되어 Layout/Paint 비용이 적다.
  • 스크롤 측정 등 빈번한 읽기 작업은 requestAnimationFrame 타이밍에 모아서 처리하기

7. 체크리스트

실제 코드 짤 때, 대략 이렇게 스스로 체크하면 도움이 된다.

  • 초기 렌더에 꼭 필요한 크리티컬 CSS만 <head>에 두었는가?
  • 나머지 CSS는 지연 로드 또는 코드 스플리팅으로 나눠두었는가?
  • 스크립트는 가능한 한 defer/type="module"을 쓰고,
    정말 필요할 때만 동기 <script>를 쓰고 있는가?
  • DOM 읽기/쓰기를 섞어서 레이아웃 스래싱을 만들고 있지 않은가?
  • 애니메이션은 가능한 transform/opacity 위주로 설계했는가?

8. 참조

마지막으로, 참고했던 문서들을 다시 적어 둔다.

  • https://stackoverflow.com/questions/7515227/describe-the-page-rendering-process-in-a-browser
  • https://codeburst.io/how-browsers-work-6350a4234634
  • https://github.com/alex/what-happens-when?tab=readme-ov-file#browser
  • 이미지 출처: https://codeburst.io/how-browsers-work-6350a4234634

오늘 글은 전체적인 브라우저 렌더링 흐름을 한 번에 훑어보는 느낌으로 정리했다.

다음에 성능 최적화나 Web Vitals 같은 걸 볼 때,
"아, 이게 Layout 단계에서 무거운 건가? Paint/Composite 문제인가?"를 구분할 수 있으면
디버깅이 훨씬 덜 막막해진다.

저작자표시 변경금지 (새창열림)

'FrontEnd > web' 카테고리의 다른 글

[실무 기록] <link rel="stylesheet">로 CSS 불러오는데 간헐적으로 CORS 에러가 나던 이유 (해결: crossorigin 제거)  (1) 2026.01.13
[실무 기록] Chrome Inspect에서 Pending authentication만 뜨고 안드로이드 디버깅이 안 될 때  (0) 2026.01.12
[실무 기록] 웹뷰 브릿지, 이거 누가 책임져야 하지?  (0) 2025.12.10
검색 엔진 크롤링과 자바스크립트 렌더링 정리  (3) 2025.09.08
동기식(Synchronous) vs 비동기식(Asynchronous)  (2) 2024.12.06
'FrontEnd/web' 카테고리의 다른 글
  • [실무 기록] Chrome Inspect에서 Pending authentication만 뜨고 안드로이드 디버깅이 안 될 때
  • [실무 기록] 웹뷰 브릿지, 이거 누가 책임져야 하지?
  • 검색 엔진 크롤링과 자바스크립트 렌더링 정리
  • 동기식(Synchronous) vs 비동기식(Asynchronous)
프론트엔드 개발자 jbeat
프론트엔드 개발자 jbeat
프론트엔드 개발자 블로그인데 일상도 쪼그으믐
  • 프론트엔드 개발자 jbeat
    jbeat 님의 블로그
    프론트엔드 개발자 jbeat
  • 전체
    오늘
    어제
    • 분류 전체보기 (44)
      • FrontEnd (43)
        • TypeScript (6)
        • JavaScript (18)
        • Next.js (3)
        • React (1)
        • Testing (2)
        • Third Party (1)
        • web (10)
        • Tooling (1)
        • coding test (0)
        • A.I (1)
      • 일상 (1)
        • wedding (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 인기 글

  • 태그

    Next.js
    CrossOrigin
    코테
    타입스크립트
    javascript
    컬렉션
    pick
    이터러블
    playwright
    WebSocket
    고차함수
    yjs
    omit
    preconnect
    Android
    Utility
    TypeScript
    배열
    주니어
    CRDT
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
프론트엔드 개발자 jbeat
브라우저 동작 원리 - 화면이 그려지기까지
상단으로

티스토리툴바