main-logo

웹브라우저는 바쁘다

URL입력부터 웹페이지가 표시되기까지 브라우저 안에서 일어나는 일들

profile
나만애옹이없어
2025년 03월 12일 · 0 분 소요

들어가며

URL을 입력하고 엔터키를 누르면 눈앞에는 웹페이지가 순식간에 보입니다. 이 짧고도 간단한 행위 뒤에 브라우저에서는 어떤 일이 펼쳐지고 있을까요?

거의 매일같이 웹을 탐색하는 우리지만 사실 그 내부에서는 어떤 복잡한 일이 벌어지고 있는지 생각해 볼 기회는 많지 않았던 것 같습니다. 이번 글에서는 그다지 관심 가지지 않았던 브라우저의 바쁜 일상에 대해서 간단히 알아보고 프런트 엔드 개발과 사용자 경험 측면에서도 생각해 볼 부분은 없는지 알아보고자 합니다.

 

브라우저가 웹페이지를 표시하는 과정

웹사이트를 사용자에게 보여주는 일련의 과정을 위해 브라우저 안에서는 실제로 복잡하고 다양한 작업이 수행됩니다. 프런트 시점에서 단계별로 살펴보겠습니다.

1. 요청

사용자가 주소를 입력하면 HTTP 통신을 통해 브라우저는 서버에 요청을 보냅니다. 이 과정에서 DNS 서버를 조회하는 등의 작업도 수반됩니다. 다른 여러 작업도 진행되지만 자세한 내용은 생략하겠습니다.

2. 응답

서버는 요청받은 정보를 바탕으로 클라이언트로 데이터를 보내줍니다. 응답은 일반적으로 상태 코드, 헤더, 본문으로 구성되어 있고 본문에는 HTML, CSS 같은 여러 리소스가 포함되어 있습니다. 브라우저는 서버로부터 응답을 받으면 상태 코드를 확인하고(200 OK, 404 Not Found 등), 헤더 정보를 분석한 후, 본문 내용을 적절히 처리합니다. 응답이 HTML 문서인 경우 브라우저는 이제 본격적으로 렌더링 과정을 시작하게 됩니다.

3. HTML 파싱과 DOM 생성

브라우저는 받은 HTML을 파싱 하여 DOM(Document Object Model)을 생성합니다. DOM은 HTML 태그의 계층 구조를 객체 기반의 트리 구조로 표현한 것입니다.

파싱 단계에서 <script> 태그를 만나면 브라우저는 DOM 생성을 일시 중지하고 스크립트를 다운로드하고 실행합니다. 이는 JavaScript가 DOM에 접근하여 구조를 변경하거나 생성할 수 있기 때문입니다.

스크립트를 처리하는 것은 실제로 여러 구간에서 일어날 수 있습니다. 그렇기에 스크립트 로드에 관해 async, defer 같은 속성이 제공되고 있죠.

4. CSSOM 생성

CSS를 참조하여 CSSOM(CSS Object Model)을 생성합니다. CSSOM은 CSS 스타일 정보의 트리 구조 표현입니다. 이 과정에서 브라우저는 모든 CSS의 계단 구조를 기준으로 분석, 계산하여 각 DOM에 적용할 최종 스타일을 결정합니다.

5. 렌더 트리 구성

DOM과 CSSOM을 결합하여 렌더 트리를 생성합니다. 렌더 트리에는 화면에 실제로 표시될 요소만 포함됩니다. display: none과 같은 속성이 적용된 요소나 <head> 영역의 요소들은 렌더 트리에 포함되지 않아요.

6. 레이아웃 계산

레이아웃 계산 단계에서는 브라우저가 각 요소의 정확한 위치와 크기를 결정합니다. 렌더 트리의 모든 요소에 대해 화면에서 어디에 어떤 크기로 표시될지를 계산하는 과정입니다.

이 단계에서 브라우저는 뷰포트 내에서의 정확한 좌표값을 계산하고, 각 요소의 박스 모델 치수를 확정합니다. 여기서 말하는 박스 모델이란 우리들에게는 매우 익숙한 개념으로, 콘텐츠 영역, 패딩, 테두리, 마진으로 구성된 요소의 전체적인 구조를 의미합니다.

