본문으로 건너뛰기

웹을 파헤치는 이유와 방법

오늘날의 성능 문제

현대의 웹 페이지나 웹 애플리케이션을 전송하는 일은 결코 간단한 문제가 아니다. 페이지 내 수백 개의 객체, 수천 개의 도메인, 변동이 심한 네트워크, 광범위한 디바이스 기능이 존재하는 환경에서 일관되고 빠른 웹 경험을 만들어 내는 것은 쉬운 일이 아니다.

웹 페이지를 가져와 렌더링하는 데 필요한 여러 단계뿐 아니라 단계마다 내재된 문제를 이해하는 것은 웹 사이트와 상호 작용하는 사용자를 불편하지 않게 하는 데 가장 중요한 부분이다.

웹 페이지의 요청 구조


브라우저는 웹 페이지를 요청할 때 화면에 페이지를 표시하는 데 필요한 모든 정보를 가져오기 위해 반복적인 절차를 거친다.

  1. 가져올 URL을 대기열에 넣는다.
  2. URL 내의 호스트이름의 IP 주소를 조회한다.
  3. 호스트로 TCP 연결을 연다.
  4. 요청이 HTTPS라면, TLS 핸드셰이크를 완료한다.
  5. 기준 페이지 URL에 대한 요청을 전송한다.

다음은 응답을 수신하여 페이지를 렌더링하는 절차를 보여준다.

  1. 응답을 수신한다.
  2. 기준 HTML이라면, HTML을 파싱하여 우선순위에 따라 페이지의 개체들의 반입을 시작한다.
  3. 페이지의 필수 개체를 수신했다면, 화면 렌더링을 시작한다.
  4. 추가 개체를 수신하면, 끝날 때까지 파싱과 렌더링을 계속한다.

앞의 절차는 페이지를 클릭할 때마다 반복되어야 한다. 이 반복적인 절차는 네트워크와 디바이스 자원에 부담을 준다. 이 중 어느 단계든 최적화하거나 제거하는 일이 웹 성능 튜닝에 핵심적인 부분이다.

중요 성능

  • 지연시간: IP 패킷이 한 지점에서 다른 지점으로 이동하는데 걸리는 시간을 말한다. 이와 관련된 것으로 RTT(Round Trip Time)이 있다. RTT는 지연 시간의 2배를 의미한다. 지연 시간은 성능의 주요 병목점이며, 서버까지 많은 왕복이 이루어지는 HTTP와 같은 프로토콜에서 특히 더 그렇다.
  • 대역폭: 두 지점 사이의 연결은 포화 상태가 되기 직전까지의 데이터양만 동시에 처리할 수 있다. 웹 페이지의 데이터 양과 연결의 용량에 따라 대역폭이 성능의 병목점이 될 수 있다.
  • DNS 조회: 클라이언트가 웹 페이지를 가져올 수 있으려면 인터넷의 전화번호부인 DNS를 사용해 호스트이름을 IP 주소로 변환해야 한다. 이 절차는 가져온 HTML 페이지에 있는 모든 고유한 호스트이름에 대해 이루어져야 하며, 호스트이름당 한 번만 하면 된다.
  • 연결 시간: 연결을 수립하려면 클라이언트와 서버 사이에 3방향 핸드셰이크라는 메시지 주고받기가 필요하다. 이 핸드셰이크 시간은 보통 클라이언트와 서버 사이의 지연 시간과 관련이 있다. 핸드셰이크를 위해서는 클라이언트가 서버로 SYN 패킷을 보내고, 서버는 그 SYN에 대한 서버의 ACK와 SYN 패킷을 클라이언트로 전송하며, 클라이언트는 다시 SYN에 대한 ACK을 서버로 전송한다.
  • TLS 협상 시간: 클라이언트가 HTTPS 연결을 하고 있다면, SSL의 후속 프로토콜인 TLS(Transport Layer Security) 협상이 필요하다. 이 때문에 서버와 클라이언트의 처리 시간에 왕복 시간이 더 추가된다.

이 시점에 클라이언트는 아직 요청을 보내지도 않았으며 DNS 왕복 시간과 TCP와 TLS를 위한 추가 시간이 이미 소요되었다. 다음으로 네트워크보다는 서버 자체의 내용이나 성능에 더 의존적인 지표를 살펴보자.

  • TTFB: 클라이언트가 웹 페이지 탐색을 시작한 때무터 기준 페이지 응답의 첫 번째 바이트를 수신한 때까지 걸린 시간을 측정한 것이다. 이것은 서버의 처리 시간뿐만 아니라 앞서 소개한 여러 지표를 합한 값이다. 한 페이지에 여러 개체가 있는 경우, TTFB는 브라우저가 요청을 전송한 시점부터 첫 번째 바이트가 되돌아온 시점까지의 시간을 측정한다.
  • 콘텐츠 다운로드 시간: 이것은 요청한 객체에 대한 TTLB(Time To Last Byte)이다.
  • 렌더링 시작 시간: 클라이언트가 사용자를 위해 얼마나 빨리 화면에 무언가를 표시할 수 있는가? 이것은 사용자가 얼마나 오랫동안 빈 페이지를 바라보았는가를 측정한 것이다.
  • 문서 완성 시간(또는 페이지 로딩 시간): 클라이언트가 페이지 표시를 완료한 시간이다.

