[WEB] 'www.google.com' 을 입력하면 (3)

'www.google.com'을 URL 입력창에 입력하면 어떻게 될까?

: 흔히 면접에서 단골 질문(?)으로 나오는 이 질문에 대해서 파헤쳐보자

Chapter

TCP connection (4 way handshake)

4 way handshake

: 이전 포스팅에 이어서 '4 way handshake'에 대해서 설명해보고자 한다. 먼저, 비유적으로 4 way handshake를 설명해보면,

A : 나는 할말 다했고 이제 연결 끊을게 !
B : 연결 끊는다고? 알겠어! + 아직 도착하지 않은 메시지를 기다림
B : 나도 할말 다했고 이제 연결 끊을게 !
A : 연결 끊는다고? 알겠어! + 아직 도착하지 않은 메시지를 기다림
A,B 모두 일정 시간의 기다림 끝에 최종적으로 전화를 끊음

사실 일상적인 전화 방식은 아니라서 비유가 좀 애매하지만, 전체 틀을 이해하기 좋은 비유라고 생각한다. 송수신을 끊는 데에도 이렇게 많은 과정이 필요한 이유는 뭘까?. 만약 서로가 통신을 끊는다고 말하고, 상대가 이를 확인하고 끊지 않으면 상대쪽에서는 통신이 끝난줄 모르고 다른 데이터를 송신할 수 있는데, 이렇게되면 이미 수신측에서는 소켓을 말소하고, 다른 소켓이 만들어졌을 수 있는데, 이경우 소켓에 정보가 우연히 일치하는 요청을 송신측에서 보냈다면, 잘못된 송수신이 이뤄지는 결과를 낳는다. 혹은 송신측에서는 데이터를 받았다는 응답을 못받게 되므로 쓸데없이 다시 요청을 보내야할 수 있다.

그러면 4 way handshake는 정확히 어떻게 이뤄질까?. 위에서 프로토콜 스택의 소켓 라이브러리 중 close() 메서드를 브라우저가 실행하면 4 way handshake가 이뤄진다. 먼저, 데이터를 다 받았다고 판단 혹은 다 보냈다고 판단(수신측이든, 송신측이든 close를 먼저 하는 쪽은 정해져있지 않다)되면 FIN이라는 플래그 비트를 보낸다. 그래서 이제 소켓을 말소하고, 연결을 끊자라는 의견(?)을 표현한다. 그러면 이를 받는쪽에서는 이를 받아서 바로 연결을 끊지 않고, 아직 수신하지 못한 데이터가 있을 수 있기에 CLOSE_WAIT 시간을 갖게 된다(이 때, FIN 플래그 비트를 잘 받았다는 ACK 비트를 따로 보낸다). 그런 다음에 수신측에서도 통신할 부분이 끝났다고 판단되면 FIN 플래그 비트를 보낸다. 그러면 이를 받는쪽에서도 바로 연결을 끊지 않고 기다리는 시간을 갖고, 아까와 마찬가지로 잘받았다는 ACK를 보낸다. 그리고 이 ACK를 받으면 최종적으로 소켓을 말소하고, 연결을 해제한다.

여기까지 정리

: 여기까지 구글 닷컴을 url창에 입력 => 브라우저의 URL 파싱 => HTTP message 생성 => DNS 조회 => 프로토콜 스택에 의뢰 및 송수신 과정 진행(socket, connect, write, read, close) => 최종적으로 html 파일 및 번들링 파일이 브라우저로 오게되고, 여기서부터 흔히 말해 브라우저 렌더링 원리와 연결되는 부분이 진행된다. 본래 OSI 7 layer and TCP/IP 모델에 대해서 설명하려고 했으나 흐름상 브라우저 렌더링 과정을 먼저 포스팅하겠다.

DOM + CSSOM => Render Tree

HTML, CSS, JS 파싱 by Browser

