📝

Chapter 4. 브라우저 렌더링

1. 브라우저 동작 원리를 알아야 하는 이유2. 브라우저 동작 원리2.1. 브라우저의 구조2.1.1. 브라우저란?2.1.2. 브라우저의 구성 요소2.1.2.1. 사용자 인터페이스 (User Interface)2.1.2.2. 브라우저 엔진 (Browser engine)2.1.2.3. 렌더링 엔진 (Rendering engine)2.1.2.4. 네트워킹 (Networking)2.1.2.5. UI 백엔드 (UI Backend)2.1.2.6. 자바스크립트 인터프리터 (JavaScript Interpreter)2.1.2.7. 자료 저장소 (Data Persistence)2.2. 브라우저와 서버의 통신 과정2.2.1. 브라우저와 서버의 통신 과정 - HTTP 요청과 응답2.2.2. 클라이언트 사이드 렌더링, 서버 사이드 렌더링2.2.2.1. CSR(Client Side Rendering)2.2.2.2. SSR (Server Side Rendering)3. 브라우저 렌더링 과정3.1. 파싱(Parsing)3.2. HTML 파싱 - DOM(Document Object Model)3.3. CSS 파싱 - CSSOM(CSS Object Model)3.4. 자바스크립트 파싱과 실행3.4.1. 자바스크립트 파싱과 CSS 파싱의 차이3.4.2. 자바스크립트 파싱 과정3.4.2.1. 자바스크립트 파싱 과정3.4.2.2. 파싱 결과로 생성된 AST, 도대체 무엇일까?3.4.2.3. AST 생성해보기3.4.3. 자바스크립트 실행 방법3.5. 렌더 트리3.5.1. 렌더 트리란?3.5.2 렌더 트리 형성 과정3.6. 레이아웃과 페인트3.6.1. 레이아웃3.6.2. 페인트3.6.3. 개발자 도구에서 레이어 확인하기3.7. 컴포지션4. 브라우저 렌더링 과정 최적화 방법4.1. <script> 태그의 async, defer 속성4.1.1. 스크립트 실행 과정4.1.2. Async와 Defer4.2. 리플로우, 리페인트 최적화4.2.1. 리플로우, 리페인트란?4.2.1.1. 리플로우4.2.1.2. 리페인트4.2.2. 리플로우, 리페인트 발생 확인하기4.2.2.1. 리플로우 발생 확인하기4.2.2.2. 리페인트 발생 확인하기4.2.3. 리플로우, 리페인트 최적화 방법4.2.3.1. 불필요한 DOM 요소 없애기4.2.3.2. 애니메이션이 적용된 요소에 절대 위치나 고정 위치 사용하기4.2.3.3. transform 사용하기4.3. 가상 DOM4.3.1. MPA와 SPA 4.3.2. 가상 DOMReference

1. 브라우저 동작 원리를 알아야 하는 이유

우리가 어떠한 제품을 판매한다 가정해 봅시다. 더 많은 고객을 끌어들이기 위해 무엇이 필요할까요? 광고, 제품의 높은 퀄리티, 수요 조사 등등 많은 요소가 고객 유치를 좌우합니다. 그렇다면 웹 개발자인 우리의 역할은 무엇일까요? 바로 제품의 퀄리티. 즉 웹브라우저의 성능을 최대로 이끌어내는 것입니다. 우리가 판매하려는 제품의 퀄리티가 떨어진다면 예민한 사용자들은 두 번 다시 우리의 제품을 구매하지 않을 것입니다. 실제로 BBC는 사이트를 로드하는 시간이 1초 추가될 때마다 사용자 수가 추가로 10% 줄어든다는 사실을 발견했습니다.[1] 그만큼 웹의 성능은 사용자를 유지하는데 매우 큰 영향을 미친다는 것을 알 수 있습니다.
 
훌륭한 개발자로 성장하기 위해서는 단순히 기능을 구현하는 것에서 그치지 않고, 사용자의 편의를 위해 성능을 개선하려는 치열한 노력도 필요할 것입니다. 그것이 우리가 브라우저의 동작원리를 알아야 하는 이유입니다. 앞으로 우리는 브라우저의 구조와 통신 과정을 이해하여 성능 문제가 발생한 위치를 알 수 있고, 렌더링 과정을 파악하여 최적화 방법을 찾을 수 있습니다. 쉬운 설명과 이해를 돕기 위한 다양한 예제를 준비했으니 브라우저 동작 원리 에 대해 알아봅시다!
 

2. 브라우저 동작 원리

2.1. 브라우저의 구조

2.1.1. 브라우저란?

우리는 하루에도 수십 번씩 인터넷에 접속하여 검색을 하고, 뉴스도 보며 여러 웹사이트를 둘러봅니다. 이러한 인터넷 접속을 위한 도구가 바로 브라우저입니다. 주로 사용하는 브라우저는 크롬(Chrome), 사파리(Safari), 엣지(Edge), 파이어폭스(Firefox), 오페라(Opera)가 있으며 이 책에서는 StatCounter 사이트[2]에 명시된 전 세계 브라우저 점유율을 기반으로 가장 높은 점유율을 차지하는 크롬에 포커싱하여 다루도록 하겠습니다.
그렇다면 브라우저는 어떤 일을 할까요? 브라우저의 핵심 기능은 우리가 보고자 하는 웹페이지를 서버에 요청하고 서버에서 받은 응답을 브라우저에 표시하여 웹 리소스를 제공하는 것입니다.
이때 브라우저는 HTML 파일을 해석하여 표시하는 과정에서 웹 표준화 기구인 W3C에서 정한 표준 명세를 따릅니다. 이렇듯 완성된 웹페이지를 보기까지 브라우저 내부에선 많은 일들이 일어납니다. 마치 무대 뒤에 있는 수많은 스태프가 각자의 역할을 하며 하나의 멋진 무대를 만드는 것처럼 말이죠!
그렇다면 브라우저는 어떤 구조로 구성되어 동작할까요? 이제부터 브라우저의 구성 요소를 살펴보겠습니다.
 

2.1.2. 브라우저의 구성 요소

브라우저는 기본적으로 사용자 인터페이스, 브라우저 엔진, 렌더링 엔진, 네트워킹, UI 백엔드, 자바스크립트 인터프리터, 자료 저장소로 구성되어 있습니다.[3]
브라우저 구성 요소브라우저 구성 요소
브라우저 구성 요소
 

2.1.2.1. 사용자 인터페이스 (User Interface)

 
notion imagenotion image
 
사용자 인터페이스는 주소 표시줄, 이전 및 다음 버튼, 북마크 옵션, 새로고침, 홈 버튼이 일반적인 요소입니다. 우리가 요청한 페이지가 표시되는 부분을 제외한 브라우저의 모든 부분이라고 생각하면 됩니다. 여기서 재미있는 점은 사용자 인터페이스에 대한 표준 명세는 없지만 오랜 기간 동안 브라우저들이 서로의 장점을 모방하여 발전해 왔고, 현재의 인터페이스로 최적화되었습니다. 그러나 필수적인 인터페이스를 정의하지 않았기 때문에 브라우저마다 지정된 스타일이 다르고, 고유한 확장 기능이 있습니다.
💡
우리가 자주 활용하는 웹 브라우저의 인터페이스는 어떻게 변화했을까?
USER INTERFACE MUSEUM 사이트[4]는 수년간 변화해온 다양한 브라우저의 인터페이스를 볼 수 있는 사이트입니다. 브라우저마다 많은 변화를 해왔고, 그 변화엔 다양한 이유가 존재하죠. 브라우저의 변천사가 궁금하다면 확인해 보세요!
notion imagenotion image
 
 

2.1.2.2. 브라우저 엔진 (Browser engine)

브라우저 엔진은 브라우저를 빠른 속도로 구동하는 중요한 기술 중 하나로, 브라우저 엔진이 없으면 웹 브라우저는 제대로 작동하지 않습니다. 그만큼 중요하여 모든 웹 브라우저의 핵심 소프트웨어 요소로 구성됩니다.
그렇다면 브라우저 엔진은 어떤 역할을 할까요? 브라우저 엔진의 주요 작업은 사용자 인터페이스와 렌더링 엔진 간의 동작을 처리하는 역할을 합니다. 만약 우리가 주소창에 주소를 입력하거나 홈 버튼을 누른다면 브라우저 엔진은 이에 대한 응답을 가져올 수 있도록 렌더링 엔진을 제어하는 것이죠.
 

2.1.2.3. 렌더링 엔진 (Rendering engine)

렌더링 엔진은 우리가 보고자 하는 웹 페이지를 화면에 그려내는 역할을 합니다. 요청된 콘텐츠가 HTML인 경우 렌더링 엔진은 HTML 및 CSS를 분석하고 최종 레이아웃이 생성되어 화면에 보여주는 것이죠.
그렇다면 현존하는 다양한 브라우저들은 모두 같은 렌더링 엔진을 사용할까요? 그렇지 않습니다. 각 브라우저마다 다양한 종류의 렌더링 엔진을 사용합니다. 크롬, 오페라(버전 15부터), 엣지는 웹킷(Webkit)에서 파생된 블링크(Blink)를 사용하고, 사파리는 웹킷(Webkit), 파이어폭스는 게코(Gecko)를 사용합니다. 그런데 어딘가 낯익지 않으신가요? 맞습니다! 바로, 웹에서 CSS를 적용할 때 크로스 브라우징을 위해 속성 앞에 붙이는 접두어입니다.
이렇듯 브라우저들은 그에 맞는 렌더링 엔진을 사용하고, 여러 단계를 거쳐 동작합니다. 그 모든 과정을 중요 렌더링 경로(Critical Rendering Path)라 하며 프론트엔드 개발자가 특히 중요하게 다뤄야 할 부분입니다. 렌더링 엔진의 동작 과정은 [3. 브라우저 렌더링 과정]에서 더 자세히 살펴보겠습니다.
💡
브라우저 엔진과 렌더링 엔진의 차이점
브라우저 엔진과 렌더링 엔진은 밀접한 관련이 있지만, 분명한 차이점이 있습니다. 브라우저 엔진은 모든 렌더링 과정을 관리하며 사용자 인터페이스 간의 통신을 처리하고, 렌더링 엔진은 DOM을 구성하여 콘텐츠를 화면에 표시하는 과정을 처리합니다.
 