7. 페인팅

계산된 레이아웃을 기반으로 각 요소들을 픽셀로 변환하여 화면에 그립니다. 이 단계에서는 텍스트, 색상, 이미지, 테두리, 그림자 등 요소의 모든 시각적인 부분을 다룹니다.

8. 추가 작업

초기 렌더링 이후에도 브라우저는 계속해서 다음과 같은 작업을 수행합니다.

  • JavaScript 이벤트 처리 준비
  • 외부 리소스 다운로드
  • 비동기 요청 처리
  • 사용자 상호작용에 대한 응답

이렇게 여러 단계를 거쳐 요청했던 웹페이지가 우리 눈앞에 그려지게 되는 것이죠!

 

리플로우와 리페인팅

반응형 웹은 사용자의 접속 디바이스나 브라우저의 크기 등에 반응하여 최적화된 화면을 사용자에게 보여줍니다. 그런데 사용자가 브라우저 창 크기를 변경하거나, JavaScript로 DOM 요소의 크기나 위치를 변경할 때 브라우저 내부에서는 또 무슨 일이 일어날까요?

브라우저는 DOM의 변화가 감지되면 변경된 요소의 크기나 위치 등을 다시 계산해야 합니다. 계산 결과 레이아웃이 변경되었다면 변경된 점을 화면에 반영해야겠죠. 이 과정이 바로 리플로우와 리페인팅입니다.

리플로우

리플로우는 요소의 크기나 위치가 변경되었을 때 화면에 어떻게 배치될지를 다시 계산하는 것입니다. 다음과 같은 상황에서 발생됩니다.

  • 윈도우 또는 요소의 크기 (위치) 변경
  • DOM 요소 추가/제거
  • 폰트, 이미지 크기 변경
  • 스타일 계산값 변경 (특히 레이아웃 관련 속성)
  • CSS 애니메이션 실행
  • getComputedStyle()과 같은 계산된 스타일 정보 요청

리페인팅

리페인팅은 화면의 픽셀을 다시 그리는 과정으로, 요소의 시각적 스타일이 변경되었을 때 발생합니다. 레이아웃에는 영향을 주지 않지만 외관이 변화하는 상황에서 발생됩니다.

  • 리플로우 이후
  • 배경색, 텍스트 색상, 그림자 등 시각적 스타일만 변경
  • 보여지는 요소의 변경 (opacity, visibility 등)

성능 최적화

리플로우는 비용이 매우 큰 작업입니다. 예를 들어, 어떤 박스 모델의 크기가 변경되었다면 해당 DOM뿐 아니라 하위에 포함된 모든 요소들의 크기를 다시 계산해야 합니다. 부모 요소의 크기에 따라서 자식 요소들의 크기도 변경될 수 있기 때문이죠. 특정 경우라면 페이지 전체를 다시 계산해야 할 수도 있습니다. 리페인팅은 리플로우에 비해 상대적으로 가벼운 작업이지만, 스타일의 복잡도에 따라 충분히 무거워질 수 있습니다. 이 또한 너무 과할 경우에 성능 저하를 피할 수 없겠죠.

리플로우와 리페인팅을 최소화하여 성능을 확보하는 몇 가지 이론을 간단한 예제를 통해서 확인해 보겠습니다. (예제를 제공해 준 claude 고맙습니다.)

DOM의 조작을 최소화하는 방향으로 처리

// 비효율적인 방식 (여러 번의 리플로우/리페인팅 발생)
for (let i = 0; i < 100; i++) {
  document.body.appendChild(document.createElement('div'));
}

// 효율적인 방식 (한 번의 리플로우/리페인팅만 발생)
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
  fragment.appendChild(document.createElement('div'));
}
document.body.appendChild(fragment);

다양한 스타일 계산이 필요한 경우라면 한 번에 처리할 수 있는 방향으로 작업

// 비효율적인 방식 (여러 번의 스타일 계산)
element.style.width = '100px';
element.style.height = '200px';
element.style.margin = '10px';