: 브라우저가 HTML, app.js 와 같은 번들링 파일 등을 서버로부터 받아온 뒤에 이를 '파싱'하는 과정이 먼저 진행된다. HTML 파일을 먼저 파싱하는데, 이 때, 브라우저는 바이트코드로 도착한 HTML 문서를 meta tag에 설정해주는 etf-8과 같은 엔코딩 형식을 참고해서 문자열로 디코딩한다. 그 다음에 그 문자열을 바탕으로 의미의 최소 단위인 '토큰'으로 토크나이징하고, 이를 바탕으로 우리가 'DOM'이라고 부르는 계층적 구조를 가진 트리 자료구조를 만든다.

DOM이란?

: DOM은 앞서 말했듯이 HTML의 계층적 구조를(root=document, 그 아래 어트리뷰트 노트, 텍스트 노드, 요소 노드) 트리 자료 구조로 만들어서 브라우저가 읽을 수 있고, DOM API 등을 제공해서 컨트롤 할 수 있도록 만든 것이다.

HTML, CSS, JS 파싱 by Browser

: 다시 돌아와서, HTML을 파싱해서 DOM을 만드는 동시에(즉, HTML을 한줄 한줄 위에서부터 파싱하면서) head 태그 안에 script, link태그로 호출하는 css, js 파일도 파싱한다(이 때, 아까 말했듯이 한줄 한줄 동기적으로 실행하기 때문에 만약에 script 태그를 만나면 js 실행을 하는 데에 있어서 블로킹이 걸린다). 먼저, css 파일을 파싱 과정은 HTML -> DOM 파싱 과정과 똑같이 이뤄지고, CSS는 결과적으로 CSSOM으로 파싱된다. 이 때, head 태그 안에 css 호출을 해주면 css는 DOM을 건들지 않기 때문에 DOM 파싱과 병렬적으로 처리된다고 한다(JS의 경우 DOM을 건들 수 있기 때문에 블로킹돼서 파싱된다). JS 파싱 과정은 다음 챕터에서 다뤄보고, 이렇게 HTML => DOM & CSS => CSSOM // DOM + CSSOM => Render Tree를 구성하게 된다는 것을 알고 넘어가자.

JS parsing(+ defer & async attribute) & AST(Abstract Syntax Tree)

AST

: JS는 보통 script 태그를 통해 js 파일을 호출하고, 이를 실행하여 DOM에 조작을 가하기도 하고, 어떤 로직을 실행하기도 한다. JS 파싱 과정은 먼저 JS도 바이트코드로 받아오고, 이를 문자열로 파싱후 토크나이징 과정을 똑같이 진행한다. 그 다음에 JS는 AST(Abstract Syntax Tree)로 HTML => DOM으로 파싱되는 것처럼 JS 만의 트리 구조의 자료구조로 만들어지고(객체들로 이뤄진), 이를 바탕으로 실제 실행을 하게된다(인터프리터).

defer & async

: 예전부터 js를 호출하는 script 태그는 </body> 태그 직전에 쓰라는 일종의 편법(?)까지는 아닌 스킬을 들어왔을 것이다. 이는 브라우저가 HTML을 파싱할 때 아까 말했듯이 싱글 스레드처럼 작동을 하기 때문에 js를 호출해서 실행하면 블로킹이 일어나는 것을 방지하기 위한 기술을 말한다. 예를 들어, head 태그 안에 script 태그로 js를 호출 및 실행할 때

const initialData = () => {
	const arr = [];
	for(let i=0;i<=1,000,000,000;i++) {
		arr.push(i);
	}
}

위와 같은 함수를 실행한다고 해보자(원래 숫자에 ','는 못붙이지만 가독성을 위해 넣었다). 저렇게되면 보통 1억개의 데이터 다룰 때 혹은 시행을 할 때 1초가 걸린다는 점을 생각하면 10억번의 시행은 10초정도의 시간이 걸린다. 이 때, head태그 안에, 즉, HTML의 body 태그 안에 부분을 파싱하기 전에 저 함수를 실행하면 유저가 브라우저에서 볼 수 있는 부분을 파싱하기 전에 10초가 딜레이 된다. 어떤 사이트를 켰는데 10초동안 아무것도 안보인다면??.. oh no...