2.1.2.4. 네트워킹 (Networking)

네트워킹은 브라우저가 요청한 웹 리소스를 전송하는 통신 과정을 처리합니다. 우리는 검색을 하거나 다른 웹사이트로 이동할 때 가고자 하는 홈페이지의 주소를 주소창에 입력합니다. 그러면 원하는 페이지로 이동하여 화면을 볼 수 있게 되죠. 바로 그 과정을 네트워킹이 처리합니다.
그렇다면 주소를 입력하는 순간 어떤 일들이 일어날까요? 우리가 주소를 입력하게 되면 브라우저는 먼저, 도메인 이름을 IP 주소로 변환해 주는 DNS(Domain Name System)을 통해 웹사이트 서버의 실제 주소를 찾습니다. IP 주소는 웹의 특정 위치를 나타내는 고유한 주소로, 우리가 사용하는 전화번호와 같은 의미이죠.
그 후에 브라우저는 해당 웹사이트를 사용자(클라이언트)에게 보내달라는 요청 메시지를 웹서버에 전송합니다. 이 메시지를 받은 서버는 요청 승인 메시지를 사용자에게 보내고 웹사이트의 데이터를 작은 단위로 잘라 하나로 뭉친 패킷이라는 블록으로 브라우저에 전송합니다. 브라우저는 그 블록을 받아 마치 레고처럼 웹사이트로 조립하고, 비로소 우리는 완성된 웹페이지를 볼 수 있게 됩니다.[5]
 
notion imagenotion image
 

2.1.2.5. UI 백엔드 (UI Backend)

UI 백엔드는 콤보 박스 및 창과 같은 브라우저의 기본 위젯을 그리는 역할을 하며 네트워킹과 같은 독립적인 요소입니다. 아래의 이미지는 콤보 박스를 macOS와 Windows에서 볼 때의 모습입니다. 차이점이 보이시나요? 각 브라우저는 여러 운영체제에서 만들어졌기 때문에 같은 웹 사이트라도 운영체제에 따라 기본 사용자 인터페이스가 다르다는 걸 알 수 있습니다.
macOS 콤보 박스macOS 콤보 박스
macOS 콤보 박스
Windows 콤보 박스Windows 콤보 박스
Windows 콤보 박스
 

2.1.2.6. 자바스크립트 인터프리터 (JavaScript Interpreter)

자바스크립트 인터프리터의 역할은 말 그대로 자바스크립트 통역사 역할을 하여 자바스크립트 코드를 분석하고 실행합니다. 모든 브라우저에는 자체적으로 자바스크립트 엔진이 탑재되어 있고, 그중 가장 잘 알려져 있는 엔진은 Google의 V8 엔진입니다. V8 엔진은 크롬 브라우저뿐만 아니라 자바스크립트 런타임인 Node.js도 지원하여 브라우저에서만 작동하는 자바스크립트를 서버에서도 작동할 수 있도록 해줍니다.
 

2.1.2.7. 자료 저장소 (Data Persistence)

브라우저 자체적으로 데이터를 저장하는 곳이 바로 자료 저장소입니다. 개발자 도구에서 애플리케이션을 통해 쿠키, 캐시, 기본 설정과 같은 사용자 데이터를 관리하고, 또한 로컬 스토리지, 세션 스토리지, IndexedDB, 웹 SQL, 파일시스템에 접근하여 데이터를 저장할 수 있습니다. 이때, 브라우저마다 저장할 수 있는 허용치가 정해져 있으며 크롬의 경우 최대 80%까지 사용할 수 있습니다.[6]
notion imagenotion image
브라우저는 지금까지 살펴본 구성 요소들로 이루어져, 최종적으로 우리가 보고자 하는 웹페이지를 제공합니다. 이렇듯 각각의 요소들이 얼마나 중요하고 복잡한지 알게 되셨을 겁니다. 이어지는 다음 장에서는 브라우저를 통해 웹페이지를 보기 위한 서버의 통신 과정을 다뤄보겠습니다.
 

2.2. 브라우저와 서버의 통신 과정

2.2.1. 브라우저와 서버의 통신 과정 - HTTP 요청과 응답

우리가 브라우저를 통해 웹 페이지를 보기 위해서는 서버의 도움이 필요합니다. 이때 사용자(클라이언트)는 서버에게 HTTP 요청을 보내고, 서버는 우리 컴퓨터에게 HTTP 응답을 돌려보냅니다. HTTP는 무엇일까요? HTTP란 Hyper Text Transfer Protocol의 약자로 링크(Hyper Text) 문서를 서로 주고받기 위한 통신 규약(Transfer Protocol)이란 뜻입니다. 웹상에서 우리가 문서를 정해진 형식으로 주고받기 위해 만든 규칙이죠.
notion imagenotion image
 
하지만 여기서 여러 번 동일한 웹 페이지를 이용해야 할 때, 매번 요청과 응답을 통해 문서를 주고받는 건 비효율적입니다. 웹 사이트와 애플리케이션의 성능이 떨어진다는 의미죠. 그래서 우리는 HTTP 캐싱(caching)을 통해 이전에 가져온 자료들을 재사용 함으로써 효율적으로 웹페이지를 렌더링 할 수 있습니다.
캐싱은 복사본으로 리소스를 가지고 있다가 우리가 요청할 때, 그것을 제공하는 기술을 말합니다. 이를 통해 서버는 모든 클라이언트에게 서비스를 제공할 필요가 없어지므로 서버의 부하를 줄이고, 웹 캐시는 서버에 비해 클라이언트와 가까이 있으므로 성능을 향상시켜줍니다.
하지만 캐시는 서버가 아닌 클라이언트의 컴퓨터에 저장이 되므로 용량을 차지한다는 단점이 있습니다. 대표적인 예시로 카카오톡은 우리의 대화 내용과 친구들의 프로필 사진을 우리의 기기(스마트폰, 컴퓨터)에 캐시로 저장을 합니다. 서버에서는 새로운 대화만을 불러오므로 용량이 너무 차지하게 된다면 대화방을 나가 용량을 확보할 수 있습니다.[7]
notion imagenotion image
 
또 다른 정보를 불러오기 위해 먼저 사용자는 연결할 서버를 파악해야합니다. 주소창에 도메인을 입력하여 웹 페이지를 호스팅하는 서버의 IP주소를 조회합니다. 인터넷에 연결된 브라우저의 패킷은 주로 TCP/IP(Transmission Control Protocol/Internet Protocol)라고 하는 전송 제어 프로토콜을 사용합니다. 이를 이용해 인터넷 서비스 제공회사 교환기, 혹은 라우터 장비를 통해 이동되어 통신 회사간 경로(라우팅)를 통해 IP주소가 있는 서버를 찾습니다. 하지만 좀더 효율적인 도달방법을 위해 직접 서버에 연결하기보다는 콘텐츠 전송 네트워크(CDN)을 이용해 콘텐츠를 사용자에게 가까이 위치시킵니다.
사용자와 서버가 연결되면 HTTP요청을 시작합니다. 요청라인에는 GET, POST, PUT, PATCH, DELETE등 메소드가 포함되어 있습니다. 대표적으로 GET메소드와 POST메소드를 많이 사용하는데요. GET 메소드는 URL로 데이터를 전달할 때 사용합니다
💡
GET메소드는 file같은 큰 파일은 get으로 전송하지 않습니다. id와 password같은 민감데이터에는 사용하지 않습니다.
서버에게 원하는 정보를 요청하는 방식으로 해당 정보에 대한 내용이 URL에 담겨 쿼리스트링 방식으로 서버에 전송이 됩니다. 이 경우에 보안에 취약하기 때문에 단순히 글과 이미지 등 정보를 서버로 부터 제공받는데 사용됩니다.
POST 메소드는 패킷안에 데이터를 넣어 전달할 때 사용합니다.
💡
POST메소드는 민감데이터나, 큰데이터에도 사용가능합니다.
이 메소드는 데이터를 생성하고 받아오는 역할을 합니다. Body로 데이터를 보낼 수 있기 때문에 보안상 중요한 데이터를 전송할 땐 POST 방식을 사용하는 것이 더 바람직합니다.
notion imagenotion image
위의 이미지에서 200이란 상태코드는 정보를 성공적으로 불러왔음을 의미합니다
응답을 받은 사용자는 응답 헤더를 검사하여 리소스를 렌더링하는 방법을 확인합니다.
웹 서버는 요청을 한 클라이언트에게 다음을 응답합니다.
  • 상태 라인 - 클라이언트에게 요청 상태를 알려줍니다.
  • 응답 헤더 - 브라우저에 응답 처리 방법을 알려줍니다.
  • 리소스 - 해당 경로에서 요청된 HTML, CSS, 자바스크립트, 이미지 파일 등등)[8]
헤더는 브라우저에 응답 본문에서 HTML 리소스를 수신했음을 알립니다.서버로부터 응답을 받은 웹 브라우저는 헤더를 검사하여 렌더링 방법에 대한 정보를 확인합니다.
웹 브라우저와 서버의 통신과정을 이해하면 문제가 발생한 위치를 파악하고 웹 사이트의 성능문제를 해결하며 사용자에게 보다 나은 경험을 제공하게됩니다.
 

2.2.2. 클라이언트 사이드 렌더링, 서버 사이드 렌더링

