웹 개발의 3요소: HTML, CSS, JavaScript
서론
웹 페이지를 만들 때 가장 기본이 되는 세 가지 언어는 HTML, CSS, JavaScript 입니다. 프로그래밍에 관심이 있다면 누구나 들어봤을 용어지만, 웹 개발을 처음 접하는 입장에서는 이들 기술의 동작과 상호작용을 먼저 이해하는 것이 중요하다고 생각합니다. 단순한 사용법이 아니라, 브라우저 내부에서 HTML/CSS/JS(JavaScript)가 처리되는 흐름, DOM(Document Object Model)과 렌더링 엔진의 역할, JavaScript의 이벤트 루프와 비동기 처리 방식 등 기본 원리를 빠르게 알아보겠습니다. 본 글에서는 다음과 같은 내용을 다룹니다.
- HTML: 구조를 정의하는 마크업 언어의 역할, 태그와 DOM의 관계, HTML5에서의 발전 사항
- CSS: 스타일을 지정하는 방법(CSSOM 개념), 캐스케이딩 원리, 렌더링 과정에서의 리플로우/리페인트 개념
- JavaScript: 상호작용 구현의 핵심 언어, 동기/비동기 처리 구조, 이벤트 루프, 실행 컨텍스트, 싱글 스레드 환경의 의미
- 세 기술의 역할 분리와 협업: 웹 페이지 로딩 순서와 렌더링 파이프라인 상에서 각 기술의 책임
- 브라우저의 처리 흐름: 브라우저가 HTML, CSS, JS를 읽고 화면을 그리는 과정
- 실무 적용: 웹 개발자가 이러한 원리를 이해함으로써 실제 개발에서 얻게 되는 이점과 활용 방안
HTML – 구조를 정의하는 언어 (Structure and the DOM)
HTML(HyperText Markup Language)은 웹 페이지의 뼈대를 만드는 마크업 언어(markup language)입니다.
말 그대로 콘텐츠를 “마크업”하여 어떤 부분이 제목이고, 단락이며, 목록이고, 이미지인지 등을 태그(tag)로 표시합니다.
HTML 파일이 브라우저에 로드되면, 브라우저의 HTML 파서(parser)가 이를 해석하여 DOM이라는 트리 구조의 객체 모델을 구성합니다.
DOM은 Document Object Model의 약자로, HTML 요소를 트리 구조로 표현한 객체 집합입니다.
각 HTML 태그는 DOM tree의 하나의 노드(node)로 변환되며, 부모-자식-형제 관계를 통해 문서 구조를 계층적으로 표현합니다.
예를 들어 <html>
태그가 트리의 루트가 되고, 그 안에 <body>
등이 자식으로, <body>
안에 <h1>
, <p>
같은 요소들이 또 자식 노드로 연결되는 식입니다.
DOM tree는 웹 페이지의 실시간 구조 표현이기 때문에, JavaScript를 통해 이 트리에 접근하여 노드를 변경하면 화면의 내용도 동적으로 바뀌게 됩니다. 즉, HTML 소스 코드는 웹 페이지의 정적인 선언이고, DOM은 브라우저 메모리 상에서 동적으로 조작 가능한 페이지 구조라고 볼 수 있습니다. DOM API를 사용하면 JavaScript로 문서의 제목을 바꾸거나, 새로운 요소를 추가/삭제하는 등이 가능한데, 이는 모두 이 DOM tree를 조작하는 것입니다.
HTML5의 등장과 시맨틱 마크업
HTML은 초창기부터 여러 버전을 거쳐 발전해왔으며, HTML5는 그 중 가장 큰 변화를 가져온 표준입니다.
HTML5에서는 단순한 문서 표시를 넘어 웹 애플리케이션 시대에 맞춰 다양한 새 기능과 시맨틱 요소들이 도입되었습니다.
예를 들어 이전에는 <div>
로 막연히 구획을 나누고 클래스 이름으로 의미를 부여하던 것을, HTML5부터는 <header>
, <nav>
, <article>
, <section>
, <footer>
등 의미론적(semantic)인 태그들을 제공함으로써 문서 구조의 의미를 분명히 나타낼 수 있게 되었습니다.
이러한 시맨틱 요소들은 브라우저나 검색 엔진, 보조 기술(스크린 리더 등)에 콘텐츠의 목적을 명확히 알려주므로 접근성 향상과 SEO(Search Engine Optimization; 검색 엔진 최적화) 측면에서 유리합니다.
또한 <video>
, <audio>
같은 멀티미디어 태그의 도입으로 플러그인 없이도 영상/음성을 재생할 수 있게 되었고, <canvas>
요소로 2D 그래픽을 스크립트로 그리는 기능도 제공됩니다.
요컨대 HTML5는 기존의 태그를 개선하고 쓸모 없어진 태그를 폐지하는 한편, 현대 웹에 필요한 새로운 요소와 API(예: Geolocation, Web Storage 등)를 대거 포함하여 웹의 표현력과 기능성을 크게 확장했습니다.
이처럼 HTML은 웹 콘텐츠의 의미와 구조를 표현하는 역할을 담당하며, 작성된 마크업은 브라우저에 의해 DOM이라는 객체 트리로 구성되어 이후 단계들의 기초가 됩니다. 다음으로는 이 구조에 스타일과 레이아웃을 적용하는 CSS의 원리에 대해 알아보겠습니다.
CSS – 스타일 규칙과 렌더링 (Styling, CSSOM, Reflow/Repaint)
CSS(Cascading Style Sheets)는 HTML(혹은 다른 마크업 언어)로 만든 구조에 스타일을 입혀주는 스타일시트 언어(style sheet language)입니다. HTML이 뼈대라면 CSS는 살을 붙여 색상, 크기, 배치, 폰트 등의 시각적 표현을 결정합니다. CSS의 작동을 이해하기 위해서는 브라우저가 CSS를 처리하는 방식을 알아봐야 합니다.
브라우저는 HTML 파싱과 병렬로 CSS 파일을 다운로드하며, CSS 파서를 통해 CSS를 해석하여 CSSOM(CSS Object Model)이라는 별도의 객체 트리를 생성합니다.
CSSOM은 DOM과 유사한 트리 구조이지만, 내용(content)이 아닌 스타일 규칙들을 담고 있습니다.
예를 들어 CSS에 h1 { color: red; }
라는 규칙이 있다면 CSSOM tree에서 h1
노드에 color: red
속성이 연결되는 식입니다.
한 가지 중요한 점은, HTML의 DOM tree와 CSSOM tree는 각각 독립적으로 만들어집니다. HTML을 모두 파싱하지 않아도 DOM tree는 차곡차곡 만들어지지만, CSSOM은 해당 CSS 파일을 모두 다운로드 및 파싱 완료하기 전까지는 사용할 수 없습니다. 이는 CSS의 C가 cascading(계단식)을 의미하는 것과 관련이 있습니다. CSS 규칙은 여러 소스(브라우저 기본 스타일, 개발자 정의 스타일시트, 사용자 스타일 등)에서 올 수 있고, 우선순위와 상속, 구체성(specificity) 규칙에 따라 최종 적용 값이 결정됩니다. 따라서 CSSOM이 완성되기 전 첫 페인트(화면 그리기)만 블로킹하고, DOM 파싱은 계속 진행됩니다 (CSS를 렌더링 차단 리소스라고 부르는 이유입니다).
CSSOM 생성이 끝나고 DOM tree도 준비되면, 브라우저는 이 둘을 결합하여 렌더 트리(render tree)를 만듭니다.
Render tree는 실제 화면에 표시될 노드들만으로 구성된 트리로, 각 노드에 적용될 스타일(계산된 스타일) 정보를 포함합니다.
예컨대 <body>
안에 <p>
가 있다면 DOM tree에는 <p>
노드가 있고, CSSOM에는 p { font-size:16px; }
규칙이 있을 때, render tree에는 <p>
노드에 font-size:16px
이 적용된 형태로 나타나는 것입니다.
참고로 display: none
처럼 아예 표시되지 않는 요소는 render tree에서 제외되고, visibility: hidden
처럼 공간만 차지하는 요소는 포함되는 식으로 렌더링에 필요한 노드만 걸러집니다.
출처: https://web.dev/articles/critical-rendering-path/render-tree-construction
Render tree가 구성되었으면, 브라우저는 리플로우(reflow) 단계를 진행합니다. 레이아웃(layout)이라고도 부릅니다. Reflow는 각 render tree 노드, 즉 각 요소의 정확한 위치와 크기를 계산하는 단계입니다. 브라우저 엔진(browser engine) 혹은 레이아웃 엔진(layout engine)이라고도 불리는 브라우저의 렌더링 엔진(rendering engine)은 화면(viewport) 크기를 기준으로, render tree의 루트부터 모든 요소의 박스 모델 (width, height, margin, padding, border 등)을 적용하여 배치합니다. 최초의 레이아웃은 초기 렌더링이지만 이후 문서 구조나 스타일이 변경되어 다시 배치가 필요한 상황이 생기면 부분적으로든 전체적으로든 reflow가 일어납니다. 예를 들어 스크립트로 새로운 DOM 노드를 추가하거나, CSS 스타일을 바꾸어 크기가 달라지면 브라우저는 변경된 부분의 레이아웃을 다시 계산하고, 필요하면 연쇄적으로 다른 요소 배치도 조정합니다. 이런 reflow는 성능 비용이 큰 작업에 속하므로, 빈번하게 발생하지 않도록 하는 것이 중요합니다.
Reflow가 완료되면 이어서 페인트(paint) 단계, 즉 화면 그리기를 수행합니다. Paint 단계에서는 render tree의 각 요소 노드에 대해 픽셀 단위로 화면에 렌더링합니다. 텍스트 내용이라면 글자 형태로 그려지고, 배경색이나 테두리, 그림자가 있으면 해당 스타일대로 픽셀이 칠해집니다. 모든 요소를 그리면 비로소 우리가 보는 웹 페이지의 비주얼이 완성됩니다. 경우에 따라 복잡한 페이지는 그림을 그릴 때 여러 개의 레이어(layer)로 쪼개 GPU 가속을 활용하기도 하는데, 이는 성능을 높이기 위한 최적화 기법입니다. 마지막으로 여러 레이어가 있다면 이를 합성(compositing)하여 최종 화면을 구성합니다.
페인트 이후에도 사용자와의 상호작용이나 JS 실행으로 인해 스타일이나 레이아웃이 변경되면, 리페인트(repaint)나 reflow+repaint가 다시 발생할 수 있습니다.
Repaint는 배치에는 영향 없고 시각적 속성만 바뀔 때(예: 색상 변경) 화면을 다시 그리는 것이고, reflow는 레이아웃 자체가 바뀌는 경우(예: 크기 변화, 위치 변화) 배치 계산부터 다시 하고 그리기까지 하는 것을 의미합니다.
Reflow는 repaint보다 비싸기 때문에, CSS 속성 중에서도 레이아웃을 변경시키는 속성(transform이나 opacity 등은 레이아웃에 영향 안 주지만 width, height, position 등은 영향)들을 다룰 때는 신중해야 합니다.
예컨대, 애니메이션을 할 때 top
이나 left
대신 CSS transform
을 사용하는 것이 reflow를 줄여 성능에 유리합니다.
정리하면, CSS의 역할은 HTML 구조에 스타일을 입히는 것입니다. 브라우저는 CSSOM을 만들고, DOM과 결합해 render tree를 생성한 다음, 레이아웃(배치)과 페인트(그리기) 과정을 거쳐 최종 화면을 구성합니다. CSS의 cascading(계단식) 특성 덕분에 여러 스타일 규칙이 충돌할 경우 브라우저는 우선순위 (!important 여부, 스타일 출처(사용자/브라우저/개발자), 구체성, 소스 상의 나중 위치 등)를 따져 최종 스타일을 결정하게 됩니다. 이제 마지막으로, 여기에 동적 동작을 부여하는 JavaScript의 원리를 살펴보겠습니다.
JavaScript – 인터랙티브한 동작의 엔진 (Interaction, Event Loop, Async)
JavaScript는 현대 웹에서 인터랙션(interaction; 상호작용)을 구현하는 데 필수적인 프로그래밍 언어입니다. HTML이 구조, CSS가 표현이라면, JavaScript는 동작을 담당합니다. 버튼을 클릭했을 때 이벤트를 처리한다든지, 서버와 통신해 데이터를 가져오고, DOM을 조작해 화면을 갱신하는 등 모든 동적인 행동은 JavaScript로 구현됩니다.
싱글 스레드와 실행 컨텍스트
JavaScript의 핵심 특징 중 하나는 싱글 스레드(single-threaded) 언어라는 점입니다. 즉, 한 번에 하나의 작업만 처리할 수 있어 동시에 둘 이상의 JavaScript 코드가 실행되지 않습니다. 다만 Web Worker 등을 이용하면 추가 스레드에서 JS를 병렬 실행할 수 있습니다. JavaScript 엔진(예: V8, SpiderMonkey 등)은 콜 스택(call stack) 하나로 현재 실행 중인 함수와 연산을 관리합니다.
함수를 호출하면 해당 함수의 실행 컨텍스트(execution context)가 생성되어 스택에 쌓이고, 함수 실행이 종료되면 스택에서 제거됩니다.
실행 컨텍스트는 언어 수준의 추상화로, 렉시컬 환경(lexical environment), 변수 환경(variable environment), 그리고 this
바인딩을 포함하여 인터프리터가 코드 실행에 필요한 모든 정보를 담습니다.
반면, 스택 프레임(stack frame)은 런타임(메모리) 수준의 구조로, 실행 컨텍스트 내부의 변수와 매개변수, this
값, 실행 흐름 정보 등을 실제로 저장하고 관리하는 단위입니다.
이러한 구조 덕분에 JavaScript는 run-to-completion 모델을 따릅니다. 즉, 한 번 시작된 함수나 콜백(callback; 인수로 전달되는 함수) 은 중간에 다른 코드로 중단되지 않고, 한 번에 완전히 실행된 후에야 다음 작업이 실행됩니다. 예를 들어, 클릭 이벤트의 콜백 함수는 실행이 시작되면 반드시 끝까지 실행된 후에야 다른 이벤트나 코드가 처리됩니다. 이를 통해 멀티스레드 환경에서 발생하기 쉬운 레이스 컨디션(race condition) 문제를 줄일 수 있습니다.
그러나 반대로 함수 실행 시간이 길어질 경우(예: 5초 이상 소요되는 계산), 그동안 콜 스택이 차단되어 UI 업데이트나 사용자 입력 처리가 멈추는 단점이 있습니다. 이로 인해 웹 페이지가 응답 없음 상태가 되기도 합니다.
이 문제를 해결하기 위해 JavaScript는 비동기 처리(asynchronous handling) 및 이벤트 기반(event-driven) 구조를 사용합니다.
브라우저 환경에서 JavaScript는 단일 스레드지만, 브라우저가 제공하는 Web API(예: setTimeout
, AJAX 요청, DOM 이벤트 리스너 등)와 내부적으로 동작하는 백그라운드 스레드를 활용해 시간이 오래 걸리는 작업을 비동기로 실행하고, 작업 완료 시 결과를 메인 스레드에 이벤트 형태로 전달합니다.
이러한 과정을 제어하는 핵심 메커니즘이 바로 이벤트 루프(event loop)입니다.
동기(synchronous) vs 비동기(asynchronous) 및 이벤트 루프 원리
JavaScript 엔진은 싱글 스레드에서 동작하기 때문에 한 번에 하나의 작업만 처리합니다.
이때 동기(synchronous) 코드는 작성된 순서대로 한 줄씩 실행되어 앞선 작업이 완료되어야 다음 줄로 넘어갑니다.
예컨대 function A() { /*오래 걸리는 작업*/ }
을 호출한 뒤에는 A가 완전히 끝나야만 console.log('B')
가 실행됩니다.
반면 비동기(asynchronous) 코드는 네트워크 요청, 타이머, 이벤트 리스너, 파일 I/O 등 결과가 준비되는 시점에 실행되도록 뒤로 미룹니다.
setTimeout(fn, 1000)
, fetch()
, Promise
, async/await
같은 API를 통해 콜백이나 후속 처리 로직이 나중에 실행되도록 예약하는 것이죠.
예를 들어
setTimeout(() => console.log('after 1s'), 1000);
console.log('immediate');
를 실행하면 “immediate”가 즉시 찍히고 1초 뒤에야 “after 1s”가 출력됩니다.
이 비동기 작업들을 관리하는 중심에 이벤트 루프(event loop)가 있고, 그 아래에는 크게 세 가지가 작동합니다.
- 콜 스택(call stack): 현재 실행 중인 함수들이 쌓이는 LIFO 구조로, 동기 코드가 전부 여기서 처리됩니다.
- 매크로태스크 큐(macrotask queue; task queue):
setTimeout
, DOM 이벤트, I/O 콜백 등 주요 비동기 작업의 콜백이 등록되는 큐 - 마이크로태스크 큐(microtask queue):
Promise.then
,queueMicrotask
,MutationObserver
등 우선순위가 높은 콜백이 모이는 별도의 큐
이벤트 루프는 콜 스택이 비워질 때마다, 먼저 microtask 큐를 모두 처리하고, 그 다음 macrotask 큐에서 하나를 꺼내 실행하는 과정을 반복합니다.
이 구조 덕분에 Promise.then
같은 microtask는 같은 “0ms 지연”을 가진 setTimeout
콜백보다도 항상 먼저 실행됩니다.
아래 예시를 이해하면 차이가 분명해집니다.
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
- 동기
console.log('A')
,console.log('D')
가 차례로 실행되어 “A”와 “D”를 출력합니다. - 스크립트가 끝난 뒤 이벤트 루프는 먼저 microtask 큐로 가서
Promise.then
콜백인 “C”를 처리합니다. - microtask가 모두 비워지면 이제 macrotask인
setTimeout
콜백 “B”를 실행합니다.
따라서 최종 출력 순서는 A → D → C → B가 됩니다.
이처럼 이벤트 루프와 두 개의 큐를 통해 JavaScript는 단일 스레드에서도 non-blocking하게 비동기 작업을 효율적으로 처리하고, UI 렌더링이나 사용자 입력과 같은 중요한 작업이 지연되지 않도록 보장합니다.
정리하면, JavaScript 엔진은 싱글 스레드(single-threaded)로 동작하며, 콜 스택(call stack)과 실행 컨텍스트(execution context) 개념으로 동기 코드를 처리합니다. 비동기 작업은 이벤트 루프(event loop)와 마이크로/매크로태스크 큐(micro/macrotask queue) 메커니즘을 통해 실행 순서를 조율합니다. 이러한 동작 원리를 이해하면, 왜 어떤 코드는 즉시 실행되고 어떤 코드는 나중에 실행되는지, UI가 왜 특정 상황에서 멈출 수 있는지 등의 질문에 답할 수 있게 됩니다.
세 기술의 역할 분리와 협업
지금까지 HTML, CSS, JavaScript 각각의 동작 원리를 살펴보았는데, 실제 웹 개발에서는 이 세 가지가 동시에 한 페이지 안에서 상호작용합니다. 웹의 위대함은 HTML로 구조를, CSS로 스타일을, JS로 동작을 기술하는 관심사 분리(separation of concerns; 디자인 원칙의 하나)에 있습니다. 이렇게 역할을 분리하면 유지보수가 쉬워지고, 디자이너와 개발자가 각자 맡은 부분에 집중할 수 있으며, 코드의 재사용성과 가독성도 높아집니다. 하지만 분리되어 있다고 해서 완전히 독립적인 것은 아니며, 브라우저 안에서는 이들이 유기적으로 협업하여 하나의 최종 결과(웹 페이지)를 만들어냅니다.
로딩 순서와 상호 영향 (Loading Sequence and Dependencies)
웹 페이지를 여는 순간을 생각해봅시다.
브라우저는 우선 HTML 파일을 다운로드하여 파싱을 시작합니다.
HTML 코드에는 <head>
섹션에 <link rel="stylesheet">
로 CSS 파일을 링크하거나 <script>
태그로 JS 파일을 포함하기도 합니다.
브라우저의 기본 동작은 HTML을 해석하면서 이러한 외부 리소스를 만나면 즉시 요청을 보내 로드하는 것입니다.
다만, 로드된 CSS와 JS가 HTML 파싱 및 렌더링에 영향을 주는 방식은 서로 다릅니다.
- CSS: 앞서 언급했듯 CSS는 렌더링을 차단(render-blocking)하는 리소스입니다. HTML 파싱 도중
<link rel="stylesheet">
를 만나면, 해당 CSS 파일을 다운로드하는 한편 DOM 생성을 계속 진행합니다. 그러나 CSSOM이 완성되기 전까지는 최종 렌더링(화면 그리기)을 지연시킵니다. 이는 CSS 규칙이 로딩 완료 후 한꺼번에 적용되어야 일관된 스타일을 입힐 수 있기 때문입니다. 따라서 개발 시<link>
태그는 가능하면<head>
에 배치하여 브라우저가 초반에 CSS를 빨리 불러오도록 하는 것이 좋습니다. - JavaScript (일반):
<script>
태그를 만나면 상황이 조금 복잡해집니다. 기본적으로 외부 JS 파일을<script src="...">
로 포함한 경우, 브라우저는 HTML 파싱을 중단하고 해당 스크립트를 다운로드받아 즉시 실행합니다. 왜 그럴까요? JavaScript는 DOM을 조작할 수도 있고, 아직 만들어지지 않은 DOM 요소를 접근하려 하면 오류가 나기 때문에, 브라우저는 안전을 위해 스크립트를 먼저 실행하고 나서 계속 HTML을 파싱하도록 설계되어 있습니다. 이로 인해 스크립트가 크거나 많으면 HTML 파싱이 빈번히 멈추게 되어 초기 페이지 로딩이 지연될 수 있습니다. 또한, 어떤 스크립트는 스타일 정보를 필요로 할 수도 있습니다. 예를 들어 JS에서getComputedStyle
을 호출한다면, 브라우저는 해당 JS를 실행하기 전에 CSSOM이 준비될 때까지 기다렸다가 실행합니다. 종합하면, 기본 설정의<script>
는 렌더링 파이프라인에서 HTML 파싱을 막고, 때로는 CSS 파싱 완료까지도 기다리게 만드는 존재입니다. - JavaScript (defer/async): 이러한 JS의 단점을 보완하기 위해 HTML5에서는
<script>
태그에defer
혹은async
속성을 사용할 수 있습니다.defer
속성은 스크립트를 백그라운드에서 다운로드하면서도 HTML 파싱을 막지 않게 하고, HTML 파싱이 모두 끝난 후에 해당 스크립트를 실행하도록 합니다. 여러defer
스크립트는 순서대로 실행되며, DOM 생성이 완료된 후 실행되므로 DOM을 안전하게 다룰 수 있다는 장점이 있습니다. 한편async
속성은 스크립트를 백그라운드로 가져오는 것은 같지만, 다운로드 완료되는 대로 즉시 실행해버립니다. 따라서 HTML 파싱 중이라도async
스크립트가 준비되면 곧바로 실행하므로 파싱이 그 순간 잠깐 중단될 수 있습니다.async
스크립트들은 서로 순서 보장이 없지만, 가장 빨리 로드된 순서대로 실행됩니다. 일반적으로 분석 코드나 소셜 미디어 위젯처럼 본문 DOM과 강하게 의존하지 않는 스크립트는async
를, 주요 애플리케이션 로직처럼 DOM이 완성된 뒤에 실행되어야 하는 것은defer
를 사용하면 페이지 로딩 성능과 기능을 모두 잡을 수 있습니다.
요컨대, HTML, CSS, JS는 각자 따로 로딩되지만 결과적으로 같은 페이지를 그리기 때문에 적절한 협업이 필요합니다. HTML은 구조를 제공하고, CSS는 모양을 입히며, JS는 이를 동적으로 변화시키거나 사용자 입력에 반응하게 합니다. HTML 코드의 위치, CSS와 JS의 로딩 방식에 따라 페이지 초기 로딩 속도나 렌더링 순서가 크게 달라질 수 있음을 유념해야 합니다.
브라우저의 렌더링 파이프라인 흐름
이제 브라우저가 HTML/CSS/JS를 받아 화면에 픽셀을 그리기까지의 전체 흐름을 정리해보겠습니다.
출처: https://web.dev/learn/performance/understanding-the-critical-path
- 네트워크 요청 및 응답: 사용자가 URL에 접속하면 브라우저는 해당 주소로 HTTP 요청을 보내고, 서버는 HTML 문서를 응답합니다. 이때 DNS 조회, TCP 연결, SSL 핸드셰이크 등의 네트워크 과정을 거치지만 여기서는 생략하고, 최종적으로 브라우저가 HTML 파일을 바이트(byte) 형태로 받는 순간부터 이야기하겠습니다.
- HTML 파싱 및 DOM 생성: 브라우저는 받은 HTML 바이트를 문자로 디코딩(대개 UTF-8)한 뒤, 이를 순차적으로 파싱합니다. HTML 파서는 글자들을 토큰(token)으로 묶고, 토큰들을 요소 노드로 변환하면서 DOM tree를 구성해 나갑니다. 이 작업은 점진적으로 일어나서, HTML 파일 일부만 도착해도 그 부분까지 DOM을 만들어놓고 나머지를 계속 기다립니다. DOM tree가 생성되면서 브라우저는 곧장 화면에 렌더링하지는 않습니다. 왜냐하면 스타일 정보가 적용되지 않았고, 또 아직 남은 HTML이나 연관된 CSS/JS가 있을 수 있기 때문입니다.
- CSS 파싱 및 CSSOM 생성: HTML 파싱 도중
<link>
로 CSS를 불러오거나<style>
태그 내에 CSS가 있다면, 브라우저는 별도로 CSS 파서를 동작시켜 CSSOM tree를 만듭니다. 앞서 말했듯 CSSOM은 CSS 규칙의 트리 구조 표현입니다. CSS는 바이트가 도착하는 즉시 스트리밍으로 파싱을 시작하고, 파일 전체를 받은 뒤 완성된 CSSOM을 확정합니다. CSSOM이 완성될 때까지 브라우저는 render tree를 만들지 않고 기다리며, 따라서 화면에 콘텐츠를 그리는 동작도 일시 보류됩니다. - JavaScript 실행: HTML 파싱 도중
<script>
태그를 만나면, 앞서 설명한 규칙에 따라 해당 스크립트를 다운로드 및 실행하게 됩니다. 기본적으로는 파서를 중단하고 즉시 실행하여 DOM이나 CSSOM을 조작할 기회를 줍니다. 만약defer
스크립트라면 DOM 생성이 끝난 뒤 실행될 것이고,async
스크립트라면 내려받는 동안 파싱은 진행되다가 다운로드 완료 시점에 실행됩니다. 이때 스크립트가 DOM 구조를 변경한다면 이미 만들어진 DOM tree에 그 변경이 적용될 것이고, 스타일을 변경하면 CSSOM이나 이후 렌더 단계에 영향을 줍니다. 또한 어떤 스크립트는 DOMParser나 AJAX로 추가 HTML을 동적으로 로드하여 DOM에 삽입하기도 하는데, 이러한 경우도 동적으로 DOM이 갱신되므로, 이후 단계인 렌더 트리 생성과 레이아웃에 포함됩니다. - Render tree 생성: HTML의 DOM과 CSS의 CSSOM이 모두 준비되었다면, 브라우저는 이 둘을 조합하여 render tree를 구축합니다. Render tree는 화면에 표시될 노드들만 포함하며, 각 노드에는 적용될 최종 스타일(계산된 스타일)이 할당됩니다. 이 과정에서 CSS cascade algorithm이 적용되어, 여러 스타일 규칙 중 어떤 것이 우선인 지 결정되고 모든 상속/계산이 완료된 스타일 값을 얻게 됩니다. 예를 들어
<p>
요소가 DOM에 있고, CSSOM에p { font-size: 16px; } body { font-size: 14px; }
가 있었다면 최종적으로<p>
노드의 글꼴 크기는 16px로 계산되는 식입니다 (구체적인 우선순위는 CSS 캐스케이드 규칙에 따름). - Layout(Reflow): Render tree가 완성되면, 레이아웃 엔진이 동작하여 각 노드의 위치와 크기를 계산합니다. 이때 브라우저는 화면(viewport)의 크기, 각 요소의 CSS 박스 모델 속성(마진, 패딩 등), 글자 크기와 줄바꿈 등 모든 요소를 고려하여 좌표를 계산합니다. 이 과정은 트리의 루트부터 하위 노드로 재귀적으로 진행됩니다. 예를 들어
<body>
영역을 화면 크기에 맞게 정하고, 그 안의 첫 번째<div>
위치와 크기를 결정하고, 그<div>
안의 텍스트 노드 위치를 잡고… 이런 식으로 모든 요소의 박스(box)를 정합니다. 만약 이미지처럼 아직 크기를 알 수 없는 콘텐츠가 있다면 일단 예상치로 배치하고 나중에 실제 크기가 들어오면 레이아웃을 조정(reflow)하기도 합니다. 첫 번째 레이아웃 이후, DOM이나 스타일이 바뀌면 이 레이아웃 과정이 다시 호출되어 부분적으로 적용될 수 있습니다. - Paint(최초 그리기) / Repaint(시각적 속성 변경 시 다시 그리기): 레이아웃 단계가 끝나면, 이제 각 요소를 화면에 실제로 그리는 단계입니다. Render tree의 각 노드 (보이는 모든 요소들)에 대해 브라우저는 계산된 스타일을 참고하여 픽셀을 칠합니다. 텍스트는 화면 폰트 렌더링으로 나타나고, 색상이나 배경 이미지, 테두리 등이 시각적으로 표현됩니다. 이 때, 최적화를 위해 브라우저는 각 요소를 여러 레이어로 나눠서 그릴 수도 있습니다. 예컨대 복잡한 합성이 필요한 부분은 별도 레이어로 관리하여 GPU에서 렌더링하기도 하고, 스크롤 시 최적화를 위해 고정 요소를 독립 레이어로 만들기도 합니다. 레이아웃 결과가 바뀌지 않는 한 reflow는 일어나지 않습니다.
- Composite: 페인트된 여러 레이어가 있다면, 이를 올바른 순서로 합성(compositing)하여 최종 화면을 구성합니다. 이제 사용자에게 완전히 렌더링된 웹 페이지가 보이게 됩니다.
이 일련의 렌더링 단계들은 초기 페이지 로드 시 한 번 거치지만, 웹 앱에서는 사용자의 클릭이나 애니메이션 등으로 DOM이나 스타일이 계속 변경되며, 그때마다 렌더 트리가 부분적으로 다시 계산되고, reflow → paint/repaint → composite 사이클이 반복됩니다. 따라서 초기 로딩 속도도 중요하지만, 이러한 동적인 변화에 얼마나 효율적으로 대응하느냐도 웹 성능의 핵심 요소입니다. (예: 한 번에 수천 개의 DOM 요소를 추가하면 layout 비용이 커져 성능 저하가 발생할 수 있습니다.)
위 과정을 이해하면, 예를 들어 “왜 CSS 파일을 <head>
에 넣어야 하는지”, “왜 큰 <script>
를 <body>
아래에 두라고 하는지”, “DOM 조작을 많이 하면 왜 느려지는지” 같은 질문에 스스로 답할 수 있게 됩니다.
즉, 브라우저의 렌더링 파이프라인을 이해하는 것은 곧 웹 성능 최적화와 직결됩니다.
실무 활용
이제 한 걸음 물러서서, 왜 이런 원리를 알아야 하는지, 실제 개발에서 어떤 식으로 활용되는지 생각해보겠습니다. 학문적으로 보면 지금까지 다룬 내용은 브라우저 구현이나 언어 디자인에 가까워 보일 수 있지만, 개발자들의 실무에도 큰 영향을 미칠 수 있습니다.
- 성능 최적화: 웹 성능은 사용자 경험에 매우 중요합니다. 렌더링 파이프라인을 이해하면, 무엇이 성능을 떨어뜨리는지 명확히 알 수 있습니다. 예를 들어 리플로우(reflow)가 비싼 작업이란 걸 알면, DOM을 업데이트할 때 가능한 한 한 번에 몰아서 변경하고, 불필요한 DOM 추가/삭제를 줄이며, 레이아웃에 영향을 주지 않는 CSS 변경 (예:
transform
이나opacity
활용)으로 효과를 내려고 고민하게 됩니다. 리페인트(repaint) 역시 빈번하면 애니메이션이 버벅일 수 있으므로, CSS 애니메이션에서 매 프레임 색상 변경보다는 GPU가속 가능한 속성 변경을 택하게 될 것입니다. - 로딩 속도 개선: HTML/CSS/JS의 협업 방식을 알면 초기 로딩 성능을 높일 방법도 보입니다. 예컨대 중요한 콘텐츠를 감싸는 CSS는 inline으로 넣거나 Critical CSS로 처리해서 최초 페인트를 빠르게 할 수 있습니다. 반대로 큰 JavaScript는
defer
나async
로 불러와서 HTML 파싱을 막지 않도록 조정합니다. 이미지도 render tree 생성 전 미리 preload하거나,<img>
에width
/height
속성을 지정해 레이아웃 공간 확보 후 로드되게 하면 레이아웃 쉬프트를 줄일 수 있습니다. 이러한 최적화 기법들은 모두 브라우저가 언제 무엇을 기다리는지 이해해야 적재적소에 적용할 수 있습니다. - 디버깅과 문제 해결: 웹 개발 중 발생하는 많은 문제들은 사실 이 원리를 알면 쉽게 원인을 파악할 수 있습니다. 예를 들어 “왜 내 CSS가 적용되지 않지?”라고 할 때, CSS 캐스케이드 우선순위를 몰랐다면 힘들게 찾았겠지만, 지금은 개발자 도구에서 CSS 규칙을 보고 Specificity나 !important 여부를 따져보면 금방 원인을 알 수 있을 것입니다. 또 “왜 버튼 클릭이 가끔 반응이 느릴까?”라는 질문도, 아! 그 시간에 메인 스레드가 큰 작업(예: 대용량 배열 정렬)을 수행 중이라 이벤트 처리가 지연된 것이구나 하고 추측할 수 있습니다. 해결책으로 web worker(백그라운드 스레드)로 그 작업을 옮기는 것도 이벤트 루프를 이해한 개발자라면 떠올릴 수 있는 방법입니다.
- 모던 프레임워크 이해: React, Vue 같은 프론트엔드 프레임워크를 쓰면 이러한 것을 몰라도 개발이 되는 것처럼 보이지만, 사실 그 내부에서도 이 원리들이 적용됩니다. React의 Virtual DOM도 결국 실제 DOM 변경을 줄여 reflow 비용을 최소화하려는 전략이고, Vue의 nextTick도 microtask를 활용해 DOM 업데이트 후에 콜백을 실행하는 메커니즘입니다. 프레임워크를 깊이 있게 쓰려면 기본 플랫폼의 동작을 아는 것이 큰 도움이 됩니다.
- 접근성과 유지보수성: HTML5의 시맨틱 요소 활용은 단지 멋으로 권장되는 것이 아니라, 스크린리더 등의 접근성을 높여주고 코드의 의미를 분명히 하여 팀원 간 협업을 원활하게 합니다. 구조와 스타일과 로직을 명확히 구분하는 습관은, 문제 발생 시 원인을 특정하기 쉽게 하고 변경 요구에도 한 부분만 수정하면 되도록 만들어줍니다.
결국, 브라우저가 돌아가는 원리를 알고 있으면 개발자는 보다 견고한 웹 앱을 만들 수 있습니다. 어떤 결정을 내릴 때 근거를 가지고 “이렇게 하면 parsing이 막히겠군”, “이 코드는 메인 스레드를 너무 점유하지 않을까?” 등을 생각하며 최적의 방식을 선택하게 됩니다. 이는 곧 사용자 경험의 향상으로 이어지고, 개발자로서도 한층 수준 높은 문제 해결 능력을 갖추게 되는 길입니다.
결론
웹 개발의 기본 삼요소인 HTML, CSS, JavaScript의 원리와 역할을 구조적으로 살펴보았습니다. 요약하자면, HTML은 콘텐츠의 의미와 구조를 담은 골격이며 브라우저는 이를 DOM tree로 만든다. CSS는 스타일 규칙을 정의하고 브라우저는 CSSOM으로 파싱한 뒤 DOM과 합쳐 render tree를 구성, 배치와 그리기 과정을 거쳐 화면에 표시한다. JavaScript는 대화형(interactive) 동작을 구현하며, 싱글 스레드(single-threaded) 환경에서 이벤트 루프(event loop)와 매크로/마이크로태스크 큐(macro/microtask queue)를 통해 비동기 작업을 처리한다. 이 세 기술은 역할을 분담하면서도 브라우저 안에서 밀접히 협력하여 우리가 보는 한 페이지를 완성한다는 점도 확인했습니다.