// 효율적인 방식 (한 번의 스타일 계산)
element.classList.add('new-dimensions');
// CSS: .new-dimensions { width: 100px; height: 200px; margin: 10px; }

이 외에도 다양한 최적화 방법이 있습니다.

  • 위치 변경 요소를 문서 흐름에서 분리하여 변경한 후 다시 삽입
  • 레이아웃 스레싱(Layout Thrashing) 방지
  • transform 같은 CSS 애니메이션의 하드웨어 가속 속성을 활용
  • will-change 속성을 사용하여 브라우저에게 미리 알려준다.

 

가상 DOM

과거 jQuery가 대세였던 시절이 있었습니다. jQuery는 DOM을 직접 선택하고 조작하는 데 편리한 API를 제공했습니다. 하지만 위에서 언급했듯이 잦은 DOM 조작은 성능 저하의 직접적인 원인이 되었기에 jQuery를 활용한 프런트엔드 개발은 복잡하고 다이나믹한 구성의 웹페이지를 개발하기에는 어느 정도 한계가 있었습니다. 특히 웹 애플리케이션이 점점 복잡해지고 트랜드가 민감해지면서 jQuery만으로는 상태 관리와 UI 업데이트를 효율적으로 처리하기 어려워졌습니다. 이후 모던 웹의 시대가 열리면서 등장한 React나 Vue 같은 프레임워크는 이러한 문제를 해결하기 위해 혁신적인 가상 DOM 기술을 도입했습니다.

가상 DOM 이란 실제 DOM 구조의 일종의 복사본을 메모리상에 올려두는 기술입니다. DOM의 변화가 감지되면 먼저 가상 DOM에서 변경사항을 적용하고, 실제 DOM과 비교한 후, 변경된 부분만 실제 DOM에 반영합니다.

가상 DOM의 작동 방식

  1. 컴포넌트의 상태가 변경되는 것을 감지
  2. 변경된 상태를 반영한 새로운 가상 DOM 트리를 생성
  3. 이전과 비교하여 바뀐 가상 DOM의 차이점을 계산
  4. 계산된 차이점만 실제 DOM에 반영

작동 방식에서 알 수 있듯이 직접 DOM을 조작하지 않는 방법으로 리플로우와 리페인팅의 발생을 최소화하여 성능 문제를 해결했습니다. 문득 가장 많이 사용되는 프레임워크인 React와 Vue의 가상 DOM은 어떤 차이가 있는지 궁금해져서 AI에게 설명을 부탁해 봤습니다.


나 : “React와 Vue의 가상 DOM 차이점을 설명해 주세요.”


React와 Vue의 가상 DOM 차이

React와 Vue 모두 가상 DOM을 사용하지만, 그 구현 방식과 접근법에는 몇 가지 주요 차이점이 있습니다.

변경 감지 메커니즘

React:

  • 명시적인 상태 관리를 사용합니다. setState() 또는 useState() Hook을 통해 상태가 변경되었음을 React에 알립니다.
  • 상태나 props가 변경되면 해당 컴포넌트와 그 하위 컴포넌트 전체를 다시 렌더링합니다.
  • 변경 사항을 감지하려면 개발자가 명시적으로 상태 변경을 알려야 합니다.

Vue:

  • 반응형 시스템을 사용합니다. 데이터 속성을 getter/setter로 변환하여 자동으로 변경 사항을 감지합니다.
  • 상태가 변경되면 Vue는 정확히 어떤 컴포넌트가 다시 렌더링되어야 하는지 알 수 있습니다.
  • Vue 3에서는 Proxy 기반 반응형 시스템으로 업그레이드되어 더 효율적인 변경 감지가 가능합니다.

렌더링 최적화

React:

  • 기본적으로 컴포넌트 상태나 props이 변경되면 해당 컴포넌트와 모든 하위 컴포넌트를 다시 렌더링합니다.
  • 불필요한 렌더링을 방지하기 위해 shouldComponentUpdate, React.memo, PureComponent 등의 최적화 도구를 제공합니다.
  • 개발자가 명시적으로 최적화 전략을 적용해야 합니다.