렌더링이란 어떤 웹페이지에 접속했을 때 그 페이지를 화면에 그려주는 것을 말합니다.
렌더링의 방식에는 크게 두 종류가 있는데요. 자바스크립트를 다운받아 브라우저에서 직접 렌더링 하는 방법을 CSR(Client Side Rendering)이라 하고, 반대로 렌더링된 리소스를 서버에서 받아와 화면에 보여주는 방법을 SSR(Server Side Rendering)이라고 합니다.[9]
 

2.2.2.1. CSR(Client Side Rendering)

자바스크립트를 이용하여 리소스를 렌더링 하는 방법을 CSR(Client Side Rendering)이라고 합니다.
<!DOCTYPE html> <html lang="en"> <head> <title>Document</title> </head> <body> <div class="root"></div> <script src="app.js"></script> </body> </html>
CSR의 간단한 예제입니다. body 태그 안에는 class=”root” 하나만 들어있습니다. HTML이 비어있기 때문에 처음 접속했을 때는 빈 화면만 보일 겁니다. 그리고 유저는 링크된 자바스크립트를 다운로드하게 됩니다. 추가적으로 데이터가 필요한 경우는 서버로부터 업데이트를 통해 동적으로 화면을 구성하게 해줍니다. CSR의 장점으로는 후속 페이지 로드 시간이 더 빠르고, 캐싱을 통해 스크립트가 저장되어 있다면, 인터넷의 연결 없이도 애플리케이션을 실행할 수 있으며, 요청할 때마다 모든 UI를 다시 로딩할 필요가 없다는 점입니다.
클라이언트 사이드 렌더링의 단점으로는 SEO(Search Engine Optimization)에 친화적이지 않다는 점이 있습니다. SEO란 검색엔진 최적화를 말하는데요. 이는 SSR의 장점에서 다루도록 하겠습니다. 그리고 초기 페이지 로딩 시간이 느리다는 점이 있습니다. 서버에서 렌더하지 않고, html을 다운받은 다음 자바스크립트나 각종 라이브러리를 다운받은 후에 사용자의 브라우저에서 렌더링하기 때문입니다.
 

2.2.2.2. SSR (Server Side Rendering)

서버에서 페이지를 모두 구성해 사용자에게 보여주는 방식을 SSR(Server Side Rendering)이라 합니다. 사용자의 브라우저 페이지를 구성하는 리소스는 모두 사본을 보는 것이며, 당연히 우리는 원본데이터에 접근할 수 없습니다. CSR과의 주된 차이점은 CSR은 자바스크립트 링크가 포함된 빈 문서를 제공하는 반면, SSR은 브라우저에 대한 서버 응답은 렌더링이 준비된 HTML이라는 것입니다. 이는 브라우저가 자바스크립트를 다운로드하고 렌더링 될 때까지 기다릴 필요가 없으며, 서버에서 렌더링을 시작한다는 걸 의미합니다.[10]
서버사이드 렌더링의 대표적인 목적은 “검색 엔진 최적화”, 그리고 “빠른 페이지 렌더링”입니다.검색엔진 최적화란 사용자의 검색을 통해 의도를 파악하여 페이지 콘텐츠를 제작하고 결과 페이지에 관련 콘텐츠를 주로 노출시킬 수 있도록 최적화하는 기법을 말합니다. 그리고 SNS를 통해 웹페이지 공유 버튼, 혹은 URL을 복사하여 콘텐츠를 공유했을 때, OG(Open Graph) 태그를 이용하여 미리 보기 화면을 설정하기에는 서버사이드 렌더링이 효율적입니다. 그리고 서버사이드 렌더링은 사용자가 화면을 처음으로 보게되는 시간을 앞당길 수 있습니다. 클라이언트사이드 렌더링처럼 자바스크립트를 로딩하고 화면에 보여주는것과 반대로 전부 그려진 화면을 받아오는건 속도의 차이가 큽니다.[11]
서버사이드 렌더링의 단점은 Blinking Issue가 있습니다. 새로고침을 하게 되면 리소스를 다시 받아와 렌더링 해야 하기 때문에 화면이 깜빡이는 현상이 나타납니다. 이는 UX 관점에서 좋지 않은 현상입니다.
네이버 페이지에서 새로고침을 반복해서 누르다 보면 사진처럼 렌더링이 덜 된 상황(Blinking Issue)이 나옵니다네이버 페이지에서 새로고침을 반복해서 누르다 보면 사진처럼 렌더링이 덜 된 상황(Blinking Issue)이 나옵니다
네이버 페이지에서 새로고침을 반복해서 누르다 보면 사진처럼 렌더링이 덜 된 상황(Blinking Issue)이 나옵니다
두 번째로는 서버의 과부하입니다. 동시간에 갑자기 사용자가 몰리게 되면 속도가 현저히 느려지게 됩니다. 리소스를 요청하는 횟수가(사용자의 수)가 많아질수록 과부하가 걸리기 쉽다는 뜻입니다. 흔히 우리가 유명가수의 공연을 보기위해 티켓 구매사이트를 접속하면 서버가 마비되는것과 같은현상입니다.
 

3. 브라우저 렌더링 과정

우리는 하루에도 여러 번 웹페이지에 방문합니다. 그리고 웹페이지에 방문하면 브라우저 화면에 페이지가 그려집니다. 이때 브라우저는 미리 준비해둔 페이지를 화면에 보여주는 걸까요? 그렇지 않습니다. 우리의 요청에 따라 브라우저의 렌더링 엔진이 실시간으로 그려내는 것에 가깝습니다. 이렇게 렌더링 엔진이 화면을 그려내는 과정을 브라우저 렌더링 과정이라고 합니다. 프론트엔드 개발자로서 브라우저 렌더링 과정을 공부하는 것은 매우 중요한 일입니다. 좋은 사용자 경험을 위해 웹페이지를 빠르게 작동시키려면 렌더링 과정에 대한 이해가 선행되어야 하기 때문입니다.
 
notion imagenotion image
 
브라우저 렌더링은 HTML을 파싱한 결과물인 DOM 트리와 CSS를 파싱한 결과물인 CSSOM 트리를 결합해서 렌더 트리를 만들고, 만들어진 렌더 트리를 바탕으로 화면에 요소를 배치하고 그려내는 과정으로 이루어집니다. 이번 챕터에서는 이러한 브라우저 렌더링 과정을 단계별로 자세히 알아보겠습니다.
 

3.1. 파싱(Parsing)

HTML 파싱을 들어가기 전에 개요에서 설명했던 파싱을 좀 더 자세히 살펴보겠습니다. 파싱이란 토큰화(Tokenize) 된 코드를 구조화시키는 과정으로, 토큰화된 코드를 파서(Parser) 역할을 하는 컴퓨터가 해석하는 순서로 진행됩니다.[12] 토큰화는 어휘 분석 단위인 토큰(Token)으로 코드를 쪼개는 것을 의미합니다.
notion imagenotion image
notion imagenotion image
왼쪽 예제를 파싱하면 오른쪽 예제처럼 문자열이 기계어로 번역되어 브라우저가 해석할 수 있는 의미 단위인 토큰이 됩니다. 파싱 과정은 서버로부터 입력받은 문자열이 정해진 어휘와 문법 규칙으로 구성된 문법을 전부 따르는지 확인하는 과정입니다. 어휘는 사용할 수 있는 단어와 조합들을 의미하고, 문법 규칙이란 어휘 사이에 적용되는 규칙들을 의미합니다. 예로 숫자 계산에서 더하기보다 곱하기가 먼저 계산되듯이 말입니다.
💡
렌더링 엔진은 HTML, CSS, 자바스크립트를 모두 파싱하나요? 렌더링 엔진은 HTML과 CSS만을 파싱합니다. 자바스크립트는 별도의 레이어에서 언어를 해석하기 때문인데요. 자바스크립트 파싱에 대해서는 [3.4. 자바스크립트 파싱과 실행]에서 자세히 알아보겠습니다.
 

3.2. HTML 파싱 - DOM(Document Object Model)

브라우저가 렌더링을 수행하려면 먼저 필요한 리소스를 서버에 요청하여 응답받습니다. 응답해온 HTML 파일은 문자열로 이루어진 텍스트이기에 브라우저는 이를 해석할 수 없습니다. 때문에 브라우저는 서버로부터 응답받은 HTML을 파싱 하여 브라우저가 이해할 수 있는 객체인 DOM을 생성합니다.[13] 면밀히 살펴보면 HTML 문서는 아래와 같은 과정을 거쳐 파싱되어 DOM을 생성합니다. DOM은 아래의 파싱 과정을 거친 자료구조입니다.
 
notion imagenotion image
 
  1. Bytes → Characters 먼저 브라우저가 서버에 리소스를 요청하면 HTML 파일을 읽어 16진수화된 데이터를 메모리를 저장하고 인터넷을 경유하여 응답합니다.
  1. Characters → Token HTML 문서를 2진수 형태로 응답받아, 선언된 인코딩 방식을 기준으로 문자열로 변환된 문서를 읽어들여 토큰들로 분해합니다.
  1. Tokens → Nodes 토큰들을 각각의 객체로 변환시켜 노드를 생성합니다. 이 노드는 DOM의 기본 요소가 됩니다.
  1. Nodes → DOM HTML 문서는 요소들의 집합으로 구성되어 있어 중첩 관계에 의해 부자 관계가 형성되는데 이를 반영하여 모든 노드들을 자료구조로 구성합니다.
 
DOM은 HTML 문서의 객체 기반 표현 방식으로, 노드 트리라는 개체 구조로 표현됩니다.[14] 트리라는 이름 뜻대로 하나의 부모 줄기에서 여러 개의 자식 나뭇가지를 가지고 있으며 나뭇가지는 잎들을 가지는 나무와 같은 구조로 이루어져 있습니다.
<!DOCTYPE HTML> <html> <head> <title>위니브 </title> </head> <body> <h1>라이캣</h1> <div>웨이드</div> </body> </html>
위의 예시는 아래 이미지와 같은 노드 트리로 표현됩니다.
 
