개인적인 학습을 위해 이 곳 에 있는 원본을 번역한 내용입니다. 오역이나 오류가 보이면 주저말고 Pull request를 만들어주세요. 어떤 의견이든 환영합니다.
이 저장소는 고전적인 인터뷰 질문인 "당신의 브라우저 주소창에 google.com을 치고 엔터를 누르면 어떤 일이 생길까요?"에 대해 답해보고자 합니다.
일반적인 얘길 하는 대신에, 우리는 가능한 한 세부적으로 이 질문에 답하고자 노력할 것입니다. 아무것도 빼먹지 않고 말입니다.
이 저장소는 공동 작업물이 되어야 합니다. 함께 파고들고 도움을 주시면 감사하겠습니다! 분명 수많은 디테일상의 실수들이 있으니, 여러분이 힘을 보태주시길 기다리겠습니다! Pull request를 보내주세요!
이 저작물은 Creative Commons Zero 라이센스를 따릅니다.
- "g"키를 누르면
- "엔터"키가 쏙 들어갑니다
- 인터럽트 발생 [키보드가 USB가 아닌 경우에]
- (Windows에서)
WM_KEYDOWN
메시지가 앱으로 전달되어요 - (OS X에서)
KeyDown
NSEvent가 앱으로 전달되어요 - (GNU/Linux에서) Xorg 서버가 키코드를 listen해요.
- URL 파싱하기
- 검색어일까 URL일까?
- 호스트명에서 ASCII 아닌 유니코드 문자열 변환
- HSTS 리스트 점검
- DNS 검색
- ARP 프로세스
- 소켓 열기
- TLS handshake
- HTTP 프로토콜
- HTTP 서버의 요청 처리
- 브라우저의 이면에서
- 브라우저
- HTML 파싱
- CSS 해석
- 페이지 렌더링
- GPU 렌더링
- 윈도우 서버
- 렌더링 후처리와 사용자에 의해 유도된 동작
이번 절에서는 키보드의 물리적인 동작과 운영체제의 인터럽트에 대해 다룹니다. 당신이 "g" 키를 누르면 브라우저는 해당 이벤트를 전달받고 자동완성 기능이 활성화됩니다. 당신이 사용하는 브라우저의 알고리즘과 당신이 개인/익명 모드를 사용하는지에 따라 다양한 제안이 URL 창 아래에 드롭다운으로 나타나죠. 대부분의 알고리즘은 결과를 검색 기록, 즐겨찾기, 쿠키, 그리고 인터넷에서 인기있는 검색어에 기반해 정렬하고 우선순위를 매깁니다. 당신이 "google.com"을 입력하는 동안 많은 코드 블록이 실행되고 매 키를 누를 때마다 제안은 정교해질 것입니다. 심지어 당신이 입력을 마치기도 전에 "google.com"을 제안할지도 모르겠네요.
명확히 설명드리기 위해, 키보드의 엔터키가 끝까지 눌러졌다고 해봅시다. 여기서, 엔터키에 할당된 전기 회로가 (직접적으로든 정전식으로든) 닫힙니다. 이것이 적은 양의 전류를 키보드에서부터 각 키 스위치 상태를 확인하는 논리 회로 소자에 흐르도록 하고, 빠르고 간헐적인 스위치 차단으로 인한 전기적 잡음을 디바운싱하며, 신호를 키코드 정수, 이 경우에는 13으로 변환해줍니다. 키보드 컨트롤러는 곧, 키코드를 인코딩해 컴퓨터로 전달합니다. 이것이 지금은 대부분 유니버설 시리얼 버스 (USB) 혹은 블루투스 연결을 통해 이루어지며, 과거에는 PS/2 혹은 ADB 연결에서 통용되던 방법입니다.
USB 키보드의 경우:
- 키보드의 USB 회로 소자는 컴퓨터 USB 호스트 컨트롤러의 핀 1로 제공되는 5V 전원으로 동작합니다.
- 생성된 키 코드는 키보드 내부 회로 소자내의 "endpoint"라고 불리는 레지스터에 저장됩니다.
- 호스트 USB 컨트롤러는 "endpoint"를 매 10ms(키보드에 따라 정의된 최소값) 이내의 시간마다 폴링하며, 이를 통해 저장된 키 코드 값을 얻어냅니다.
- 이 값은 USB SIE (Serial Interface Engine) 으로 넘어가 저수준의 USB 프로토콜에 맞는, 하나 혹은 그 이상의 USB 패킷으로 변환됩니다.
- 패킷들은 D+와 D- 핀 (가운데 있는 둘) 을 이용한 차동 신호를 통해 최대 1.5 Mb/s의 속도로 전송되는데, HID (Human Interface Device) 디바이스는 늘 "저속 장치"로 분류되기 때문입니다 (USB 2.0 compliance).
- 이 직렬 신호는 곧 컴퓨터의 호스트 USB 컨트롤러에서 디코딩되고, 컴퓨터의 HID (Human Interface Device) 유니버설 키보드 디바이스 드라이버에 의해 변환됩니다. 키 값은 이제 운영 체제의 하드웨어 추상화 레이어로 전달됩니다.
가상 키보드의 경우 (터치 스크린 장치 등에 있는):
- 사용자가 현대적인 정전식 터치 스크린에 손가락을 올리면, 작은 양의 전류가 손가락으로 흐릅니다.
이것이 전도층의 정전기를 통해 회로를 완성시키고 스크린 위의 해당 지점에 전압 하강을 유도합니다.
그러면
스크린 컨트롤러
는 키 입력의 좌표를 알리는 인터럽트를 발생시킵니다. - 이제 모바일 운영체제는 현재 키 입력 이벤트의 초점을 자신의 GUI 요소 중 하나(여기서는 가상 키보드 어플리케이션 버튼)에 알립니다.
- 가상 키보드는 이제 소프트웨어 인터럽트를 일으켜 '키 입력' 메시지를 OS에 되돌려줄 수 있습니다.
- 이 인터럽트는 현재 키 입력 이벤트의 초점을 알립니다.
키보드는 인터럽트 요청 라인 (IRQ) 를 통해 신호를 보내는데, 이 라인은 인터럽트 컨트롤러에 의해
인터럽트 벡터
(정수 값) 에 연결되어 있습니다. CPU는 Interrupt Descriptor Table
(IDT) 을 활용해 커널에서 제공된 함수들 (인터럽트 핸들러
) 에 인터럽트 벡터를 연결하구요.
인터럽트가 도착하면, CPU는 IDT와 인터럽트 벡터를 살펴보고 적절한 핸들러를 실행합니다. 이에 따라서,
커널에 진입하게 됩니다.
HID 트랜스포트는 키 눌림 이벤트를 HID가 사용하는 형태의 스캔코드로 변환하는 KBDHID.sys
드라이버에 전달합니다. 이 경우에 스캔코드는 VK_RETURN
(0x0D
)가 되죠.
KBDHID.sys
드라이버는 KBDCLASS.sys
(키보드 클래스 드라이버) 와 접속합니다.
이 드라이버는 모든 키보드와 키패드 입력의 안전한 처리를 담당합니다. 그리고는 (설치된 서드파티
키보드 필터로 메시지를 전달한 후에) Win32K.sys
를 호출합니다. 이 모든 일은
커널 모드에서 일어나죠.
Win32K.sys
는 어떤 창이 활성화 돼 있는지를 GetForegroundWindow()
API를 통해
알아냅니다. 이 API는 브라우저 주소창의 윈도우 핸들을 제공하겠네요. Windows의 "message pump"는
곧, SendMessage(hWnd, WM_KEYDOWN, VK_RETURN, lParam)
을 호출합니다.
lParam
은 키눌림의 더 자세한 정보를 가리키는 비트마스크입니다: 반복 횟수(여기선 0),
진짜 스캔 코드 (OEM 별로 상이하지만, 보통은 VK_RETURN
), 특수키(alt, shift, ctrl 같은)가
함께 눌렸는지 (여기선 안 눌렸죠), 그리고 몇 가지 다른 상태에 대한 정보가 담겨있어요.
Windows의 SendMessage
API는 특정한 창 핸들 (hWnd
) 의 큐에 메시지를 추가하는 간단한
함수입니다. 그리고나서, hWnd
에 할당된 (WindowProc
이라 불리는) 주 메시지 처리 함수가
큐에 있는 메시지들을 처리하기 위해 호출됩니다.
활성화 된 창 (hWnd
) 은 실제로 편집을 제어하며 여기서의 WindowProc
은 WM_KEYDOWN
메시지에 대한 메시지 핸들러를 갖게 됩니다. 이 코드는 SendMessage
로 전달된 세 번째 파라미터
(wParam
) 를 들여다보는데요, 사용자가 엔터키를 쳤다는 걸 알려주는 게 VK_RETURN
이기
때문입니다.
인터럽트 신호는 I/O Kit kext 키보드 드라이버에 인터럽트 이벤트를 발생시킵니다. 이 드라이버는 해당
신호를 OS X의 WindowServer
프로세스에 전달되는 키 코드로 변환합니다. 그 결과로서,
WindowServer
는 어떠한 적절한 곳 (활성화 혹은 리스닝하는 곳과 같은 곳) 에라도 이벤트 큐가
들어있는 Mach의 포트를 통해 이벤트를 보내게 됩니다. 그리고 나면 이벤트는 이 큐에서,
mach_ipc_dispatch
함수를 호출할 수 있는 권한을 가진 스레드에 의해 읽힙니다. 일련의 과정은
NSApplication
메인 이벤트 루프에 의해, NSEventType
의 KeyDown
이라는
NSEvent
를 통해 처리됩니다.
그래픽이 제공되는 X 서버
를 사용할 땐, X
가 일반적인 이벤트 드라이버 evdev
를
키 눌림 확인에 활용합니다. 키코드를 스캔코드로 다시 맵핑하는 것은 X 서버
고유의 키맵과 룰에 따라
이뤄지고요. 키 눌림의 스캔코드 맵핑이 완료되면, X 서버
는 해당 문자를 윈도우 관리자
(DWM, metacity, i3 등등) 에 전달하여, 윈도우 관리자
가 활성화된 창에 문자를 보내게 하죠.
문자를 전달받은 창에서는 그래픽을 표현하는 API가 적절한 폰트 기호를 적절한 선택 영역에 찍어줍니다.
이제 브라우저는 URL (유일 자원 지시자) 을 담고 있는 아래의 정보를 가지고 있어요:
프로토콜
"http"- '하이퍼 텍스트 전송 규약'을 사용하시오
자원
"/"- 메인 (인덱스) 페이지를 가져오시오
프로토콜이나 유효한 도메인 이름이 주어지지 않으면, 브라우저는 주소창에 놓인 텍스트를 브라우저의 기본 웹 검색엔진에 넘겨줍니다. 많은 경우에 이 URL에는 어떤 브라우저로부터 전달되었는지 검색엔진이 알 수 있게 해주는 특수한 부분 텍스트가 붙습니다.
- 브라우저는 호스트네임에서
a-z
,A-Z
,0-9
,-
, 혹은.
아닌 문자들을 확인합니다. - 지금의 호스트명은
google.com
이기때문에 유니코드가 없지만, 있을 때에는 브라우저가 URL에서 호스트명 부분에 퓨니코드 (Punycode) 인코딩을 하기도 합니다.
- 브라우저는 "미리 불러들인 HSTS (HTTP Strict Transport Security)" 리스트를 점검합니다. 이 리스트는 HTTPS로만 연결되도록 요청한 웹사이트의 목록이죠.
- 웹사이트가 목록에 있다면, 브라우저는 요청을 HTTP 대신 HTTPS로 보내게 됩니다. 그렇지 않다면, 첫 요청은 HTTP로 보내지구요. (웹사이트가 HSTS 목록에 없더라도 여전히 HSTS 정책을 사용할 수 있다는 점을 알아두세요. 사용자의 첫 HTTP 요청에 대한 응답으로 사용자가 반드시 HTTPS 요청을 보내도록 요구한다는 내용을 받게 되는 것이죠. 하지만, 이 단일 HTTP 요청이 잠재적으로 사용자를 다운그레이드 공격 (downgrade attack) 에 취약하도록 할 수도 있고, 이 때문에 HSTS 목록이 현대적인 웹 브라우저에 들어있는 것입니다.)
- 브라우저는 도메인이 캐시에 들어있는지 확인합니다. (크롬에서 DNS 캐시를 보려면, chrome://net-internals/#dns 으로 가보세요).
- 만약 못 찾으면, 브라우저는 검색을 하기 위해 (OS에 따라 상이하지만)
gethostbyname
라이브러리 함수를 호출합니다. gethostbyname
은 DNS를 통한 호스트명 확인을 시도하기 전에, 호스트명이 로컬의 (OS에 따라 위치가 다른) hosts 파일에서 참조될 수 있는지 봅니다.gethostbyname
이 캐시와hosts
파일 모두에서 호스트명을 못 찾으면, 곧 네트워크 스택에서 정의된 DNS 서버에 요청을 보냅니다. 일반적으로 로컬 라우터나 인터넷 공급자의 캐시 DNS 서버로 보내지죠.- 만약 DNS 서버가 같은 서브넷에 존재한다면 이 네트워크 라이브러리는 DNS 서버에 대해
ARP 프로세스
를 거칩니다. - 만약 DNS 서버가 다른 서브넷에 존재한다면, 네트워크 라이브러리는 기본 게이트웨이 IP에 대해
ARP 프로세스
를 거칩니다.
ARP (주소 결정 프로토콜, Address Resolution Protocol) 브로드캐스트를 보내기 위해서는 네트워크 스택 라이브러리가 검색할 목적지 IP의 주소를 알아야 합니다. 또, ARP 브로드캐스트를 보내는 데 사용하는 인터페이스의 MAC 주소 역시 알아야 합니다.
가장 먼저, ARP 캐시가 목적지 IP의 ARP 항목을 가지고 있는지 점검합니다. 만약 캐시에 있다면 라이브러리 함수는 다음의 형태로 결과를 리턴합니다: 목적지 IP = MAC.
항목이 ARP 캐시에 없다면:
- 라우트 테이블을 검색해서 목적지 IP 주소가 로컬 라우트 테이블의 서브넷에 존재하는지 봅니다. 존재한다면, 라이브러리가 그 서브넷에 속하는 인터페이스를 활용합니다. 없다면, 라이브러리는 우리 기본 게이트웨이의 서브넷에 속하는 인터페이스를 활용합니다.
- 선택된 네트워크 인터페이스의 MAC 주소가 검색이 됩니다.
- 네트워크 라이브러리는 레이어 2 (`OSI 모델`_에서 데이터 링크 레이어) 를 통해 ARP 요청을 보냅니다:
ARP Request
:
Sender MAC: interface:mac:address:here Sender IP: interface.ip.goes.here Target MAC: FF:FF:FF:FF:FF:FF (Broadcast) Target IP: target.ip.goes.here
컴퓨터와 라우터 사이에 어떤 하드웨어가 있는지에 따라:
직접 연결시:
- 컴퓨터가 라우터에 직접 연결되어 있으면 라우터는
ARP Reply
를 회신합니다.(아래를 확인하세요)
허브:
- 컴퓨터가 허브에 연결되어 있으면, 허브가 ARP 요청을 모든 포트에 브로드캐스트합니다. 라우터가 동일한
"Wire"에 연결되어 있으면, 허브가
ARP Reply
를 회신하게 되지요.(아래를 확인하세요)
스위치:
- 만약 컴퓨터가 스위치에 연결되어 있다면, 스위치가 자신의 로컬 CAM/MAC 테이블을 확인해 어떤 포트가 지금 찾고자하는 MAC 주소를 가지고 있는지 봅니다. 스위치에 해당 MAC 주소가 없다면 ARP 요청을 모든 포트에 다시 브로드캐스트 하게 되지요.
- 스위치가 MAC/CAM 테이블에서 해당 주소를 찾으면 ARP 요청을 해당 주소의 포트에 보냅니다.
- 라우터가 동일한 "wire"에 있다면, 스위치가
ARP Reply
를 회신합니다.(아래를 확인하세요)
ARP Reply
:
Sender MAC: target:mac:address:here Sender IP: target.ip.goes.here Target MAC: interface:mac:address:here Target IP: interface.ip.goes.here
이제 네트워크 라이브러리는 우리 DNS 서버나 DNS 프로세스를 재개할 수 있는 기본 게이트웨이 중 하나의 IP 주소를 갖고 있습니다:
- DNS 클라이언트는 1023 보다 큰 숫자의 소스 포트를 이용해, UDP 포트 53번에 소켓을 구성합니다.
- 만약 응답 크기가 너무 크다면, TCP가 대신 사용됩니다.
- 로컬/ISP의 DNS 서버가 해당 정보를 갖고 있지 않다면, 재귀적인 탐색이 수행되고 SOA가 도달해서 해답이 되돌아올 때까지 DNS 서버 리스트를 타고 올라갑니다
브라우저가 목적지 서버의 IP 주소를 받으면, 거기서 호스트명과 포트 번호(HTTP 프로토콜에서 기본값 80,
HTTPS에서는 443)를 뽑아내어, socket
이라는 이름의 시스템 라이브러리를 호출하고 TCP 소켓 스트림
- AF_INET/AF_INET6
과 SOCK_STREAM
- 을 요청합니다.
- 이 요청은 먼저 TCP 세그먼트가 제작되는 Transport 레이어로 전달됩니다. 목적지 포트는 헤더에 더해지고, 출발지 포트는 커널의 동적 포트 범위 (리눅스의 ip_local_port_range) 에서 선택됩니다.
- 이 세그먼트는 추가적인 IP 헤더를 덧씌우는 Network 레이어로 보내집니다. 지금의 머신뿐 아니라 목적지 서버의 IP 주소도 담아 패킷을 만들죠.
- 패킷은 곧 Link 레이어에 도착합니다. 머신 NIC의 MAC 주소에 게이트웨이(로컬 라우터)의 MAC 주소까지 포함한 프레임 헤더가 더해지죠. 전과 마찬가지로, 커널이 게이트웨이의 MAC 주소를 모르면, ARP 쿼리를 브로드캐스트 해서 찾아야합니다.
이 지점에서 패킷은 다음 중 하나로 전송될 준비를 마칩니다:
대부분의 집이나 소규모 업체의 인터넷 연결에서 패킷은 컴퓨터로부터, 아마도 로컬 네트워크를 통해, 모뎀 (MOdulator/DEModulator) 으로 보내지고 이를 통해 디지털 신호인 1과 0이, 전화나 케이블, 혹은 무선 통신 연결 등으로 전달되기 적합한 아날로그 신호로 변환됩니다. 그 연결의 반대편에서는 아날로그 신호를 디지털 신호로 되돌려주는 또 다른 모뎀이 다음 네트워크 노드 가 출발지와 도착지를 분석할 수 있도록 해줍니다.
대부분의 큰 사업체나 몇몇 신축 단지에서는 데이터를 다음 네트워크 노드 까지 디지털로 직접 연결해주는 광케이블 및 다이렉트 이더넷 연결이 존재하기도 합니다.
결국, 패킷은 로컬 서브넷을 관리하는 라우터에 도착합니다. 거기서부터, 패킷은 자율 시스템 (AS) 의 보더 라우터까지, 다른 자율 시스템까지, 그리고 결국 목적지 서버까지 여행하게 되죠. 이 때 지나치는 각각의 라우터는 IP 헤더로부터 목적지 주소를 추출해내서 적절한 다음 단계가지 이어줍니다. IP 헤더 내의 Time to live (TTL) 영역은 라우터를 하나씩 지날 때마다 감소됩니다. TTL 영역이 0이 되거나 도달한 라우터의 큐에 (네트워크 혼잡과 같은 이유로) 자리가 없을 때 패킷은 드롭됩니다.
이 송수신 동작은 다음의 TCP 연결 흐름을 따라 여러 차례 일어납니다:
- 클라이언트가 초기 순서 번호 (ISN, Initial Sequence Number) 을 선택하고, ISN을 설정하는 중임을 나타내는 SYN 비트가 set된 한 패킷을 서버로 보냅니다.
- 서버가 SYN을 수신하고 수용가능한 상태인지 확인합니다:
- 서버가 자신의 initial sequence number를 고릅니다
- 서버가 ISN 선택중임을 알리는 SYN 비트를 set합니다
- 서버가 (클라이언트 ISN + 1) 을 ACK 영역에 붙이고 첫 번째 패킷을 확인했다고 알리는 ACK 플래그를 추가합니다
- 클라이언트가 패킷을 하나 보내 연결을 확인해줍니다:
- 자신의 ISN을 하나 올립니다
- 수신자 확인 번호를 하나 올립니다
- ACK 필드를 set합니다.
- 데이터가 다음과 같이 옮겨집니다:
- 한 쪽에서 N개의 데이터 바이트를 보내면서, SEQ를 해당 숫자만큼 증가시킵니다
- 반대편이 그 패킷 (혹은 연결된 여러 패킷) 을 받았다고 알리면, 상대로부터 마지막에 받았던 순서와 같은 ACK 값을 담아 ACK 패킷을 보냅니다
- 연결을 끊을 때:
- 닫는 쪽이 FIN 패킷을 보냅니다
- 반대편이 FIN 패킷을 ACK하고 자신의 FIN을 보냅니다
- 닫는 쪽이 반대편의 FIN을 ACK와 함께 확인하고 알립니다
- 클라이언트 컴퓨터가 자신의 Transport Layer Security (TLS) 버전, 암호 알고리즘 목록 그리고
사용 가능한 압축 방식을
ClientHello
메시지에 담아 서버로 보냅니다. - 서버는 클라이언트에게 TLS 버전, 선택한 암호 알고리즘, 선택한 압축 방식 그리고
CA (Certificate Authority) 가 사인한 서버의 공개 인증서를
ServerHello
메시지에 담아 답장합니다. 이 인증서는 대칭키가 생성되기 전까지 클라이언트가 나머지 handshake 과정을 암호화하는 데에 쓸 공개키를 담고 있죠. - 클라이언트는 서버측 디지털 인증서가 유효한지를, 신뢰할 수 있는 CA 목록을 통해 확인합니다. 만약 CA를 통해 신뢰성이 확보되면, 클라이언트는 의사 난수 (pseudo-random) 바이트를 생성해 서버의 공개키로 암호화하구요. 이 난수 바이트는 대칭키를 정하는 데에 사용됩니다.
- 서버는 난수 바이트를 자기 개인키로 복호화해 대칭 마스터키 생성에 활용합니다.
- 클라이언트는
Finished
메시지를 서버에 보내면서, 지금까지의 교환 내역을 해시한 값을 대칭키로 암호화하여 담습니다. - 서버는 스스로도 해시를 생성해 클라이언트에서 도착한 값과 일치하는지 봅니다. 일치하면, 서버도 마찬가지로
대칭키를 통해 암호화한
Finished
메시지를 클라이언트에 보내죠. - 이제부터 TLS 세션이 대칭키로 암호화된 어플리케이션 (HTTP) 데이터를 전송합니다.
구글이 만든 웹 브라우저라면, 페이지를 가져오기 위해 HTTP 요청을 보내는 대신, 서버에게 HTTP에서 SPDY로 "업그레이드"할 것을 협상해봅니다.
만약 클라이언트가 SPDY를 지원하지 않고 HTTP만 쓴다면, 서버에 다음과 같은 요청을 보내죠:
GET / HTTP/1.1 Host: google.com Connection: close [other headers]
[other headers]
부분은 HTTP 사양에 따라 콜론으로 구분되고 각각 새 줄로 나뉘는 일련의 키-값
쌍을 나타냅니다. (이 부분은 사용된 브라우저가 HTTP 스펙을 벗어나는 어떠한 버그도 없을 때를 가정해요.
웹 브라우저가 HTTP/1.1
을 쓴다는 것도 마찬가지인데, 그렇지 않을 경우엔 Host
헤더가 요청에
포함되지 않고 GET
요청에 명시된 버전이 HTTP/1.0
혹은 HTTP/0.9
일 수도 있습니다. )
HTTP/1.1은 송신자측에서 응답을 받은 직후에 연결이 끊어질 것이라는 신호를 보내기 위해 "close"라는 연결 옵션을 정의합니다. 아래의 예처럼 말이죠.
Connection: close
영구 접속을 허용하지 않는 HTTP/1.1 어플리케이션은 반드시 "close" 연결 옵션을 모든 메시지에 포함해야 합니다.
요청과 헤더를 보낸 후에, 웹 브라우저는 하나의 빈 줄을 서버에 보내 요청 내용이 모두 보내졌음을 알립니다.
서버는 요청의 상태를 나타내는 코드와 다음과 같은 형태의 답신으로 응답하죠:
200 OK [response headers]
빈 줄을 하나 붙인 뒤, www.google.com
의 HTML 본문을 페이로드에 담아 보냅니다. 서버는 곧
연결을 끊거나, 클라이언트가 보낸 헤더에 요청이 있었을 시, 추가적인 요청을 위해 재사용될 수 있도록
연결을 유지해둡니다.
웹 브라우저에서 보낸 HTTP 헤더에, 마지막으로 보냈던 파일이 브라우저에 캐시되어 있고 그 뒤로 변하지
않았다는 판단을 내릴 만큼 충분한 정보 (예를 들어, 웹 브라우저가 ETag
헤더를 포함시켰다든지) 가
담겨 있었다면, 아래와 같이 응답할 수도 있어요:
304 Not Modified [response headers]
페이로드 없이, 대신 브라우저가 자체 캐시에서 HTML 폼을 가져오게 말이죠.
HTML을 파싱한 후에는, 브라우저 (그리고 서버) 가 이 과정을 HTML 페이지에서 참조되는 모든 자원
(이미지, CSS, favicon.ico, 기타 등등) 에 대해 반복합니다. 요청이 GET / HTTP/1.1
대신
GET /$(URL relative to www.google.com) HTTP/1.1
이 된다는 것만 빼고 말입니다.
HTML이 www.google.com
이 아닌 도메인의 자원을 참조할 땐, 브라우저가 다른 도메인을 확정하는
단계로 되돌아가 해당 도메인에 대해 여기까지의 과정들을 밟습니다. 요청에 들어있는 Host
헤더는
google.com
대신 적당한 서버 이름으로 설정되겠죠.
HTTPD (HTTP 데몬) 서버는 서버측에서 요청/응답을 처리하는 친구입니다. 가장 흔한 HTTPD 서버는 리눅스용인 Apache나 nginx 그리고 윈도우용인 IIS가 있죠.
- HTTPD (HTTP 데몬) 은 요청을 받습니다.
- 서버는 요청을 다음의 파라미터들로 쪼개는데:
- HTTP 요청 메소드 (
GET
,HEAD
,POST
,PUT
,PATCH
,DELETE
,CONNECT
,OPTIONS
, 혹은TRACE
중 하나). 주소창에 URL을 직접 입력한 경우에는,GET
이겠구요. - 도메인, 이 경우에는 - google.com.
- 요청된 경로/페이지, 이 경우에는 - / (특정한 경로/페이지가 없었으면, / 가 기본 경로입니다).
- HTTP 요청 메소드 (
- 서버는 google.com에 해당하는 가상 호스트가 서버에 설정되어 있는지 확인합니다.
- 서버는 google.com이 GET 요청을 받아들일 수 있는지 봅니다.
- 서버는 해당 클라이언트에게 이 메소드가 허용되는지 봅니다 (IP, 인증, 기타 등등을 통해서요).
- 서버에 다시쓰기 모듈이 설치돼있으면 (Apache의 mod_rewrite 혹은 IIS의 URL Rewrite같은). 받은 요청을 지정된 규칙 중 하나에 연결시키려 하죠. 연결 규칙이 발견되면, 서버는 그 룰로 요청을 다시쓰기 합니다.
- 서버는 요청에 대응되는 내용을 가져오는데, 우리 케이스에서는 "/"가 메인 파일이기 때문에, 인덱스 파일로 가게 됩니다 (이걸 덮어쓸 때도 있지만, 이게 가장 흔한 방식이에요).
- 서버는 가져온 파일을 핸들러를 통해 분석합니다. 구글이 PHP 위에서 동작한다면, 서버는 인덱스 파일을 해석하는 데 PHP 를 쓸 것이고, 결과물을 클라이언트에게 흘려보내겠죠.
서버가 브라우저에 자원 (HTML, CSS, JS, 이미지, 기타 등등) 을 제공하면 브라우저는 아래 프로세스를 수행합니다:
- 파싱 - HTML, CSS, JS
- 렌더링: DOM 트리 생성 → 트리 렌더링 → 렌더링 된 트리 배치 → 렌더링 된 트리 색칠
브라우저는 당신이 고른 웹 자원을, 서버에 요청하고 브라우저 창에 보여주는 역할을 합니다. 자원은 보통 HTML 파일이지만, PDF나 이미지, 혹은 다른 타입일 수도 있습니다. 자원의 위치는 유저가 명시한 URI (통합 자원 식별자 Uniform Resource Identifier) 로 확인할 수 있구요.
브라우저가 HTML을 해석하고 보여주는 방식은 HTML과 CSS 스펙에 명시돼 있습니다. 이 스펙들은 W3C (World Wide Web Consortium) 기구가 유지하는데, 이 곳이 바로 웹 표준화 기구입니다.
브라우저의 유저 인터페이스들은 서로 유사한 점이 많습니다. 일반적인 유저 인터페이스 구성요소들은:
- URI를 적는 주소창
- 뒤로 그리고 앞으로 버튼
- 즐겨찾기 기능
- 현재 문서를 새로고치거나 멈추는 새로고침과 멈춤 버튼
- 당신의 홈페이지로 갈 수 있는 홈 버튼
브라우저의 High Level Structure
브라우저의 구성요소는: The components of the browsers are:
- 유저 인터페이스: 유저 인터페이스는 주소창, 뒤로/앞으로 버튼, 즐겨찾기 메뉴 등등을 포함합니다. 당신이 요청한 페이지를 보는 창을 제외한 브라우저의 모든 부분이죠.
- 브라우저 엔진: 브라우저 엔진은 UI와 렌더링 엔진 사이에 일어나는 일을 통제합니다.
- 렌더링 엔진: 렌더링 엔진은 요청된 내용을 보여주는 부분을 책임집니다. 예를 들어 만약 요청된 내용이 HTML이면, 렌더링 엔진은 HTML과 CSS를 분석하고, 처리된 내용을 화면에 띄워줍니다.
- 네트워킹: 네트워킹은 HTTP와 같은 네트워크 요청을, 플랫폼별로 다른 구현체를 활용해 플랫폼-독립적인 인터페이스 뒤에서 처리하죠.
- UI 백엔드: UI 백엔드는 콤보박스나 창 같은 기본적인 위젯을 그리는 데 쓰입니다. 이 백엔드는 플랫폼에 구애받지 않는 포괄적인 인터페이스를 노출시킵니다. 내부적으로는 운영 체제의 유저 인터페이스 메소드들을 활용하면서요.
- JavaScript 엔진: JavaScript 엔진은 JavaScript 코드를 분석하고 실행하는 데 활용됩니다.
- 데이터 저장소: 데이터 저장소는 유지가 되는 계층입니다. 브라우저가 쿠키같은 갖가지 종류의 데이터를 저장해둬야 할 수도 있거든요. 브라우저는 또 localStorage와 IndexedDB, WebSQL, 파일시스템과 같은 저장 메커니즘을 지원합니다.
렌더링 엔진은 네트워킹 계층에서 요청한 문서의 내용을 받아오기 시작합니다. 보통 8kB 덩어리로 이뤄지죠.
HTML 파서의 주된 역할은 HTML 마크업을 파스 트리로 분석해내는 겁니다.
이렇게 나온 트리 ("파스 트리 parse tree") 는 DOM 요소와 속성 노드의 트리입니다. DOM은 Document Object Mode의 줄임말이고요. 이 친구는 HTML 문서와 HTML 요소를 JavaScript 같은 외부 요소와 이어주는 인터페이스의 객체 표현 방식입니다. 이 트리의 루트는 "Document" 객체입니다. 스크립트를 통한 모든 조작보다 앞서, DOM은 마크업과 거의 일대일인 관계를 갖습니다.
파싱 알고리즘
HTML은 일반적인 탑-다운이나 바텀-업 방식의 파서로는 분석할 수 없습니다.
그 이유는:
- 관대한 언어적 특성.
- 브라우저는 흔히 알려진, 잘못된 HTML들을 지원하기 위해 전통적으로 에러를 용인해왔다는 사실.
- 파싱 과정은 재진입 가능하다는 것입니다. 다른 언어에서, 소스는 파싱 과정에서 변하지 않지만, HTML에서는, 동적 코드 (예를 들어 document.write() 호출을 담고 있는 스크립트 요소) 가 추가적인 토큰을 추가할 수도 있어서, 파싱 과정이 실제로 입력값을 바꿉니다.
일반적인 파싱 기술을 쓸 수 없으니, 브라우저는 임의의 파서를 활용해 HTML을 파싱합니다. 파싱 알고리즘은 HTML5 스펙에 상세히 서술돼있습니다.
알고리즘은 두 단계를 포함하고 있습니다: 토큰화와 트리 생성이죠.
파싱이 끝난 후의 동작
브라우저가 페이지에 링크돼있는 외부 자원 (CSS, 이미지, JavaScript 파일, 기타 등등) 을 가져오기 시작합니다.
이 단계에서 브라우저는 해당 문서가 상호작용 중이라는 표시를 해두고 "deferred" 모드에 있는 스크립트를 파싱하기 시작합니다: 반드시 문서를 분석한 후에 실행되어야 하는 것들이죠. 문서의 상태는 "complete" 으로 설정되고 "load" 이벤트가 촉발됩니다.
HTML 페이지에 "유효하지 않은 문법"이라는 에러는 절대 없다는 것을 알아두세요. 브라우저가 어떠한 내용이든 고치고 넘어가니까요.
<style>
태그 내용과,style
속성값으로 되어있는 CSS 파일들을 "CSS lexical and syntax grammar" 를 활용해 파싱합니다.- 각각의 CSS 파일은
Stylesheet object
로 파싱되는데, 여기서 각 객체는 selector 및 CSS 문법에 해당하는 객체들과 함께 CSS 규칙들을 담고 있습니다. - CSS 파서는 특정한 파서 생성기가 사용됐을 경우에 탑-다운이나 바텀-업도 가능합니다.
- DOM 노드를 훑고, 각 노드의 CSS 스타일 값을 계산하면서 '프레임 트리'나 '렌더 트리' 만들어요.
- 자식 노드들의 너비를 더해 '프레임 트리' 내 각 노드의 선별된 너비를 거꾸로 계산하고 그 노드의 수평 여백, 경계, 그리고 패딩도 계산합니다.
- 각 노드가 사용 가능한 너비를 자식들에게 할당하면서 위에서 아래로 실제 너비를 계산합니다.
- 문자 래핑을 적용하고 자식 노드의 높이, 그리고 노드의 여백, 경계, 패딩을 더해 각 노드의 높이를 거꾸로 계산합니다.
- 각 노드의 좌표를 위에서 계산된 정보를 통해 뽑아냅니다.
- 더 복잡한 과정은 요소들이
float
이거나,absolutely
혹은relatively
으로 위치해있을 때처럼 다른 복잡한 특성이 쓰일 때 일어납니다. http://dev.w3.org/csswg/css2/ 와 http://www.w3.org/Style/CSS/current-work 에서 더 자세한 정보를 확인하세요. - 레이어를 만들어 페이지 내 어떤 부분이 그룹으로 애니메이션화 될 수 있도록 다시-래스터화 되지 않는지 서술합니다. 각 프레임/렌더 객체는 레이어에 배정됩니다.
- 페이지의 각 레이어를 위해 텍스쳐가 할당됩니다.
- 각 레이어의 프레임/렌더 객체를 가로지르며 해당 레이어의 그리기 명령이 실행됩니다. 이 과정은 CPU에 의해 래스터화 하거나 D2D/SkiaGL을 활용해 GPU에 직접 그리기도 합니다.
- 위의 모든 과정은 최근에 웹 페이지가 렌더링될 때 계산된 값을 재활용 할 수 있어서, 이후의 변화에 대해서는 적은 노력이 듭니다.
- 페이지 레이어는 합성 과정으로 넘어가고 거기에서 크롬 브라우저나 iframe 그리고 애드온과 같은 다른 시각 요소들과 합쳐집니다.
- 마지막 레이어 위치가 계산되고 합성 명령이 Direct3D/OpenGL 등을 통해 발행됩니다. GPU 명령 버퍼는 비동기적 렌더링을 위해 비워지고 프레임은 윈도우 서버로 전송됩니다.
- 렌더링 과정에서 그래픽 처리 연산 레이어는 범용
CPU
나 그래픽 프로세서인GPU
모두 사용 가능합니다. GPU
를 그래픽 렌더링 연산에 쓸 때에는 그래픽 담당 소프트웨어 레이어가 해당 업무를 여러 조각으로 쪼개어,GPU
의 막강한 부동 소수점 연산 병렬처리를 통해 렌더링을 수월하게 만들죠.
렌더링이 끝나면, 특정한 절차 메커니즘 (Google Doodle 애니메이션 같은) 혹은 사용자의 상호작용 (요청을 검색창에 치고 제안을 받는 등) 에 따라 브라우저는 JavaScript 코드를 실행합니다. Flash나 Java가 실행되기도 하는데, 지금 다루는 Google 홈페이지에서는 아닙니다. 스크립트는 추가적인 네트워크 요청을 만들기도 하고, 페이지 자체나 레이아웃을 바꾸기도, 새로운 페이지를 렌더링하고 그려주기도 합니다.