그래서 나온 해결책이 아까말한 body태그의 닫는 태그 직전에 script태그를 두는 방법이다. 이는 body 태그 내에 브라우저에 렌더링 되는 부분을 파싱 후에 js를 실행해서 위와 같은 딜레이를 막아준다.

그럼 async, defer는 뭔가? script 태그에 <script async /> <script defer /> 이런식으로 써주는 async, defer attribute는 비동기적 처리를 지원한다. 먼저, async, defer는 공통적으로 script 태그를 실행할 때 비동기적으로 파일을 다운(호출)받는다. 하지만, async는 호출이 끝나면 바로 실행한다는 점에서 여전히 html 파싱을 블로킹할 수 있다는 점이 특징이고, defer는 호출을 비동기적으로 함과 동시에 호출이 완료돼도 바로 실행하지 않아서 HTML 파싱을 블로킹하지 않는다. 'domContentLoaded' 이벤트가 실행(dom 파싱이 완료됐을 때 발생하는 이벤트)됐을 때 해당 script 태그를 실행하게 하는 attribute이다. 이 두 속성을 이용하여 적절한 비동기 세팅을 해줄 수 있다. async는 여러가지 js파일을 호출할 때 순서 상관없이 일단 비동기적으로 호출을 한다음에 먼저 다운받는 순서대로 실행해도 될 때 쓰면 좋을 것이다.

layout and paint

: 다시 렌더 트리 생성 단계로 돌아와서, 렌더 트리에는 'display: none;'의 특성을 가진 객체는 표현되지 않는다. 이는 말그대로 렌더 트리기 때문에 렌더링 되는 부분에 대한 정보만을 포함하기 때문에 그렇다(렌더링 되지 않는 부분에 대한 정보는 포함x). 이렇게 렌더 트리를 구성했으면 이제 이를 바탕으로 화면의 레이아웃을 측정한다. 어떤 위치에 뭐가 위치하고, 몇 px 아래에 뭐가 있고 등의 정보들을 파악하는 단계이다. 그 다음엔 마지막으로 paint 즉, 실제 앞서 파악한 정보들을 바탕으로 유저가 볼 부분을 그리는 단계가 이뤄지고, 최종적으로 유저에게 렌더링된다.

reflow and repaint(re-rendering)

: 만약 DOM API 혹은 화면 레이아웃 변동 등의 인터랙션이 일어나서 DOM, CSSOM 등이 변하게 되면 렌더 트리를 재구성하고, 이 렌더 트리를 바탕으로 레이아웃을 다시그리고(reflow) 이를 바탕으로 다시 페인팅(repaint)을 하는 리렌더링 과정이 일어난다. 이 때, 이 리렌더링 이슈 때문에 브라우저의 퍼포먼스 혹은 비용 소모 이슈가 생겨나고 여기서 VirtualDOM vs IncrementalDOM 등의 다른 이슈들도 파생된다. 리렌더링 자체가 비용 소모 및 시간이 꽤 걸리는 작업이기에 브라우저의 성능을 높이기 전까지 이 리렌더링 과정을 최소화해야할 책임(?)이 개발자에게 주어진다...ㅎ

여기까지 정리

: 여기까지 'www.google.com'을 URL 검색창에 치면 일어나는 일에대 해서 살펴봤다. 물론 방화벽, 프록시 서버, CDN 등을 방문하는 등의 세부 경로가 또 있지만, 이 부분에 대해서는 추후 공부를 더하고 추가하고자 한다. 이에 더하여 OSI 7 layer는 따로 포스팅을 해보고자 한다.

references

  • 성공과 실패를 결정하는 1%의 네트워크 원리(Tsutomu Tone)
  • 모던 자바스크립트 Deep Dive(이웅모)

좋은 웹페이지 즐겨찾기