notion imagenotion image
 

3.3. CSS 파싱 - CSSOM(CSS Object Model)

브라우저는 렌더링 엔진을 통하여 엔진을 통해 HTML 문서와 CSS 파일을 파싱합니다.[15] 여기서 CSS 파싱도 HTML 파싱과 같은 것 아닌가 하는 생각이 드실 수도 있지만 조금은 다릅니다. HTML의 파싱 과정에 비해서는 복잡하진 않습니다. 렌더링 엔진은 먼저 HTML을 처음부터 한 줄씩 순차적으로 파싱 하여 DOM을 생성해 가면서 해석을 실행할 수 있지만, CSS 파싱은 CSS를 로드하는 <link> 태그, style 태그를 만나면 DOM 생성을 일시 보류합니다. CSS 파서는 전체 파일을 다운로드할 때까지 파싱을 해나갈 수 없습니다.
CSS 파싱 과정이 끝나게 되면, 코드의 내용들과 순서를 바탕으로 DOM과 같은 트리를 구성하는데 이를 CSSOM 트리라고 부르게 됩니다. CSSOM은 스타일을 구성하는 트리 구조인 객체 모델입니다. 트리 안에는 스타일, 선택자 등의 정보가 노드안에 들어가게 됩니다.
 
CSSOM의 상속CSSOM의 상속
CSSOM의 상속
CCSS의 상속을 그대로 반영하여 생성되는 CSSOM은 위 예제와 같이 body 요소에 적용한 속성이 ul 요소에도 적용이 되고, ul에 적용한 속성은 li에 적용되는 상속 관계를 반영하여 구성됩니다.[16]
CSS 태그는 상단에 위치시켜야 합니다. CSSOM이 만들어져 브라우저가 렌더 트리를 만들고 렌더링을 진행하기 때문에 최대한 빠르게 구성할 수 있도록 해주어야 합니다. 브라우저가 CSS 파싱을 완료하게 되면 HTML 파싱이 중단된 지점에서 부터 다시 HTML 파싱을 실시하게 됩니다.
 

3.4. 자바스크립트 파싱과 실행

3.4.1. 자바스크립트 파싱과 CSS 파싱의 차이

HTML과 CSS의 파싱 과정을 알아보았습니다. 그렇다면 자바스크립트의 파싱 과정은 어떻게 이루어질까요? 자바스크립트의 파싱 과정은 CSS 파싱과 크게 다르지 않습니다. 둘의 차이를 통해서 자바스크립트 파싱이 어떻게 이루어지는지 알아보도록 합시다.
 
CSS 파싱 과정은 아래와 같습니다.
  1. HTML을 한 줄씩 읽어나가다가 <link> 태그, <style> 태그를 만나면 DOM 생성을 일시 보류한다.
  1. CSS 파싱은 렌더링 엔진 에서 이루어진다.
  1. CSS 파싱 과정을 거치며 파싱의 결과물로 CSSOM을 생성한다.
  1. CSS 파싱이 완료되면 HTML 파싱이 중단된 지점으로 다시 돌아가 DOM 생성을 다시 시작한다.
 
아래는 자바스크립트의 파싱 과정입니다.
  1. HTML을 한 줄씩 읽어나가다가 <script> 태그나 자바스크립트 파일을 만나면 DOM 생성을 일시 보류한다.
  1. 자바스크립트 파싱은 자바스크립트 엔진에서 이루어진다.
  1. 자바스크립트 파싱 과정을 거치며 파싱의 결과물로 AST를 생성한다.
  1. 자바스크립트 파싱이 완료되면 HTML 파싱이 중단된 지점으로 다시 돌아가 DOM 생성을 다시 시작한다.
 
보시다시피 자바스크립트 파싱 과정과 CSS 파싱 과정은 매우 닮았습니다. 그러나 2가지 차이점을 가집니다. 자바스크립트와 CSS 파싱 과정에서의 차이점은 파싱이 이루어지는 엔진과, 파싱 과정을 통해 생성된 결과물이 다르다는 것입니다. CSS 파싱은 렌더링 엔진에서 이루어지지만 자바스크립트 파싱은 자바스크립트 엔진에서 이루어집니다. 또한 파싱 과정의 결과물로서 CSS는 CSSOM을 생성하지만, 자바스크립트는 AST를 생성합니다. AST에 대한 설명은 [3.4.2.2. 파싱 결과로 생성된 AST, 도대체 무엇일까?]에서 자세히 다루겠습니다.
 
CSS와 자바스크립트 파싱이 이루어지는 엔진과 결과물의 차이CSS와 자바스크립트 파싱이 이루어지는 엔진과 결과물의 차이
CSS와 자바스크립트 파싱이 이루어지는 엔진과 결과물의 차이
 

3.4.2. 자바스크립트 파싱 과정

3.4.2.1. 자바스크립트 파싱 과정

자바스크립트 파싱 과정을 통해 AST가 생성된다는 것을 알게 되었습니다. 자바스크립트 코드로부터 파싱이 어떻게 이루어지는지와 AST가 생성되는 과정을 좀 더 상세히 살펴보겠습니다.
자바스크립트 파싱 과정자바스크립트 파싱 과정
자바스크립트 파싱 과정
 
우리가 작성한 코드가 어떻게 AST로 변환될 수 있는 걸까요? 답은 자바스크립트 엔진입니다. 자바스크립트 엔진에는 토크나이저와 파서라는 부품들이 우리가 보이지 않는 곳에서 일을 수행합니다. 이 때문에 우리가 작성한 코드로부터 AST로 변환이 가능한 것입니다.
 
AST 생성 과정 즉, 자바스크립트 코드의 파싱 과정은 위의 그림과 같이 동작합니다. 자바스크립트 엔진은 우리가 작성한 소스 코드로부터 토크나이저를 통해 먼저 어휘 분석이라고 불리는 렉시컬 분석(Lexical Analysis)을 수행한 후, 파서를 통해 구문 분석이라고 불리는 신택스 분석(Syntax Analysis)을 수행합니다. 첫 번째 단계인 렉시컬 분석은 소스 코드로부터 의미 있는 요소들을 토큰으로 나누는 작업을 말하며, 두 번째 단계인 신택스 분석은 렉시컬 분석을 통해 만들어진 토큰 목록을 가져와 소스 코드의 구문(문법)을 검증해주는 작업을 말합니다. 이렇게 렉시컬 분석 단계와 신택스 분석 단계를 모두 거쳐 최종적으로 AST가 생성됩니다.
💡
인터프리터(Interpreter)와 JIT 컴파일러(Just In Time Compiler)
인터프리터와 컴파일러는 모두 프로그래밍 언어를 기계어로 변환해주어 기계가 이해 가능한 언어로 변환해주는 프로그램이라는 점은 같지만 이 둘은 차이점이 있습니다.
먼저, 인터프리터는 코드를 한 줄씩 읽어나가며 기계어로 변환합니다. 첫 번째 줄, 두 번째 줄을 차례대로 읽어나가기 때문에 에러가 발생한 곳을 바로 알 수 있습니다.[17] 따라서, 실시간으로 코드 수정이 가능하다는 장점을 가지지만 한 줄씩 읽어나가는 특징으로 인해 실행 속도가 느리다는 단점을 가집니다. 초기 자바스크립트 엔진에는 인터프리터만 존재했지만, 느린 실행 속도라는 치명적인 단점으로 인해 새로운 엔진인 JIT 컴파일러 등장했습니다.
JIT 컴파일러는 인터프리터와 반대로 한 줄씩 코드를 읽어나가는 것이 아닌 한꺼번에 전체 코드를 읽어 기계어로 변환해줍니다. 따라서 인터프리터와 반대되는 속성을 가집니다. 에러가 발생한 곳을 바로 발견할 수는 없지만 실행 속도가 빠르다는 장점을 가집니다.[18]
 
현재의 자바스크립트 엔진은 인터프리터와 JIT 컴파일러의 장점 모두 활용해서 사용하고 있습니다. 설명의 편의를 위해 이 뒤에서부터는 둘의 차이는 고려하지 않고 ‘인터프리터’로 통일하겠습니다.
 

3.4.2.2. 파싱 결과로 생성된 AST, 도대체 무엇일까?

AST란 무엇일까요? 위키백과에서는 AST를 다음과 같이 설명하고 있습니다.
컴퓨터 과학에서 추상 구문 트리(abstract syntax tree, AST), 또는 간단히 구문 트리(syntax tree)는 프로그래밍 언어로 작성된 소스 코드의 추상 구문 구조의 트리이다. 이 트리의 각 노드는 소스 코드에서 발생되는 구조를 나타낸다. 구문이 추상적이라는 의미는 실제 구문에서 나타나는 모든 세세한 정보를 나타내지는 않는다는 것을 의미한다.[19] -위키백과-
 
위 설명에서 알 수 있듯이, AST란 소스 코드로부터 생성된 트리 구조를 말합니다. 쉽게 말해, 특정 프로그래밍 언어로 작성된 소스 코드를 의미와 구문 분석을 통하여 기계가 이해 가능한 구조로 변경시킨 트리를 의미합니다. AST는 어디에 사용되는 걸까요? AST는 인터프리터가 프로그램을 읽으며 문법상 틀린 부분이 있는지 구문 분석(Semantic Analysis)을 수행하는 단계에서 사용됩니다. 따라서 AST의 모든 단계를 무사히 거쳤다면 작성한 프로그램에는 이상이 없고, 정확하다는 것을 의미합니다.
 

3.4.2.3. AST 생성해보기