Vue:

  • 컴포넌트 종속성을 자동으로 추적하여 실제로 변경된 데이터에 의존하는 컴포넌트만 다시 렌더링합니다.
  • 대부분의 경우 추가적인 최적화 없이도 효율적으로 작동합니다.
  • 명시적인 최적화 코드가 적게 필요합니다.

템플릿 vs JSX

React:

  • JSX를 사용하여 UI를 구성합니다. JavaScript 내에서 HTML과 유사한 구문을 사용합니다.
  • 가상 DOM 생성 및 업데이트를 위해 JavaScript 함수를 사용합니다.

Vue:

  • HTML 기반 템플릿 구문을 사용하며, 이를 내부적으로 렌더링 함수로 컴파일합니다.
  • 템플릿을 통해 더 정적인 분석이 가능하여 컴파일 시점에 더 많은 최적화를 적용할 수 있습니다.

이러한 차이점들로 인해 Vue는 종종 "즉시 사용 가능한" 성능 최적화를 제공하는 반면, React는 더 명시적인 접근 방식을 취하고 개발자에게 더 많은 제어권을 제공합니다.


… 단순히 차이점만 물어봤는데 디테일한 부분까지 알려줘서 놀라웠습니다..

 

외부 리소스 로딩

브라우저는 초기 HTML 파싱 이후에도 다음과 같은 리소스를 계속해서 로드합니다. 다음과 같은 외부 리소스가 있겠죠.

  1. 이미지: 웹페이지에서 가장 흔한 추가 리소스로, <img> 태그 또는 CSS 배경 이미지로 사용합니다.
  2. 폰트: 웹 폰트도 외부 리소스로 분류됩니다.
  3. 미디어 파일
  4. 추가 스크립트: 비동기 데이터 요청으로 필요한 시점에 추가적으로 로드할 수도 있죠.

리소스 로딩을 최적화하기 위해 지연 로딩(Lazy Loading), 사전 로딩(Preloading)등의 기법을 사용하기도 합니다.

<!-- 이미지 지연 로딩 -->
<img src="placeholder.jpg" data-src="actual-image.jpg" loading="lazy" alt="설명">

<!-- 리소스 사전 로딩 -->
<link rel="preload" href="important.js" as="script">

이벤트 처리

웹페이지는 사용자의 조작에 따른 다양한 이벤트를 처리할 준비도 해야 합니다. 사용자의 상호작용에 따라서 DOM이 변경될 수도 있고 필요하다면 화면을 업데이트해야 하기 때문이죠.

리플로우와 리페인팅에 영향을 크게 미칠 수 있는 이벤트 중에 스크롤 이벤트가 있습니다. 스크롤은 매우 빈번하게 발생되며 UI 개발시 자주 제어하는 경우가 많기에 적절한 최적화 방법을 알아두면 성능 최적화에 도움이 될 겁니다.

  • 스로틀링 기법 적용
  • requestAnimationFrame을 사용하여 브라우저 렌더링 주기에 맞춤

와 같은 방법을 생각해 볼 수 있습니다.

또한, 사용자 상호작용으로 인해서 요소의 크기나 위치가 다이나믹하게 변화하는 구성을 가진 웹페이지의 경우라면 그에 맞는 이벤트 최적화 기법을 적용해서 성능을 확보하는 고민이 필요하겠죠. 다양한 상황에 대응하는 처리 방법에 대해서도 잘 알고 있어야겠다는 생각이 드네요.

 

마치며

처음에는 단순히 브라우저 내부에서 무슨 일이 벌어지는지 정확히 알고 싶다는 궁금증이 성능 최적화라는 부분까지 이어졌네요. 그동안은 그저 추상적인 개념만 잡혀 있던 내용이었는데 구체적으로 알고 보니 상당히 재미있었습니다.

특히 요즘 관심 있어 하던 성능 최적화와 연관되어 생각할 거리가 많았던 점이 고무적이었습니다. 이론으로만 알고 있던 각 프레임워크의 가상 DOM 작동 방식과 리플로우와 리페인팅의 개념과 작동 방식을 이해함으로써 보다 쾌적하고 매끄러운 사용자 경험을 제공하는 웹페이지를 개발할 수 있다는 점에서 도움이 많이 되었던 시간이었습니다.