HTTP/1의 문제점

HTTP/1.0과 HTTP/1.1의 문제

HOL 블로킹

브라우저는 특정 호스트에서 단 하나의 개체만 가져오려고 하지 않는다. 브라우저는 대개 한번에 많은 개체를 가져오려고 한다. 특정 도메인에 모든 이미지를 넣어둔 웹사이트 하나를 생각해보자. HTTP/1은 그 이미지들을 동시에 요청하는 어떠한 메커니즘도 제공하지 않는다. 단일 연결상에서 브라우저는 요청 하나를 보내고 그 응답을 수신한 후에야 또 다른 요청을 보낸다. h1은 브라우저가 많은 요청을 한 번에 보낼 수 있게 해주는 파이프라이닝이라는 기능이 있지만, 브라우저는 여전히 전송된 순서대로 하나씩 응답을 수신한다. 추가로, 파이프라이닝 기능은 상호 운용성과 배포 측면에서 사용하기 어렵게 하는 여러 문제가 있다.

여러 요청이나 응답 중 어디엔가 문제가 발생하면 그 요청/응답을 뒤따르는 모든 것들은 막혀버린다. 이 현상을 HOL(Head of Line Blocking)이라고 한다. 요즘 브라우저는 특정 호스트에 최대 6개의 연결을 열고 각 연결로 요청을 전송해 어느 정도 병렬 처리가 가능하다. 하지만 각 연결은 여전히 HOL 블로킹의 영향을 받을 수 있다. 게다가 이는 제한된 디바이스 자원을 적절히 사용하는 방법이 아니다.

TCP의 비효율적인 사용

TCP는 보수적인 환경을 가정하고 네트워크상의 다양한 트래픽 용도에 공평하게 동작하도록 설계되었다. TCP의 혼잡 회피 메커니즘은 최악의 네트워크 상태에서 동작하도록 만들어졌고, 경쟁적인 요구가 있는 환경에서 비교적 공평하게 동작한다. 이것이 TCP가 성공적이었던 이유 중 하나로, TCP가 데이터를 전송하는 가장 빠른 방법이라기 보다는 가장 신뢰성 있는 방법이기 때문이다. 여기서 핵심은 congestion window(혼잡 윈도우)라는 개념이다. 혼잡 윈도우는 수신자가 확인(ACK)하기 전까지 송신자가 송신할 수 있는 TCP 패킷의 수를 의미한다. 예를 들어 혼잡 윈도우가 1로 설정되어 있다면, 송신자는 단 하나의 패킷만 전송하며, 그 패킷에 대한 수신자의 확인을 받아야만 또 다른 패킷을 전송할 것이다.

패킷이란 무엇인가?

패킷, 더 구체적으로 IP 패킷은 패킷의 길이, 전송 방법(출발지와 목적지), TCP 통신에 필요한 여러 항목을 정의하고 있는 구조(프레임)로 캡슐화된 bytes의 모음(페이로드)이다. 패킷의 페이로드 하나에 넣을 수 있는 가장 큰 데이터 크기는 1460bytes이다. 14600bytes 크기의 이미지가 있다면 10개의 패킷으로 나누어질 것이다.

한 번에 하나의 패킷만 전송하는 것은 매우 비효율적이다. TCP는 현재 연결에 알맞은 혼잡 윈도우의 크기를 결정하기 위한 slow start이라는 개념이 있다. slow start의 설계 목적은 새로운 연결이 네트워크의 상태를 감지해 이미 혼잡한 네트워크를 약화시키지 않게 하는 것이다. slow start를 통해 송신자는 ACK을 수신할 때마다 패킷 수를 늘려서 전송할 수 있다. 이는 두 개의 패킷이 확인되면 다시 네 개의 패킷을 전송할 수 있음을 의미한다. 이 기하급수적인 증가는 얼마 되지 않아 프로토콜에 정의된 상한선에 도달하며, 그 시점에 이 견결은 이른바 혼잡 회피(congestion avoidance) 단계로 들어갈 것이다.

최적의 혼잡 윈도우 크기를 얻는 데 몇 번의 왕복이 필요하다. 또한 성능 문제를 해결하는 데 그 몇 번의 왕복은 중요한 시간이다. 현대의 운영체제는 보통 4에서 10의 초기 혼잡 윈도우 크기를 사용한다. 패킷의 최대 크기인 약 1460bytes라면, 송신자는 5840bytes만 전송후 ACK을 기다려야 한다. 요즘의 웹 페이지는 HTML과 모든 종속 개체를 포함해 평균 약 2MB의 데이터가 있다. 이상적인 환경에서 운이 따라 준다면, 이는 페이지를 전송하는데 약 9번의 왕복 시간이 소요될 것임을 의미한다.