AST가 어떤 형식으로 되어있는지 궁금하지 않으신가요?
AST explorer 사이트[20]는 자바스크립트 코드를 입력하면 AST로 변환해주어 AST의 구조를 확인할 수 있게 해주는 사이트입니다. 아래 그림은 해당 사이트에 진입한 화면입니다. 보시다시피 페이지 왼쪽에는 소스 코드를 입력하는 창이 있고, 페이지 오른쪽에는 소스 코드로부터 트리 형태로 변환된 AST가 출력되는 창이 있습니다. 이 사이트를 통해 자바스크립트 코드로부터 변환된 AST 결과를 확인해보겠습니다.
AST explorer 사이트의 초기 화면AST explorer 사이트의 초기 화면
AST explorer 사이트의 초기 화면
 
왼쪽 페이지에 아래 코드를 작성하고 변환된 결과를 확인해 봅시다.
let x = '잡았다 요 돔!'
AST explorer 사이트에 작성한 자바스크립트 코드를 기반으로 생성된 AST 일부AST explorer 사이트에 작성한 자바스크립트 코드를 기반으로 생성된 AST 일부
AST explorer 사이트에 작성한 자바스크립트 코드를 기반으로 생성된 AST 일부
위 그림과 같이 소스 코드가 텍스트 형식에서 트리 구조 형식으로 바뀐 것을 확인할 수 있습니다. Abstract는 단어 그대로 추상적이라는 뜻입니다. 따라서 위에서 생성된 AST는 자세한 의미를 나타내는 것이 아닌, 대략적으로 트리 뭉치를 의미합니다.
 

3.4.3. 자바스크립트 실행 방법

자바스크립트 엔진은 어떻게 자바스크립트 코드를 실행할까요? 저희는 자바스크립트 파싱 과정을 통해 AST가 생성된다는 것을 알게 되었습니다. 생성된 AST는 인터프리터 (Interpreter)를 통해 바이트 코드(Byte Code)로 변환됩니다. 바이트 코드를 인터프리터에서 실행함으로써 비로소 자바스크립트는 동작이 실행되는 것입니다. 아래 그림과 같이 파싱과 실행의 일련의 과정으로 자바스크립트 소스 코드는 실행될 수 있습니다.
자바스크립트 파싱과 실행 과정자바스크립트 파싱과 실행 과정
자바스크립트 파싱과 실행 과정
 

3.5. 렌더 트리

앞서 브라우저는 우리가 보이지 않는 곳에서 많은 일을 하고 있다는 것을 알 수 있었습니다. 브라우저는 우리가 모르게 내용을 설명하는 HTML 언어로부터 DOM을 생성하고, 스타일 규칙을 생성하는 CSS로부터 CSSOM을 생성했습니다. 하지만 DOM과 CSSOM이 생성된다고 해서 바로 브라우저에 픽셀이 렌더링 되는 것이 아닙니다. 브라우저가 우리가 보이지 않는 곳에서 하는 일이 한 가지 더 있습니다. 바로 렌더 트리(Render Tree) 구축입니다. 이번 장에서는 렌더 트리가 무엇인지, 렌더 트리는 어떻게 형성되는지 알아보겠습니다.
 

3.5.1. 렌더 트리란?

렌더 트리는 무엇일까요? 렌더 트리는 문서의 내용을 구조화한 DOM과 스타일이 정의된 CSSOM이 렌더링 엔진에 의해 결합되어 형성된 결과물을 말합니다. 이렇게 결합된 렌더 트리는 노드의 위치와 크기 정보를 담고 있습니다. 렌더 트리를 생성하는 목적은 무엇일까요? 렌더 트리 형성의 목적은 요소의 위치와 크기 정보를 정확하게 계산하여 문서의 내용을 정확한 순서로 페인팅(Painting)하기 위해서 입니다. 이런 페인팅 과정을 통해 최종적으로 렌더링된 픽셀을 시각적으로 볼 수 있게 됩니다. 이 부분은 [3.6. 레이아웃과 페인트]에서 자세히 다루겠습니다.
notion imagenotion image
DOM과 CSSOM이 결합된 렌더 트리DOM과 CSSOM이 결합된 렌더 트리
DOM과 CSSOM이 결합된 렌더 트리
 

3.5.2 렌더 트리 형성 과정

앞서 DOM과 CSSOM의 노드들이 결합되어 렌더 트리가 형성된다는 것을 알았습니다. 어떤 과정으로 결합이 이루어질까요? 모든 노드들이 한꺼번에 결합이 되는 걸까요? 아닙니다. DOM과 CSSOM 노드가 생성되는 방식에 각각의 규칙이 있듯이 렌더 트리도 규칙에 따라 노드들이 결합됩니다.
 
렌더 트리 형성 과정에서 모든 노드가 결합되는 것이 아니며, 생성된 렌더 트리에는 화면에 그릴 노드만(눈에 보이는 내용만) 포함되어 있습니다. 렌더 트리가 형성되는 과정을 통해 어떤 노드가 선별되어 최종적으로 브라우저에 출력되는지 렌더 트리 구축 과정을 자세히 살펴봅시다. 렌더 트리에는 화면에 그릴 노드만(눈에 보이는 내용만) 포함되어 있다라는 것을 염두에 두며 읽어주시길 바랍니다.
 
notion imagenotion image
위의 그림은 DOM 트리와 CSSOM을 보여주고 있습니다. 먼저, 렌더 트리를 생성하기 위해 DOM 트리의 루트 부분부터 시작합니다. 여기서 DOM 트리의 루트는 p 노드이고, 이에 상응하는 CSS 규칙을 확인합니다.
 
CSSOM 보면 p 노드에 대해 모든 텍스트를 “font-size : 16px”, “font-weight : bold” 로 적용하여 렌더링하는 매칭 규칙이 보입니다. 따라서 p 노드는 매칭된 CSS속성과 함께 렌더 트리로 복사됩니다. 그 후 루트 노드인 p 노드를 마치면 트리 아래쪽에 위치한 텍스트 노드인 ‘잡았다’로 향합니다. 이것을 복사해서 렌더 트리에 붙입니다.
 
 
notion imagenotion image
 
다음은 span 노드에 도착한 후, 이에 맞는 CSS 규칙을 찾습니다. CSSOM을 보면 span 노드를 가지고 있고, DOM과 동일하게 p 노드의 자식입니다. 그러나 span의 규칙 중 display: none이 보입니다. 이 속성은 span이 렌더링 되면 안 된다는 것을 말해줍니다. 앞서 ‘렌더 트리에는 화면에 그릴 노드만(눈에 보이는 내용만) 포함되어 있다’라는 것을 염두에 둬달라고 말씀드렸습니다. 따라서 display: none이 적용된 span 노드를 건너뛰고, span의 자식 노드인 텍스트 ‘멋쟁이’도 건너뜁니다. display:none이라는 속성은 상속되기 때문에 span 노드의 자식을 건너뛴 것입니다.
 
notion imagenotion image
 
마지막으로 다음 노드인 텍스트 ‘요 돔’을 렌더 트리로 한 번 더 복사합니다. 이렇게 내용과 스타일 모두를 가지는 렌더 트리가 형성됩니다.
💡
visibility: hidden과 display: none visibility: hiddendisplay: none 은 화면에 요소를 보이지 않게 만들어주는 속성입니다. 그러나 이 두 가지 속성은 완전히 다르게 동작합니다. visibility: hidden은 렌더 트리에서 요소를 제거하지 않으며, 해당 속성이 적용된 요소는 여전히 레이아웃에서 공간을 차지합니다. 반면에 display: none 속성이 적용된 요소는 렌더 트리에서 완전히 제거됩니다. visibility: hidden 은 빈 상자로 렌더링되어 레이아웃을 차지하지만 잠시 화면에서 숨기는 것이고, display: none은 레이아웃에서 완전히 제거되는 것이라고 생각하면 됩니다.[21]
 

3.6. 레이아웃과 페인트

3.6.1. 레이아웃

렌더 트리를 구성한 후에는 레이아웃(Layout) 단계가 진행됩니다. 렌더 트리는 각 요소의 위치나 크기에 관련된 정보들을 담고 있습니다. 그렇다면 이제 렌더링을 위한 준비가 끝난 것일까요? 아쉽게도 이 정보만으로는 요소들이 화면에서 정확하게 어디에, 어떤 크기로 위치할 것인지는 알 수가 없습니다. 예를 들어 화면 왼 편에는 커다란 박스 1개가 위치해있고 오른 편에는 작은 박스 2개가 겹쳐져 위치해있다는 정보만 주어진다면, 화면에서 요소들이 정확히 어떻게 배치될지 그리기 어려울 것입니다.
 
notion imagenotion image
레이아웃 단계에서는 너비와 높이 등의 요소와 관련된 속성과, 요소가 배치되는 방법, 위치를 결정합니다. 브라우저는 각 요소들이 화면 전체에서 어디에, 어떤 크기로 배치될지 결정하기 위해 렌더 트리의 루트부터 노드를 순회하며 계산을 진행합니다. 이후 계산된 모든 값들은 절대적인 단위인 픽셀(px) 값으로 변환하여 렌더 트리에 반영합니다. 다음 그림과 같이 요소의 넓이(width)를 퍼센트(%) 단위로 지정하였다면, 레이아웃 단계 이후 이 값은 전체 화면 크기(Viewport)를 기준으로 픽셀 값으로 변환하게됩니다.
 
notion imagenotion image
 

3.6.2. 페인트

페인트(Paint) 단계는 레이아웃에서 계산된 정보를 기반으로 화면에 픽셀을 칠합니다. 그동안 텍스트로 존재했던 요소들이 이 과정을 통해 이미지화됩니다. 페인트는 일반적으로 여러 개의 레이어(Layer) 위에서 수행되는데, 레이어는 레이아웃을 기반으로 렌더링 시 페인트 할 대상 영역을 나누어 놓은 것입니다. 요소들의 배치 속성 상태에 따라 나누어 생성되며 브라우저마다 기준이 다르지만 보통은 video, canvas, position, 3d, z-index, filter 관련 속성 등을 기준으로 나누어지게 됩니다.
레이어는 나중에 변경 사항이 발생할 경우 브라우저 전체가 아닌 변화할 요소의 레이어만 다시 칠하는 방법으로 해결이 가능하기 때문에 성능 개선에 도움이 됩니다. 만약 다시 레이아웃이 발생하거나 다시 페인트가 발생하게 된다면 이는 성능 저하의 원인이 되며, 최적화에 대한 자세한 내용은 다음 장에서 다루고 있습니다.
 
