브라우저 내부 살펴보기
Table of contents
브라우저가 HTML을 해석하고 화면에 나타내는 방법은 HTML,CSS 표준에 따르게 되는데, 브라우저에 따라 스펙 정의가 좀 다를 수도 있다.
시작하기 전 용어 설명
컴퓨터나 스마트폰 애플리케이션을 실행할 때 애플리케이션을 구동하는 것이 바로 CPU와 GPU다.
- CPU: 컴퓨터의 두뇌
- GPU: 간단한 작업에만 특화되어 잇지만 여러 GPU 코어가 동시에 작업을 수행할 수 있음. 그래픽 작업 처리를 위해 개발되었다.
- 프로세스: 애플리케이션이 실행하는 프로그램
- 스레드: 프로세스 내부에 있으며 프로세스로 실행되는 프로그램의 일부
브라우저는 프로세스와 스레드를 어떻게 사용할까? 스레드를 많이 사용하는 프로세스 하나만 사용할 수도 있고, 스레드를 조금만 사용하는 프로세스를 여러 개 만들어 IPC로 통신할 수도 있다.
브라우저의 구성요소
- 유저 인터페이스(UI) : 주소바, 뒤로가기, 앞으로가기, 북마크 메뉴 버튼 등등
- 브라우저 엔진: 렌더링 엔진에 작업을 요청하고 다룸
- 렌더링 엔진: 요청된 컨텐츠를 회면에 표시하게 만들어주는 엔진. HTML과 CSS을 파싱하고 화면에 파싱된 콘텐츠를 표현. 파이어폭스는 모질라에서 직접 만든 게코(Gecko) 엔진을 사용하고 사파리와 크롬은 웹킷(Webkit) 엔진을 사용한다.
- 네트워킹: HTTP request와 같은 네트워크 호출을 위해 필요
- UI 백엔드: 콤보박스나 윈도우와 같은 기본 위젯을 화면에 그리는데 필요
- 자바스크립트 해석기: 자바스크립트 코드를 파싱하고 실행하는데 필요
- 데이터 스토리지: persistence layer. 로컬에만 저장한다. (예를 들면 쿠키, 로컬스토리지 등)
여기서 중요한 점은, 크롬 같은 브라우저는 렌더링 엔진의 여러 인스턴스 각 탭마다 하나씩 실행한다는 점이다. 각 탭은 별도의 프로세스에서 실행된다. 가장 간단한 예로 탭마다 렌더러 프로세스를 하나 사용하는 경우를 생각해 보자. 3개의 탭이 열려 있고 각 탭은 독립적인 렌더러 프로세스에 의해 실행된다. 이때 한 탭이 응답하지 않으면 그 탭만 닫고 실행 중인 다른 탭으로 이동할 수 있다. 만약 모든 탭이 하나의 프로세스에서 실행 중이었다면 탭이 하나만 응답하지 않아도 모든 탭이 응답하지 못하게 된다. 그리고 프로세스 개수가 한도에 다다르면 동일한 사이트를 열고 있는 여러 탭을 하나의 프로세스에서 처리한다.
브라우저 아키텍쳐 (Chromium 기준)
- 브라우저 프로세스: 주소 표시줄, 북마크 막대, 뒤로 가기 버튼, 앞으로 가기 버튼 등을 제어한다. 네트워크 요청이나 파일 접근과 같이 눈에 보이지는 않지만 권한이 필요한 부분도 처리한다.
- 렌더러 프로세스: 탭 안에서 웹 사이트가 표시되는 부분의 모든 것을 제어한다. 예전 렌더러 프로세스는 여러 개가 만들어져 각 탭마다 할당되었지만, 지금은 사이트마다 프로세스를 할당한다. 여기서 말하는 렌더러 프로세스는 주요 렌더링 경로(Critical Rendering Path)이다.
- 플러그인 프로세스: 웹 사이트에서 사용하는 플러그인(예: Flash)을 제어한다.
- GPU 프로세스: GPU 작업을 다른 프로세스와 격리해서 처리한다. GPU는 여러 애플리케이션의 요청을 처리하고 같은 화면에 요청받은 내용을 그리기 때문에 GPU 프로세스는 별도 프로세스로 분리되어 있다.
- 기타 프로세스: 확장 프로그램 프로세스, 유틸리티 프로세스 등의 프로세스가 있음.
브라우저 프로세스
- 브라우저 프로세스는 탭 영역 밖에 있는 모든 부분을 제어한다.
- 브라우저의 주소 표시줄에 URL을 입력하면 브라우저가 인터넷에서 데이터를 가져와서 페이지를 표시한다. 간단한 이 동작에서 사용자가 사이트를 요청하고 브라우저가 페이지 렌더링을 준비하는 과정(이 글에서는 이 과정을 ‘내비게이션’이라고 하겠다)에 초점을 맞춰 살펴보겠다.
브라우저 프로세스의 구성요소 (스레드)
다른 것들도 있지만 본문은 세가지만 언급했음
- UI 스레드: 브라우저의 버튼과 입력란을 그림. 렌더러 프로세스를 먼저 찾거나 네트워크 요청과 동시에 렌더러 프로세스를 시작한다.
- 네트워크 스레드: 인터넷에서 데이터를 가져오기 위한 스택을 다룸
- 스토리지 스레드: 파일에 대한 접근을 제어
내비게이션 처리방법
내비게이션 처리를 대강 요약하자면 아래와 같다.
- 입력 처리: 주소 표시줄에 URL 입력. UI 스레드가 검색어인지 쿼리인지 판별.
- 내비게이션 시작: URL 입력하고 엔터키 누르면서 시작. UI 스레드가 네트워크 호출 시작.
- 응답 읽기: 호출하고 나서 응답 본문이 들어오면 그 응답을 읽음
- 렌더러 프로세스 찾기: 응답을 읽고 각 형식에 따라 적당한 렌더러 프로세스를 찾음 (HTML이면 렌더러 프로세스, ZIP이면 다운로드 매니저)
- 내비게이션 실행: 데이터와 렌더러 프로세스가 전부 준비된 상태. 문서 로딩하면서 브라우저 프로세스도 업데이트 (주소 표시줄, 뒤로 가기 버튼 등등)
- 로드 완료: 렌더러 프로세스는 계속 리소스를 로딩하고 페이지 렌더링. 끝내면 브라우저 프로세스로 IPC 메시지를 보냄
여기서 IPC 메시지란 프로세스간 통신(Inter-Process Communication, IPC) 메시지를 의미한다.
1. 입력 처리
사용자가 주소 표시줄에 타이핑을 시작하면 UI 스레드는 먼저 ‘입력되는 내용이 검색어(search query)인지 URL인지’ 확인한다. Chrome에서 주소 표시줄은 검색창이기도 하다. UI 스레드는 입력되는 내용을 파싱해서 검색 엔진으로 이동할지 요청한 사이트로 이동할지 결정해야 한다.
2. 내비게이션 시작
사용자가 Enter 키를 누르면 사이트의 콘텐츠를 가져오기 위해 UI 스레드가 네트워크 호출을 시작한다. 로딩 스피너가 탭의 모서리에 표시되고, 네트워크 스레드는 요청에 대한 DNS Lookup 및 TLS 연결 설정과 같은 적절한 프로토콜을 거쳐 요청을 처리한다. 이때 네트워크 스레드가 HTTP 301과 같은 서버 리디렉션 헤더를 수신할 수도 있다. 그런 경우에는 네트워크 스레드가 UI 스레드와 통신해 서버가 리디렉션을 요청했다는 것을 알린다. 그런 다음 새로운 URL 요청이 시작된다.
3. 응답 읽기
응답 본문인 페이로드가 들어오기 시작하면 네트워크 스레드는 필요에 따라 스트림의 처음 몇 바이트를 확인한다. 페이로드가 어떤 형식의 데이터인지는 응답 헤더의 Content-Type 헤더가 알려 주지만 정보가 없거나 잘못된 정보가 있을 수 있다. 그래서 이때 MIME 스니핑을 실행해 데이터의 실제 형식을 알아낸다. Chromium 소스 코드의 주석에 적힌 것처럼 데이터의 실제 형식을 알아내는 것은 ‘까다로운 작업’(tricky business)이다. 이 주석을 보면 브라우저가 얼마나 다양한 방법으로 Content-Type 헤더와 페이로드를 처리하는지 알 수 있을 것이다.
이 단계는 또한 Safe Browsing의 검사가 실행되는 단계이다. 도메인과 응답 데이터가 악성 사이트로 알려진 사이트와 일치하는 것 같다면 네트워크 스레드는 경고 페이지를 표시하라고 알린다. 이에 더해서 CORB(Cross-Origin Read Blocking) 기능이 서로 다른 사이트(cross-site)의 민감한 데이터가 렌더러 프로세스에서 실행되지 않게 검사한다.
4. 렌더러 프로세스 찾기
모든 검사가 끝나고 브라우저가 요청된 사이트로 이동해야 한다고 네트워크 스레드가 확신하게 되면 네트워크 스레드는 UI 스레드에 데이터가 준비되었음을 알린다. 그러면 UI 스레드는 웹 페이지의 렌더링을 수행할 렌더러 프로세스를 찾는다. 네트워크 요청이 응답을 받기까지 수백 밀리초가 걸릴 수 있기 때문에 이 과정을 더 빨리 진행하기 위한 최적화가 적용되어 있다. 2단계에서 UI 스레드가 네트워크 스레드로 URL 요청을 보낼 때 UI 스레드는 이미 어느 사이트로 이동할지 알고 있다. UI 스레드는 렌더러 프로세스를 먼저 찾거나 네트워크 요청과 동시에 렌더러 프로세스를 시작한다. 이런 방식에서는 모든 것이 예상대로 잘 진행된다면 네트워크 스레드가 데이터를 받을 때 이미 렌더러 프로세스는 준비 상태에 있게 된다. 만약 다른 사이트로 리디렉션이 이루어져 다른 프로세스가 필요하게 되면 미리 준비한 프로세스가 사용되지 않을 수도 있다.
5. 내비게이션 실행
이제 데이터와 렌더러 프로세스가 준비되었으므로 내비게이션을 실행하도록 브라우저 프로세스에서 렌더러 프로세스로 IPC 메시지를 전송한다. 또한 렌더러 프로세스가 HTML 데이터를 계속 수신할 수 있도록 브라우저 프로세스는 데이터 스트림을 전달한다. 렌더러 프로세스에서 내비게이션이 실행되었다는 것을 브라우저 프로세스가 확인하고 나면 내비게이션이 완료되고 문서 로딩 단계가 시작된다.
이 시점에 주소 표시줄이 업데이트되고 보안 표시와 사이트 설정 UI도 새 페이지의 사이트 정보를 반영해 갱신된다. 탭에 대한 세션 기록이 업데이트되어 뒤로 가기 버튼과 앞으로 가기 버튼도 방금 이동한 사이트를 반영해 작동한다. 탭이나 창을 닫은 이후 탭과 세션을 복원할 수 있게 세션 기록이 디스크 드라이브에 저장된다.
6. 로드 완료
내비게이션이 실행되면 렌더러 프로세스는 계속 리소스를 로딩하고 페이지를 렌더링한다. 이 단계에서 일어나는 일은 다음 글에서 자세하게 다루겠다. 렌더러 프로세스가 렌더링을 ‘끝내면’ 브라우저 프로세스로 IPC 메시지를 보낸다(이 시점은 페이지의 모든 프레임에서 onload 이벤트의 실행까지 끝낸 이후이다). 그러면 UI 스레드는 탭에서 로딩 스피너의 작동을 중지한다.
‘끝낸다(finish)’라고 표현한 이유는 클라이언트 사이드의 JavaScript가 여전히 추가적인 리소스를 로드하거나 이후에 새로운 뷰를 렌더링할 수도 있기 때문이다.
추가 1. 다른 사이트로 내비게이션
간단한 내비게이션이 완료되었다. 그런데 사용자가 주소 표시줄에 다른 URL을 다시 입력하면 어떻게 될까? 브라우저 프로세스는 동일한 단계를 거쳐 다른 사이트로 이동을 처리한다. 하지만 그전에 현재 렌더링된 사이트에서 beforeunload
이벤트를 확인해야 한다. beforeunload
이벤트는 탭을 닫거나 이동하려고 할 때 “이 사이트를 떠나시겠습니까?”라는 경고창을 만들 수 있다. JavaScript 코드를 포함해 탭 안의 모든 것은 렌더러 프로세스에 의해 처리되므로 브라우저 프로세스는 새로운 내비게이션 요청이 들어오면 현재 렌더러 프로세스를 확인해야 한다. (내비게이션 요청은 렌더러 프로세스->브라우저 프로세스로 넘어간다)
추가 2. 서비스 워커
- 서비스 워커는 애플리케이션의 코드에 네트워크 프락시를 작성할 수 있는 수단이다.
- 서비스 워커를 통해 웹 개발자는 무엇을 로컬 캐시에 저장할지, 언제 네트워크에서 새 데이터를 가져올지 제어할 수 있다.
- 서비스 워커가 캐시에서 페이지를 로드하도록 설정되었다면 네트워크에서 데이터를 가져오도록 요청할 필요가 없다.
- 기억해야 할 중요한 점은 서비스 워커가 렌더러 프로세스에서 실행되는 JavaScript 코드라는 점이다.
- 서비스 워커가 등록되면 서비스 워커의 범위는 참조로 유지된다. 내비게이션이 발생하면 네트워크 스레드는 도메인을 등록된 서비스 워커의 범위와 비교한다. 해당 URL에 등록된 서비스 워커가 있으면 UI 스레드는 서비스 워커 코드를 실행하기 위해 렌더러 프로세스를 찾는다. 서비스 워커는 네트워크에 데이터를 요청하지 않고 캐시에서 데이터를 가져올 수 있다. 또는 네트워크에 새 리소스를 요청할 수도 있다.
- 웹페이지와는 별개로 작동한다. 예를 들어 푸시 알림, 백그라운드 동기화와 같은 기능이 바로 서비스 워커인데 최신 기술이므로 Browser compatibility 확인하자.. 그리고 더 알고싶으면 문서 읽어보기