h1은 다중화를 지원하지 않기 때문에, 브라우저는 특정 호스트에 보통 6개의 연결을 연다. 이는 혼잡 윈도우 널뛰기가 동시에 6번 일어나야 함을 의미한다. TCP는 이 연결들이 함께 잘 동작하게는 해주지만 이들이 최적의 성능을 발휘하도록 보장하지는 못한다.

비대한 메시지 헤더

h1은 요청된 객체를 압축하는 메커니즘을 제공하긴 하지만 메시지 헤더를 압축하는 방법은 없다. 실제로 헤더는 크기가 증가할 수 있다. 응답 패킷에서는 개체 크기 대비 헤더 크기의 비중이 매우 낮지만, 요청 패킷에서는 헤더가 대부분의 바이트를 차지한다. 쿠키가 있는 경우, 요청 헤더의 합이 수 킬로바이트 크기로 커지는 것은 이상한 일이 아니다.

헤더 압축 기능이 없기 때문에, 클라이언트가 대역폭 제한에 걸릴 수도 있다. 이는 저대역폭 또는 과밀 링크에서 특히 더 그렇다. 대표적인 예가 바로 경기장 효과(stadium effect)이다. 수만 명의 사람들이 동시에 같은 장소에 있을 때, 모바일 통신 대역폭은 빠르게 소진된다. 헤더를 압축해 요청 크기를 줄이면 이와 같은 상황에서 도움이 되며 시스템 부하도 전반적으로 줄어들 것이다.

제한적인 우선순위

브라우저가 하나의 호스트에 다수의 소켓(이 소켓들은 각각 HOL 블로킹을 겪는다.)을 열고 개체를 요청하기 시작할 때, 그 요청들의 우선순위를 지정하기 위한 옵션은 매우 제한적인데, 그것은 바로 요청을 보내거나 보내지 않거나 둘 중 하나이다. 페이지에서 어떤 개체는 다른 개체보다 훨씬 더 중요하다. 브라우저는 높은 우선순위의 개체를 먼저 가져오려고 다른 개체에 대한 요청을 보류하는데, 그러는 동안 우선순위가 낮은 개체들은 줄줄이 대기 상태로 빠지게 된다. 이로 인해 브라우저가 높은 우선순위 항목을 기다리는 동안 서버는 낮은 우선순위 항목을 처리할 기회를 얻지 못해 페이지 다운로드 시간이 전체적으로 길어질 수 있다. 또는 브라우저가 페이지를 처리하는 방식 때문에, 브라우저가 높은 우선순위 개체를 발견했어도 이미 반입된 낮은 우선순위 항목 뒤에서 높은 우선순위 개체가 막혀버리는 경우도 있다.

서드파티 개체

현대의 웹 페이지에서 요청되는 것 중에는 웹 서버의 제어 범위에서 완전히 벗어나 있는 것들이 많으며, 이를 서드파티 개체라고 한다. 서드파티 개체의 탐색과 처리는 보통 요즘의 웹 페이지를 불러오는 데 소요되는 시간의 절반을 차지한다. 서드파티 개체가 페이지 성능에 미치는 영향을 최소화하려는 기법이 많이 있다. 하지만 웹 개발자가 직접 제어할 수 있는 범위를 벗어난 콘텐츠가 많은 경우, 그 개체들 중 일부는 성능을 저하시키고 페이지 렌더링을 지연 또는 중단시킬 가능성이 있다. 웹 성능에 관한 어떠한 논의도 이 문제를 언급하지 않고는 끝나지 않을 것이다.

웹 성능 기법


웹 페이지 대부분의 경우, 브라우저 시간의 대부분은 호스팅 인프라에서 가져온 초기 콘텐츠(보통 HTML)을 표시하기 보다는, 모든 콘텐츠를 가져와 클라이언트에서 페이지를 렌더링하는데 소요된다.

그 결과, 웹 개발자들은 클라이언트의 네트워크 지연을 줄이고 페이지 렌더링 시간을 최적화하는 방식으로 성능을 개선하는 데 점점 더 많은 관심을 기울이고 있다.

웹 성능 모범 사례

DNS 조회를 최적화하라

DNS 조회는 호스트와 연결이 수립되기 전에 이루어져야 하므로, 이 조회 절차는 가능한 빨라야 한다.

  • 고유한 도메인/호스트이름의 수를 제한하라.
  • 조회 지연 시간을 줄여라. DNS를 제공하는 인프라의 토폴로지를 이해하고 모든 최종 사용자의 위치에서 정기적으로 조회 소요 시간을 측정하라.
  • 초기 HTML이나 응답에 대해 DNS 프리페치를 활용하라. 이는 초기 HTML을 내려받아 처리하는 동안 그 페이지에 있는 특정 호스트이름들의 DNS 조회를 시작할 것이다.
<link rel="dns-prefetch" href="//ajax.googleapis.com">

이 기법들은 DNS의 고정적인 오버헤드를 최소화하는데 도움을 줄 것이다.

TCP 연결을 최적화하라

  • preconnect를 활용하라. 필요하기 전에 미리 연결을 수립해둠으로써 waterfall critical path에서