페인트 단계에서는 화면에 픽셀을 칠하는 것뿐 아니라 레이아웃 과정의 결과를 바탕으로 페인트 레코드(Paint Records)를 생성하게 됩니다. 페인트 레코드는 어떤 요소가 더 앞에 배치되는지, 겹쳐지는 요소 간의 렌더링 순서가 정해진 명령의 모음입니다. CSS 속성에 따른 명령 순서가 정해져 있으며 이를 바탕으로 순서가 결정됩니다. 명령 순서는 쌓임 맥락(Stacking Context)에 대한 공식 문서[22]에서 확인 가능합니다.
 
레이아웃을 기반으로 레이어가 나누어진 화면
출처: 제주코딩베이스캠프레이아웃을 기반으로 레이어가 나누어진 화면
출처: 제주코딩베이스캠프
레이아웃을 기반으로 레이어가 나누어진 화면 출처: 제주코딩베이스캠프
레이어 위에 픽셀을 칠한 화면레이어 위에 픽셀을 칠한 화면
레이어 위에 픽셀을 칠한 화면
 

3.6.3. 개발자 도구에서 레이어 확인하기

브라우저가 실제로 어떻게 레이어를 구성하는지 간단한 실습을 통해 확인해 보겠습니다. 개발자 도구의 레이어(Layers) 탭에서는 현재 화면에 대한 레이어 정보를 확인할 수 있습니다. 크롬 브라우저를 열고 제주코딩베이스캠프 사이트[23]에 접속합니다.
 
  1. 개발자 도구를 열고 레이어 탭으로 이동해 봅시다. (이미지를 참고해주세요.)
      • 우측 상단 케밥 메뉴(⋮) → 도구 더보기 → 레이어
      notion imagenotion image
 
  1. 레이어 탭에서는 리스트와 이미지로 레이어 확인이 가능합니다.
    1. notion imagenotion image
 
  1. 상단 메뉴바의 회전 기능을 사용하여 레이어를 더 직관적으로 볼 수 있습니다. 마우스를 드래그하여 3차원으로 레이어를 관찰해 보세요!
    1. notion imagenotion image
 

3.7. 컴포지션

컴포지션(Composition) 단계에서는 페인트 단계에서 준비한 레이어들을 차곡차곡 순서대로 브라우저 위에 픽셀을 표기하게 되며 나누었던 레이어들을 합성하여 화면에 보여줍니다.
이러한 렌더링 과정을 통해, 우리는 드디어 화면에서 웹 페이지를 볼 수 있게 되었습니다.
 

4. 브라우저 렌더링 과정 최적화 방법

4.1. <script> 태그의 async, defer 속성

4.1.1. 스크립트 실행 과정

notion imagenotion image
브라우저의 렌더링 과정에서, HTML 파싱 중 <script> 태그를 만나면 일시적으로 파싱을 중지하고 스크립트를 다운로드, 실행합니다. 이는 때때로 스크립트를 모두 불러오고 실행할 때까지 페이지의 콘텐츠 표시를 지연시켜 사용자 경험에 불편을 초래할 수도 있습니다. 혹은 어떤 기능이 아직 생성되지 않은 DOM 요소에 접근을 필요로 할 경우 스크립트가 제대로 실행되지 않을 수도 있습니다.
이를 방지하기 위해서 대개 두 가지 방법이 추천됩니다. 그중 첫 번째는, <script> 태그를 <body> 태그 최하단에 위치시키는 것입니다. 스크립트를 제외한 HTML 요소들을 전부 파싱하고 DOM 트리를 구성한 뒤에, 비로소 <script> 태그를 실행하도록 그 위치를 조절하는 것입니다.
두 번째는 스크립트 내에서 DOMContentLoaded 이벤트를 활용하는 것입니다. 이 이벤트는 이름을 보면 유추할 수 있듯이, DOM 콘텐츠가 전부 로드 되었을 때 즉, HTML 파싱이 모두 끝났을 때 발생하는 이벤트입니다.
window.addEventListener("DOMContentLoaded", () => { console.log("DOM 구성이 완료됐습니다. 잡았다, 요 돔!"); });
따라서 위의 예시와 같이, 해당 이벤트가 발생했을 때 제작자가 구현하고자 하는 기능이 실행되도록 하면 HTML의 파싱이 다 끝날 때까지 스크립트 실행을 지연시킬 수 있습니다.
하지만 <script> 태그가 HTML 요소들 중간에 있어야 하는 경우가 있을 수도 있고, 콘텐츠가 다 표시되었으나 스크립트 파일을 불러오는 시간이 길어 그동안 기능들을 이용할 수 없게 되기도 합니다. 이러한 문제들을 근원적으로 해결하기 위해 HTML5에서는 <script> 태그의 속성(Attribute)으로 Async와 Defer가 추가됐습니다.[24]
 

4.1.2. Async와 Defer

기본적으로 이 두 속성은 <script> 태그의 src 속성을 통해 외부 스크립트 파일을 읽어오는 경우에만 사용이 가능하며, 인라인 스크립트에는 영향을 미치지 않습니다. 또한 이 둘은 Boolean 속성으로 명시할 경우 참(True)값을 가지게 되고, 명시하지 않으면 거짓(False)값을 가지게 됩니다. 이는 <script async src=””>와 같이 속성의 값을 추가할 필요 없이 속성 이름을 기재하는 것만으로 사용이 가능하다는 얘기입니다(1).
async와 defer는 모두 비동기적으로 외부 스크립트 파일을 불러옵니다. 즉, HTML의 파싱이 이루어지는 동시에 스크립트 파일을 불러올 수 있습니다. 그러나 스크립트 파일을 실행시키는 시점에서 두 속성은 차이가 있습니다.
notion imagenotion image
async 속성은 HTML 파싱과 동시에 스크립트 파일을 불러오다가, 스크립트 파일의 다운로드가 끝나면 HTML 파싱을 잠시 멈추고 스크립트 파일을 실행합니다. 스크립트 파일이 다운로드 되는 대로 바로 실행되는 특징 때문에 async 속성을 사용한 스크립트 파일들은 순서를 예측할 수가 없습니다. 따라서 순서를 지켜야 하는 스크립트들이라면 아래에서 설명할 defer 속성을 사용하는 편이 순서를 예측할 수 있어서 권장됩니다.
notion imagenotion image
defer 속성은 HTML 파싱과 동시에 스크립트 파일을 불러오며, 스크립트 파일의 다운로드가 끝나더라도 HTML 파싱을 멈추지 않고, HTML 파싱이 모두 끝나고 DOM 트리가 구성된 후에 스크립트 파일을 실행합니다(2). 실행 순서는 <script> 태그의 순서를 따르므로, 제작자가 스크립트 파일의 실행 순서를 결정할 수 있습니다.[25]

(1) XHTML에서는 defer=”defer”와 같이 속성 이름과 속성 값을 모두 명시해야 합니다.
(2) 하지만 이는 DOMContentLoaded 이벤트 발생 전에 일어납니다.
 

4.2. 리플로우, 리페인트 최적화

4.2.1. 리플로우, 리페인트란?

리플로우, 리페인트가 일어나는 단계리플로우, 리페인트가 일어나는 단계
리플로우, 리페인트가 일어나는 단계
렌더링 과정이 한 번 완료되면 더 이상의 렌더링 과정은 일어나지 않을까요? 그렇지 않습니다. DOM 트리나 CSSOM 트리에 변경이 일어나면 렌더 트리가 재생성됩니다. 그리고 새로운 렌더 트리를 토대로 레이아웃, 페인트 단계를 다시 거치게 됩니다.
 

4.2.1.1. 리플로우

레이아웃 단계를 다시 거치는 과정을 리플로우(Reflow)라고 합니다. 리플로우는 요소의 레이아웃(크기나 위치)에 변화가 일어났을 때 발생하는데, 이 단계에서는 변경된 렌더 트리를 바탕으로 레이아웃을 다시 계산합니다. 렌더링 과정은 차례대로 일어나기 때문에 리플로우가 일어나면 리페인트가 무조건 일어납니다.
요소들의 실제 크기와 위치를 다시 계산하는 과정은 매우 무거운 작업이므로 리플로우가 적게 일어날수록 렌더링 성능 최적화와 좋은 사용자 경험에 도움이 됩니다. 리플로우에 걸리는 시간이 짧아질수록 로딩 속도가 빨라지고, 빠른 로딩 속도는 사용자 경험에 긍정적인 영향을 주기 때문입니다.
💡
레이아웃 변경이 일어나지 않아도 리플로우가 발생하는 경우가 있어요! 요소의 레이아웃이 변경되었을 때 뿐만 아니라 현재 레이아웃 수치를 가져오는 clientWidth, clientHeight 등의 속성을 사용했을 때도 리플로우가 발생합니다.
 

4.2.1.2. 리페인트

페인트 단계를 다시 거치는 과정을 리페인트(Repaint)라고 합니다. 리페인트가 발생하면 렌더링 엔진은 화면에 픽셀을 다시 그립니다. background, color, outline처럼 시각적인 스타일에만 영향을 미치는 속성의 값이 변경되면 리플로우가 발생하지 않고 리페인트만 일어납니다.
리페인트는 요소의 크기, 위치가 정해진 상태에서 화면만 다시 그리면 되므로 리플로우보단 가벼운 작업입니다. 그러나 리플로우보다 가벼운 작업이라고 해서 리페인트가 렌더링 속도에 영향을 미치지 않는 것은 아닙니다. 오페라에 의하면 리페인트가 일어나는 순간 렌더링 엔진은 모든 요소를 확인해서 화면에 보이는 요소인지, 보이지 않는 요소인지 결정하는 작업을 진행하게 되는데 이 과정에 비용이 많이 든다고 합니다.[26]
 
