[Internet] 브라우저의 동작 방식
인터넷을 사용하는 이들은 빠른 로딩과 원활한 상호작용을 원한다. 이를 위해선 웹 성능이 뒷받침되어야 하는데, 이때 고려하는 주요 사항은 지연시간과 브라우저의 싱글 쓰레드 동작이다.
지연 시간과 같은 경우에는 웹 브라우저가 요청을 보내가 응답을 받아 이를 화면에 반영하는 데 걸리는 시간을 의미한다. 당연히 짧을수록 웹 페이지가 빠르게 로드되어 좋은 사용성을 가져온다.
그리고 대부분의 웹 브라우저는 JS 엔진이 싱글 쓰레드 방식으로 작동한다. 즉 한 번에 하나의 작업만 처리할 수 있는 구조다. JS 코드를 실행하는 동안에는 다른 작업(UI 업데이트, 이벤트 처리 등)을 동시에 처리할 수 없다. 그렇기 때문에 무거운 JS 코드를 실행하게 되면 DOM 렌더링이나 이벤트 처리가 지연될 수 있다.
이러한 점을 고려하면서 웹 브라우저가 어떻게 동작하는지 알아보자.
탐색(Navigation)
탐색은 찾고자 하는 페이지 자원이 어디에 위치하는지 찾고 이것을 가져오기 위한 요청을 보내는 것이다.
DNS 조회(DNS Lookup)
해당 내용은 바로 전 글에서 다뤘으니 이것을 참고하자.
TCP 핸드셰이크(TCP Handshake)
IP 주소를 알아낸 뒤에는, 브라우저는 서버와 TCP 핸드셰이크를 수행한다. 이는 세션 시작 전 3개의 메시지를 주고받는다. 각각 SYN(SYNchronize), SYN-ACK(SYNchronize-ACKnowledgement), ACK(ACKnowledgement)라는 이름을 가진다. 서로 정보를 주고받길 원하는 두 컴퓨터가 HTTP 요청과 같은 데이터를 전송하기 전에 연결 매개변수를 협상하는 과정이다.
TLS 협상(TLS Negotiation)
요즘은 대부분 HTTP에서 보안이 강화된 확장 버전인 HTTPS를 사용한다. 해당 연결을 위해선 TCP 핸드셰이크 이후 또 다른 핸드셰이크인 TLS 협상이 필요하다. 해당 핸드셰이크는 통신 암호화에 쓰일 암호를 결정하고 서버를 확인하여 실제 데이터 전송 전에 안전한 연결이 이뤄지도록 한다. 크게 3단계로 이루어진다.
- Client Hello: 클라이언트가 서버에게 자신이 지원하는 암호봐 방법(Cipher Suite), 지원하는 TLS 버전 등을 전달한다.
- Server Hello: 서버가 Client Hello에 응답하는 단계이다. 서버는 클라이언트의 제안 중 하나를 선택하여 암호와 방법, TLS 버전, 서버 인증서 등을 담아 응답한다.
- Certificate Verification: 클라이언트가 서버에서 보낸 인증서를 검토한다.
- Premaster Secret Generation: 클라이언트는 이후 세션에서 데이터를 암호화하는데 사용되는 Premaster Secret이라는 비밀 정보를 생성한다. 그리고 이것을 서버 공개키로 암호화하여 서버에 전송한다. 그럼 서버는 자신의 비밀 키로 암호를 풀고 premaster secret을 얻는다.
- Session Key Generation & Finished Message: 클라이언트와 서버는 premaster secret을 바탕으로 세션 키를 생성하고 이는 실제 데이터를 암호화하는데 사용된다. 그리고 클라이언트와 서버는 각각 협상을 완료했다는 메시지를 세션 키로 암호화하여 서로에게 전송한다.
- 이후 클라이언트와 서버는 세션 키를 사용해 모든 데이터를 암호화하여 전송한다. 그렇기에 외부의 누군가가 데이터를 가로채도 내용을 알 수 없다.
응답
웹 서버와 연결이 성립되고 난 뒤, 웹 브라우저는 사용자를 대신하여 웹 서버에 첫 번째 요청을 전송한다. 주로 HTML 파일을 요청하게 되는데, 서버는 이 요청을 받으면 다음과 같은 형식의 HTML 파일과 함께 추가 정보를 담은 응답 헤더를 브라우저로 응답한다.
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>TEST</title>
<link rel="stylesheet" src="styles.css" />
<script src="test.js"></script>
</head>
<body>
<h1 class="heading">My Page</h1>
<div>
<img src="test.jpg" alt="image description" />
</div>
</body>
</html>
브라우저가 요청을 보내고 HTML의 첫 번째 데이터를 받기까지 걸리는 시간을 TTFB(Time to First Byte)라고 한다. 해당 데이터는 보통 14kb 정도의 크기다.
그리고 브라우저가 HTML을 읽으면서 링크나 이미지를 만나면, 이 자원들을 따로 요청해서 가져온다. 이 과정이 모두 끝나야 웹 페이지가 완전히 표시된다.
혼잡 제어(Congestion Control) / TCP Slow Start
인터넷에서 데이터를 보낼 때, 데이터를 세그먼트라는 단위로 작게 쪼개어 전송한다. 이때 TCP는 이 세그먼트들이 순서대로 도착하도록 보장한다.(서버는 몇 개의 세그먼트를 보내고 나면, 클라이언트가 데이터를 잘 받았는지 확인하는 ACK 메세지를 전송한다.)
이때 생각해봐야 할 것은 세그먼트를 한번에 너무 적게 혹은 많이 보냈을 때이다. 서버가 세그먼트를 하나씩 보내고, 그때마다 클라이언트의 확인(ACK)을 기다리면 데이터 전송 속도가 느려질 것이다. 반대로, 서버가 한 번에 너무 많은 세그먼트를 보내면 네트워크가 혼잡해질 수도 있을 것이다.
이에 대한 해결책이 TCP Slow Start이다. 처음에는 적응 양의 데이터를 보내고, 클라이언트가 데이터를 잘 받았다는 확인(ACK)을 보내면 전송량을 점차적으로 늘린다. 그리고 네트워크 상태에 따라 전송량을 조절한다.
이 과정에서 한 번에 보낼 수 있는 세그먼트의 수를 의미하는 혼잡 윈도우(CWND)라는 개념이 쓰인다. 이 값은 처음에 작게 시작하지만, 클라이언트가 ACK를 보낼 때마다 2배로 증가한다. 만약 ACK를 받지 못하면 CWMD가 반으로 감소한다. 이렇게 세그먼트 전송량의 균형을 맞추는 것이다.
구문 분석(Parsing)
브라우저가 첫 번째 데이터 청크를 받으면, 그 정보를 분석한다. 이 과정은 브라우저가 네트워크로부터 받은 데이터를 DOM이나 CSSOM으로 바꾸는 단계이다. 이는 렌더러가 화면에 페이지를 그리는 데 사용된다. 브라우저는 HTML 마크업을 내부적으로 DOM 형태로 표현하며, 이 DOM은 JS의 다양한 API를 통해 조작할 수 있다.
요청한 HTML 페이지의 크기가 처음 받은 데이터(약 14kb) 보다 커도, 브라우저는 이미 받은 데이터로부터 분석을 시작하고 페이지 렌더링을 시도한다. 이는 웹 최적화에서 중요한데, 첫 14kb 안에는 페이지 렌더링에 필요한 최소한의 HTML이나 CSS가 포함되어야 한다는 의미이다. 그리고 화면 렌더링 이전에 HTML, CSS, JS를 구문 분석해야 한다.
이렇게 HTML, CSS, JS를 화면에 픽셀로 변환하는 일련의 단계를 중요 렌더링 경로(Critical Rendering Path)라고 한다. 이를 다섯 단계로 살펴보자
DOM 트리 구축(Building the DOM Tree)
웹 페이지를 로드할 때, 브라우저는 먼저 HTML 코드를 읽고 처리한다. 이 과정에서 브라우저는 HTML 문서를 토큰이라는 작은 조각들로 나누고, 이를 기반으로 DOM 트리라는 구조를 만든다.
토큰은 HTML의 시작 태그, 종료 태그, 속성 이름, 속성 값 등을 의미한다. 예를 들어, <div class="container">라는 HTML 코드에서 <div>는 시작 태그, class="container"는 속성과 값의 조합이 된다.
DOM 트리는 웹 문서의 구조를 설명하는 일종의 지도와 같다. 예를 들어, 최상위 요소인 <html> 태그는 DOM 트리의 최상위 노드가 되고, <div> 안에 <p>가 있으면 <p>는 <div>의 자식노드가 된다. 이렇듯 해당 트리 구조는 태그 간의 관계를 반영한다. DOM 트리의 노드가 많을수록 트리 생성에 더 많은 시간이 걸린다. 또한 다른 유형의 자원이 로드될 때 구문 분석의 흐름이 아래와 같이 달라질 수 있다.
- 이미지: 구문 분석기가 HTML을 읽다가 이미지를 만나면 브라우저는 이미지를 서버에 요청하면서 동시에 HTML 구문 분석을 계속한다. 즉 이미지 로드 과정은 HTML 구문 분석을 방해하지 않는다.
- CSS: CSS 파일을 만나도 HTML 구문 분석은 계속된다. CSS 파일이 로드되고 적용될 때까지 페이지의 스타일을 표시되지 않아도 전체 구문 분석 과정은 멈추지 않는다.
- <script> 태그: async나 defer 같은 특별한 설정이 없는 한, 브라우저는 스크립트를 먼저 실행한 후에 구문 분석을 계속할 수 있다. 그렇기에 스크립트는 페이지 로드 과정에서 병목 구간이 될 수 있다.
프리로드 스캐너(Preload Scanner)
DOM 트리를 만드는 작업은 브라우저의 메인 쓰레드에서 실행된다. 메인 쓰레드는 브라우저에서 가장 중요한 작업을 처리한다. 메인 쓰레드가 HTML 코드를 처리하는 동안 프리로드 스캐너라는 도구가 별도로 작동한다. 이 스캐너는 HTML에서 중요 자원(CSS, JS, 이미지, 폰트 등)을 미리 찾아 브라우저가 다운로드를 시작할 수 있게 한다. 이렇게 하면 브라우저가 필요한 자원을 미리 받아볼 수 있어서, HTML을 처리하면서 자원이 로드되는 걸 기다리지 않을 수도 있다.
<link rel="stylesheet" src="styles.css" />
<script src="myscript.js" async></script>
<img src="myimage.jpg" alt="image description" />
<script src="anotherscript.js" async></script>
예를 들어 위와 같은 경우에는 메인 쓰레드가 HTML과 CSS를 분석하고 있을 때, 프리로드 스캐너는 스크립트와 이미지를 다운로드하기 시작할 것이다.
CSSOM 구축
CSSOM이란 CSS를 브라우저가 이해할 수 있도록 변환한 구조다. 이 구조는 DOM가 유사한 트리 구조로 되어있다. 브라우저는 웹 페이지의 CSS 파일을 받아서 그 안의 규칙들을 분석하여 CSSOM을 생성한다.
브라우저가 CSS 규칙을 적용할 때는, 가장 일반적인 규칙부터 시작해 점점 더 구체적인 규칙을 적용한다. 이렇게 여러 규칙이 겹쳐지며 최종 스타일이 정해지는 과정을 캐스케이딩이라고 한다.
CSSOM 트리를 만드는 과정은 매우 빠르게 이뤄지는 편이다. 보통은 웹 페이지 로딩에 큰 영향을 끼치지 않으며, 한 번의 DNS 조회보다도 짧은 시간이 소요된다. 다르게 말하면 웹 성능 최적화에서는 큰 개선 여지가 없는 부분이다.
JavaScript 컴파일
CSS가 분석되고 CSSOM이 만들어지는 동안 브라우저는 JS 파일과 같은 다른 자원도 다운로드한다. JavaScript는 해석 -> 컴파일 -> 분석 과정 이후 실행된다. 이 과정에서 스크립트는 추상 구문 트리로 변환된다. 일부 브라우저 엔진은 이 추상 구문 트리를 인터프러터에 전달하여, 메인 쓰레드에서 실행될 바이트코드를 생성한다.
접근성 트리 구축
브라우저는 접근성 트리를 생성한다. 보조 기술(ex. 화면 리더기)은 이 트리를 이용해 웹 컨텐츠를 이해하고 해석한다. 접근성 객체 모델(AOM)은 DOM의 의미를 담고 있다. 브라우저가 DOM을 업데이트하면 AOM도 업데이트된다.
렌더
렌더링 과정은 웹 페이지가 사용자의 화면에 어떻게 그려지는지를 설명한다. 이 과정의 각 단계에 대해 살펴보자.
스타일
구문 분석 단계에서 도출했던 DOM과 CSSOM을 합성하여 렌더 트리를 만들게 된다. 렌더 트리는 화면에 실제로 보일 요소들만 포함한다. 예를 들어 HTML 요소 중 "display: none;" 스타일이 적용된 요소는 렌더 트리에서 제외된다("visibility: hidden" 속성 같은 경우에는 자리를 차지하기 때문에 렌더트리에 포함). 그리고 보여지는 요소는 그 노드에 해당하는 CSSOM 규칙이 있고 CSS 캐스케이드 방식에 따라 해당 노드의 계산된 스타일을 결정한다.
레이아웃
렌더 트리가 생성되고 이를 바탕으로 각 노드의 정확한 위치와 크기를 계산하는 단계이다. 렌더 트리는 각 노드의 위치나 좌표를 알지는 못하므로 각 객체의 정확한 위치와 크기를 결정하기 위해 브라우저는 렌더 트리의 루트부터 시작하여 순회한다.
이때 모든 요소는 위와 같은 박스 모델에 따라 처리된다. 즉 각 요소는 마진, 테두리, 패딩, 실제 내용의 크기를 포함하여 계산된다. 그리고 레이아웃 계산은 뷰포트(브라우저 화면)의 크기를 기준으로 수행된다. 그러므로 다양한 디스플레이 크기와 해상도에 따라 요소의 크기와 위치가 조정된다.
이렇게 노드의 위치와 크기가 처음 계산된 후, 페이지의 일부분이나 전체에 대한 크기나 위치가 변경되면 리플로우가 발생한다. 예를 들어 이미지 로딩이 늦어져 해당 노드의 크기가 페이지 로드 후에 결정되면 이미지 주변의 레이아웃을 다시 계산할 수도 있다. 리플로우는 성능에 영향을 줄 수 있다. 그렇기에 크기를 미리 지정해 주는 방법 등을 사용해 해당 현상을 줄일 수 있다.
페인트
브라우저가 처음으로 사용자에게 의미가 있는 내용을 화면에 그리는 단계이다. 즉 이 과정에서 웹 페이지의 각 요소가 픽셀로 변환되어 실제 화면에 보인다. 처음으로 화면에 내용은 나타나는 것을 첫 번째 의미 있는 페인트(FMP)라고 한다. 이 단계에서 텍스트, 색상, 경계선, 그림자, 버튼, 이미지 등 시각적 요소가 화면에 그려진다.
브라우저는 이 모든 작업을 빠르게 처리해야 한다. 부드러운 스크롤과 애니메이션을 위해서 브라우저의 메인 쓰레드가 스타일 계산, 리플로우, 페인트 작업으로 부하를 받지 않도록 16.67ms 이내로 처리하는 것이 중요하다. 이를 위해서 페인트 작업은 몇 개의 레이어로 구분된다.
합성
위에서 페인트 작업이 몇 개의 레이어로 구분이 된다고 했는데, 이를 합성이라고 한다. 웹 페이지의 다양한 섹션이 서로 다른 레이어에서 그려지고 이 레이어들이 올바른 순서로 화면에 겹쳐져 화면에 나타난다. 그리고 이는 페이지가 계속해서 자원을 로드하는 동안에도 정확한 렌더링을 보장한다. 예를 들어 이미지 로딩이 늦어져 페이지 레이아웃에 영향을 줄 수 있는데, 이때 이미지 사이즈를 미리 지정하지 않았다면 이미지 로딩 후 레이아웃 단계로 돌아가 다시 레이아웃 설정을 시작하게 된다. 이때 필요한 부분만을 다시 페인트 하고 필요하다면 다시 합성할 수 있다.
상호작용
웹 페이지가 화면에 모두 그려졌다고 모든 준비가 끝난 것은 아니다. 예를 들어 페이지가 로드되어도 지연된 JS 파일을 다운로드하고 실행해야 한다면, 그동안 메인 쓰레드는 계속 바쁘게 작동하게 된다. 이로 인해 스크롤, 터치, 클릭 같은 상호작용이 제대로 이루어지지 않을 수 있다.
TTI(Time To Interactive)는 웹 페이지가 사용자와 상호작용할 준비가 되기까지의 시간을 측정하는 지표다. 첫 콘텐츠가 화면에 나타난 후, 페이지가 사용자 입력에 50ms 이내로 반응할 수 있을 때를 '상호작용 가능' 상태로 본다. 결론적으로 JS 파일의 다운로드, 분석, 실행이 메인 쓰레드를 너무 오래 점유하지 않도록 유의할 필요가 있다.
참고자료