리플로우와 리페인트의 특징을 정리하면 아래와 같습니다.
리플로우
리페인트
수행 단계
레이아웃 단계를 다시 거침
페인트 단계를 다시 거침
발생 상황
요소의 레이아웃에 변화가 생겼을 때 또는 요소의 레이아웃 수치를 가져올 때
리플로우가 일어났을 때 또는 시각적인 스타일에 변화가 생겼을 때
발생 상황 예시
- DOM 요소 추가, 제거 시 - 브라우저 창 크기 변경 시 - width, height, top, left, padding 등 레이아웃에 영향을 미치는 속성 사용 시 - clientWidth, cliengHeight, scrollTop 등 요소의 레이아웃 수치를 가져오는 속성 사용 시
- background, color, outline, visibility, box-shadow, text-decoration 등 시각적인 스타일에 영향을 미치는 속성 사용 시
💡
CSS Triggers 어떤 CSS 속성 값이 변경됐을 때 리플로우, 리페인트, 컴포지션 중 어떤 단계가 수행되는지 알려주는 CSS Triggers 사이트[27]를 참고해보세요! 브라우저 렌더링 엔진 별로 확인이 가능합니다.
 

4.2.2. 리플로우, 리페인트 발생 확인하기

4.2.2.1. 리플로우 발생 확인하기

크롬 개발자 도구의 성능 탭을 이용해서 확인할 수 있습니다. 아래 예제를 통해 실습 해 보겠습니다. 예제 코드를 실행하면 클릭할 때마다 리스트 아이템을 추가하는 버튼이 브라우저에 나타납니다. DOM 요소 추가는 리플로우를 발생하게 하므로 버튼을 클릭할 때마다 리플로우가 일어날 것입니다.
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>리플로우 발생 확인</title> </head> <body> <button type="button" id="add-button">Click Me</button> <ul id="list"></ul> <script> const addButton = document.querySelector("#add-button"); addButton.addEventListener("click", function() { const liEl = document.createElement("li"); liEl.innerText = "추가된 리스트"; document.querySelector("#list").appendChild(liEl); }); </script> </body> </html>
 
버튼을 누르지 않은 초기 상태에서 먼저 리플로우 발생 여부를 살펴보겠습니다. 확인을 위해 개발자 도구의 성능 탭으로 들어가 새로고침 버튼을 클릭해서 페이지 로드를 기록합니다.
개발자 도구의 성능 탭에서 새로고침 버튼의 위치개발자 도구의 성능 탭에서 새로고침 버튼의 위치
개발자 도구의 성능 탭에서 새로고침 버튼의 위치
새로고침 버튼을 클릭해서 페이지 로드를 기록하고 있는 화면새로고침 버튼을 클릭해서 페이지 로드를 기록하고 있는 화면
새로고침 버튼을 클릭해서 페이지 로드를 기록하고 있는 화면
 
페이지 로드 기록이 끝나면 이벤트 로그 탭에 들어가 레이아웃을 검색합니다. 검색 결과를 보면 레이아웃 계산이 한 번만 일어났음을 알 수 있습니다. 레이아웃 계산이 한 번만 일어났다는 말은 최초 렌더링 단계에서만 레이아웃 계산이 일어났다는 말과 같습니다. 따라서 이 경우에는 재생성된 렌더 트리를 바탕으로 레이아웃 계산을 다시 진행하는 리플로우 과정이 발생하지 않았다는 것을 알 수 있습니다.
버튼을 클릭하지 않은 초기 상태에서 레이아웃 계산 횟수를 확인하는 화면버튼을 클릭하지 않은 초기 상태에서 레이아웃 계산 횟수를 확인하는 화면
버튼을 클릭하지 않은 초기 상태에서 레이아웃 계산 횟수를 확인하는 화면
 
이제 리플로우를 발생시킨 후 살펴보겠습니다. 다시 새로고침 버튼을 클릭한 후 페이지 로드 기록이 진행되는 동안 버튼을 3번 클릭해 리스트 아이템을 추가합니다.
페이지 로드 기록이 진행되는 동안 버튼을 3번 클릭해 리스트 아이템을 추가한 화면페이지 로드 기록이 진행되는 동안 버튼을 3번 클릭해 리스트 아이템을 추가한 화면
페이지 로드 기록이 진행되는 동안 버튼을 3번 클릭해 리스트 아이템을 추가한 화면
 
이 경우에는 총 4회의 레이아웃 계산이 발생합니다. 최초 렌더링 시 한 번, 버튼을 클릭할 때마다 리스트 아이템이 추가되면서 리플로우가 세 번 일어났기 때문입니다.
3개의 리스트 아이템을 추가한 상태에서 레이아웃 계산 횟수를 확인하는 화면3개의 리스트 아이템을 추가한 상태에서 레이아웃 계산 횟수를 확인하는 화면
3개의 리스트 아이템을 추가한 상태에서 레이아웃 계산 횟수를 확인하는 화면
 

4.2.2.2. 리페인트 발생 확인하기

크롬 개발자 도구 콘솔 패널의 렌더링 탭에서 페인트 플래시를 선택하면 리페인트 되는 요소가 강조 표시됩니다.
개발자 도구 콘솔 패널의 렌더링 탭에서 페인트 플래시를 선택한 화면개발자 도구 콘솔 패널의 렌더링 탭에서 페인트 플래시를 선택한 화면
개발자 도구 콘솔 패널의 렌더링 탭에서 페인트 플래시를 선택한 화면
💡
콘솔 패널이 보이지 않으면? 개발자 도구를 연 후 ESC 키를 눌러보세요. 렌더링 탭이 보이지 않으면? 콘솔 왼쪽의 케밥 메뉴 아이콘을 눌러보세요.
notion imagenotion image
 
페인트 플래시 기능을 이용하면 언제 어떤 요소에 리페인트가 일어나고 있는지 확인할 수 있습니다. 아래 예시는 크롬 개발자 도구 테스트 샘플 페이지[28]에서 확인한 화면입니다.
페인트 플래시 기능 사용 시 화면페인트 플래시 기능 사용 시 화면
페인트 플래시 기능 사용 시 화면
💡
개발자 도구를 이용한 성능 분석 방법을 더 자세히 알고 싶나요? 크롬 개발자 도구 공식 사이트의 성능 챕터[29]를 참고해보세요! 성능 탭 사용법, 성능 분석 방법, 타임라인 이벤트 등에 대한 상세한 설명이 있습니다.
 

4.2.3. 리플로우, 리페인트 최적화 방법

여러 가지 방법이 있겠지만 이번 챕터에서는 리플로우, 리페인트를 간단히 최적화할 수 있는 세 가지 방법에 대해서 알아보겠습니다.
 

4.2.3.1. 불필요한 DOM 요소 없애기

notion imagenotion image
id=”hello”div 요소의 레이아웃을 변경했을 때 왼쪽과 오른쪽 트리 중 어느 트리가 영향을 더 많이 받을까요? 당연히 왼쪽 트리보다 노드의 수가 많고 깊이가 깊은 오른쪽 트리가 더 영향을 많이 받을 것입니다.
이처럼 DOM 트리가 커지게 되면 레이아웃 변경 시 계산해야 하는 요소들이 많아져 리플로우에 걸리는 시간이 늘어나게 됩니다. 따라서 리플로우 최적화를 위해서는 불필요한 DOM 요소를 제거하는 게 중요합니다.
 

4.2.3.2. 애니메이션이 적용된 요소에 절대 위치나 고정 위치 사용하기

애니메이션이 적용된 요소에는 position: absoluteposition: fixed를 사용하는 것이 좋습니다. 절대 위치나 고정 위치를 사용하면 일반적인 문서 흐름에서 벗어나게 되어 크기나 위치에 변화가 생겨도 다른 요소들에 영향을 미치지 않기 때문입니다.[30]
 
예제를 통해 고정 위치를 사용하기 전, 후의 렌더링 시간을 비교해보겠습니다. 아래 예제를 실행하면 너비, 높이 변화 애니메이션이 적용된 박스가 나타납니다. 이 박스는 일반적인 문서 흐름을 따르고 있어서 크기가 변화할 때마다 다른 요소들의 레이아웃에도 영향을 줍니다.
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>절대 위치나 고정 위치 사용하기</title> <style> #box { width: 10px; height: 10px; background-color: salmon; animation: changeSize 1s alternate infinite; } @keyframes changeSize { to { width: 300px; height: 300px; } } </style> </head> <body> <div id="box"></div> <h1>🖤</h1> <p>잡</p> <p>았</p> <p>다</p> <p>요</p> <p>돔</p> </body> </html>
위 예제를 실행한 후 리플로우, 리페인트에 걸린 시간을 알아보기 위해 개발자 도구의 성능 탭에서 새로 고침 버튼을 클릭해 페이지 로드를 기록합니다. 기록이 끝난 후 요약 탭에 출력 되는 타임라인을 확인해봅시다. 확인 결과 이 경우에는 렌더링 이벤트에 41밀리초, 페인팅 이벤트에 31밀리초가 걸렸다는 것을 알 수 있습니다.
일반적인 문서 흐름을 갖고 있는 요소에 애니메이션을 적용한 경우일반적인 문서 흐름을 갖고 있는 요소에 애니메이션을 적용한 경우
일반적인 문서 흐름을 갖고 있는 요소에 애니메이션을 적용한 경우
💡
크롬 개발자 도구의 타임라인에서 렌더링 이벤트와 페인팅 이벤트의 의미[31] 렌더링 이벤트 : 렌더 트리를 바탕으로 각 요소의 정확한 위치와 크기를 계산하는 과정(레이아웃과 리플로우) 페인팅 이벤트 : 계산된 위치를 토대로 화면에 색상을 그리는 과정(페인트와 리페인트)
 
아래 코드는 position: fixed를 이용해 고정 위치를 사용하도록 CSS를 수정한 코드입니다.
#box { position: fixed; width: 10px; height: 10px; background-color: salmon; animation: changeSize 1s alternate infinite; } @keyframes changeSize { to { width: 300px; height: 300px; } }
위 CSS 코드를 반영하여 실행한 후 페이지 로드 기록을 확인한 결과 렌더링 이벤트 24밀리초, 페인팅 이벤트 17밀리초로 일반적인 문서 흐름에 따라 배치되었을 때보다 렌더링, 페인트 이벤트에 걸리는 시간이 줄어들었습니다.
고정 위치를 갖고 있는 요소에 애니메이션을 적용한 경우고정 위치를 갖고 있는 요소에 애니메이션을 적용한 경우
고정 위치를 갖고 있는 요소에 애니메이션을 적용한 경우
 

4.2.3.3. transform 사용하기

요소를 확대/축소하거나 요소의 이동이 필요할 때는 width, height나 top, left 같은 속성보다 transform 속성을 사용하는 것이 좋습니다. transform은 리플로우, 리페인트를 발생시키지 않고, 레이어를 합성하는 컴포지션 단계만 다시 수행하도록 하는 속성이기 때문입니다.
예제를 통해 transform 속성을 사용했을 때, 사용하지 않았을 때의 렌더링 시간을 비교해보겠습니다. 아래 예제는 width, height를 사용해서 박스를 확대/축소하는 코드입니다.
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>transform 사용하기</title> <style> #box { width: 50px; height: 50px; background-color: salmon; animation: changeSize 1s alternate infinite; } @keyframes changeSize { to { width: 300px; height: 300px; } } </style> </head> <body> <span>width, height 사용</span> <div id="box"></div> </body> </html>
해당 코드를 실행했을 때는 렌더링 이벤트에 32밀리초, 페인팅 이벤트에 20밀리초가 소요되었습니다.
 width, height 속성을 사용해서 요소의 크기를 변경한 경우 width, height 속성을 사용해서 요소의 크기를 변경한 경우
width, height 속성을 사용해서 요소의 크기를 변경한 경우
 
아래 예제는 transform 속성을 사용해서 박스를 축소/확대하도록 수정한 코드입니다.
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>transform 사용하기</title> <style> #box { width: 50px; height: 50px; background-color: salmon; animation: changeSize 1s alternate infinite; transform-origin: top left; } @keyframes changeSize { to { transform: scale(6); } } </style> </head> <body> <span>transform 사용</span> <div id="box"></div> </body> </html>
수정한 코드 실행 결과 렌더링, 페인팅 이벤트 모두 0밀리초로 매우 많은 시간이 절약되었습니다.
transform 속성을 사용해서 요소의 크기를 변경한 경우transform 속성을 사용해서 요소의 크기를 변경한 경우
transform 속성을 사용해서 요소의 크기를 변경한 경우
💡
애니메이션 구현에도 transform을 이용하세요! transform을 사용하면 CPU 대신 GPU가 렌더링을 처리하는 하드웨어 가속이 적용됩니다.[32] 간단한 작업을 병렬처리 하는데 특화된 GPU를 이용하면 CPU로 처리하는 것보다 훨씬 빠르고 부드럽게 애니메이션을 그릴 수 있습니다.
 
React, Vue.js 같은 모던 SPA 라이브러리들은 렌더링 성능 최적화를 위해 가상 DOM을 이용합니다. 가상 DOM에 대해서는 다음 챕터에서 자세히 알아보겠습니다.
 

4.3. 가상 DOM

4.3.1. MPA와 SPA

MPA(Multi-page Application)란 두 개 이상의 페이지로 구성된 웹사이트로, 사용자와 상호작용하기 위해서는 현재 접속 중인 페이지에서 다른 페이지로 이동하여 그 때마다 화면이 다시 새로고침되는 방식으로 작동합니다. 사용자가 웹사이트를 이용할 때, 콘텐츠의 내용이 바뀌거나 어떤 기능을 실행하기 위해서는 여러 개의 페이지를 오가며 매번 대상 페이지가 렌더링 돼야 합니다. 예를 들어서 브라우저 화면에 강과 산, 그리고 해의 그림을 표시해주는 index.html이 존재하고, 해는 없고 강과 산의 그림만 표시해주는 01.html이 존재한다고 가정해보겠습니다. 처음 접속 시에 index.html 페이지를 불러온 뒤에, 해가 없는 그림으로 화면이 바뀌기 위해서는 01.html을 새로 불러와 렌더링 해야 하는 것입니다. MPA로 이루어진 웹사이트들은 주로 서버 사이드 렌더링(SSR) 방식을 사용합니다.
해가 없는 화면을 보고 싶었다면해가 없는 화면을 보고 싶었다면
해가 없는 화면을 보고 싶었다면
새로운 페이지를 불러와 렌더링해야 합니다.새로운 페이지를 불러와 렌더링해야 합니다.
새로운 페이지를 불러와 렌더링해야 합니다.
반면 SPA(Single-page Application)란 한 개의 페이지만으로 구성되어, 최초 접속 시에 동작에 필요한 모든 자료들을 한 번에 다운로드하고, 페이지 전환 없이 콘텐츠의 내용을 변경하거나 다양한 기능을 실행할 수 있는 웹사이트를 얘기합니다. SPA로 이루어진 웹사이트들은 주로 클라이언트 사이드 렌더링(CSR) 방식을 사용합니다.
MPA로 구성된 웹사이트 환경에서 화면의 데이터가 변경될 경우 다른 페이지를 불러와 새로고침 하기 때문에 화면이 깜빡거립니다. 받아와야 할 데이터가 많아 받는 시간이 오래 걸릴 경우에는 사용자가 아무 것도 없는 화면을 그동안 바라보고 있어야 할 수도 있습니다. 이러한 불편을 해소하고 보다 나은 사용자 경험을 제공하기 위해서 최근에는 대부분의 웹사이트들이 SPA로 구성됩니다. 하지만 이러한 SPA 환경에서도 DOM 조작을 통해 데이터가 변경될 경우, 렌더링 과정을 다시 거쳐야 합니다. 잦은 리플로우, 리렌더링 과정은 많은 비용을 발생시키기 때문에 이를 조절하기 위해 다음 소개드릴 가상 DOM이 등장하게 됐습니다.[33]
 

4.3.2. 가상 DOM

지금의 웹사이트들은 갈수록 더 많은 기능을 제공하고, 또 보다 자연스러운 사용자 경험을 제공하기 위해서 DOM을 빈번히 조작하게 되고, 조작 방법 또한 더욱 복잡해져 가고 있습니다. 이런 환경에서 DOM의 조작이 이루어질 때마다 전체적인 렌더링 과정을 계속 거치는 것은 매우 많은 비용을 수반하고, 따라서 기대와 달리 사용자 경험에 악영향을 끼칠 수 있습니다. 가상 DOM은 렌더링 과정을 최소화 시키기 위해 사용됩니다.[34]
가상 DOM을 이용해 렌더링하는 과정입니다.가상 DOM을 이용해 렌더링하는 과정입니다.
가상 DOM을 이용해 렌더링하는 과정입니다.
가상 DOM은 실제 DOM을 복제하여 추상화한 객체로, 실제 DOM이 가지고 있는 DOM API는 제공하지 않습니다. 단지 실제 DOM의 노드들과 스타일 요소들을 기억하고 있다가 DOM 조작이 이루어진 경우, 변경사항들을 계산하여 한 번에 실제 DOM에 전달합니다. 그 다음 실제 DOM에서는 전체 렌더링 과정을 다시 거치지 않고, 변경된 부분만을 렌더링 합니다. 가상 DOM을 이용하면 기존에 여러 번 렌더링 하던 과정을 합쳐서 한 번에 진행할 수 있는 것입니다. 가상 DOM은 React와 Vue.js 라이브러리에서 주로 사용됩니다. 하지만 이는 렌더링 비용을 줄이기 위한 절대적인 방법이 아닙니다. Angular 프레임워크에서는 가상 DOM 대신 실제 DOM을 통해 변경사항을 계산하고 적용하는 Incremental DOM 기술을 사용합니다. 또한 Svelte 컴파일러는 컴파일 시점에서 가상 DOM과 같은 계산을 미리 진행하여 이러한 기술을 위해 다른 코드를 추가할 필요가 없습니다.
💡
Svelte란?[35] 2016년에 출시된 반응형 웹 어플리케이션 개발을 위한 도구입니다. 적은 코드 작성, 가상DOM을 사용하지 않음, 진정한 반응성을 특징으로 내세우고 있습니다. 이러한 특징들은 소스코드의 용량을 줄이고, 웹페이지가 동작할 때 더욱 빠른 속도로 반응하게 해주어 사용자 경험을 향상시킬 수 있습니다. 이런 특징들을 가질 수 있는 데에는 특별한 이유가 있습니다. React와 Vue 같은 다른 라이브러리를 사용했을 때에는 기본적으로 사용자가 소스코드와 함께 해당 라이브러리를 받아와야 하고, 실제로 브라우저 내에 웹페이지가 실행될 때 소스코드를 해석하게 됩니다. 반면 Svelte는 배포하기 전 컴파일하는 과정에서 소스코드를 해석하며, 따라서 꼭 필요한 정보들만 담은 배포 파일을 만드는 데 차이가 있습니다.
 

Reference

1. 브라우저 동작 원리를 알아야 하는 이유
 
2. 브라우저 동작 원리
 
3. 브라우저 렌더링 과정
 
4. 브라우저 렌더링 과정 최적화 방법