웹에 날개를 달아주는 웹 성능 최적화 기법
- 본 책을 읽고 책의 내용을 간략하게 정리한 글입니다.
Chapter 2. 웹 최적화
웹 최적화란
- 웹 최적화란 웹 성능을 구현하기 위해 최고의 조건을 만드는 다양한 노력을 의미
프론트엔드 최적화
- 웹 UI/UX와 관련된 최적화
- HTML, CSS, 자바스크립트, 이미지 파일, 타사 파일 또는 이들과 어우러져 있는 컨텐츠를 만들 때 최적화 진행
- 프론트엔드 최적화가 잘 되어 있는 웹 사이트는 브라우저에서 컨텐츠를 다운로드, 로딩, 렌더링할 때 속도가 빨라지는 효과가 있음
- 프론트엔드 최적화를 하는 대표적인 기술
- 스크립트를 병합하여 브라우저의 호출 개수를 줄임
- 스크립트 크기를 최소화하여 byte 자체를 줄임
- 스크립트를 gzip 등으로 압축하여 전달
- WebP 등으로 브라우저 이미지 형식 최적화
- 이미지 손실, 무손실 압축
- Cache-Control 응답 헤더를 통해 브라우저 캐시 사용
- 도메인 수를 줄여 DNS 조회 최소화
- DNS 정보 미리 읽어오기
- CSS를 HTML 상단에, 자바스크립트를 HTML의 하단에 위치
- 페이지 미리 읽어오기 (page prefetching)
- 타사 스크립트가 웹 성능을 방해하지 않도록 조정
백엔드 최적화
- 웹 UI를 로직에 맞게 만드는 최적화
- 웹 서버, 웹 애플리케이션 서버, 데이터베이스, 로드 밸런싱, DNS 서버 등이 대표적인 백엔드
- 백엔드 최적화는 프론트엔드에 비해 가시적인 효과가 크지 않지만 네트워크를 정상적으로 사용하고 컨텐츠를 전달하기 위해 반드시 필요한 요소
- 백엔드 최적화를 하는 대표적인 방법
- DNS 응답이 빨라지도록 서버 증설
- DNS 응답을 빠르게 할 수 있도록 DNS 정보를 최대한 캐싱
- 웹 서버가 있는 데이터 센터의 네트워크 출력 (throughput) / 대역폭 (bandwidth) 증설
- 웹 서버, 웹 애플리케이션 서버의 CPU/RAM 증설
- 프록시 서버를 설정하여 웹 컨텐츠 캐싱
- CDN(Content Delivery Network) 사용하여 인터넷상에 컨텐츠 캐싱
- 데이터베이스 정규화로 디스크 I/O 최적화
- 데이터베이스 캐싱으로 응답 빠르게
- 로드 밸런싱을 통해 가장 성능이 좋은 웹 서버로 요청 연결
- 웹 애플리케이션 로직을 가볍고 빠르게 개발
프로토콜 최적화
- 웹 컨텐츠를 전달하는 HTTP/HTTPS 프로토콜 자체의 효과를 극대화하면 웹 서버가 클라이언트에게 컨텐츠를 최대 속도와 최저 지연 시간으로 전달 가능
- 웹 컨텐츠를 더 빠르게 요청하고 응답하도록 프로토콜을 업그레이드하는 과정
TCP/IP 프로토콜
- 웹에서는 TCP/IP 프로토콜의 일종인 HTTP를 사용
- OSI 7 계층 모델에서 TCP는 4번째인 전송 계층에 속하고 HTTP는 7번째인 응용 계층에 속함
- TCP 네트워크를 사용하는 네트워크에 있어 대표적인 성능 지표는 대역폭과 지연 시간
- 대역폭 : 특정 시간 동안 얼마나 많은 네트워크 트래픽을 보낼 수 있는지 (시간당 전송량)
- 지연 시간 : 클라이언트와 서버 간 컨텐츠를 전달하는 물리적인 시간
- Round Trip Time (RTT) : 서버와 클라이언트 두 호스트를 모두 왕복하는 데 걸리는 지연 시간
- 인터넷상의 게임이나 화상 채팅 등에 대한 품질에 영향을 주는 값
TCP 혼잡 제어
- TCP 네트워크의 통신량을 조절하여 TCP 네트워크가 혼잡해지지 않도록 하는 방식
- 패킷을 보내는 쪽에서 네트워크에서 수용할 수 있는 양을 파악하고 그만큼의 패킷만 보내는 약속으로 TCP 혼잡 해결
- 호스트가 네트워크의 상태를 시시각각 파악하고 전송 속도를 조절하는 것 또한 혼잡 제어 기능 중 하나
- 대표적인 혼잡 제어의 기술들
- 느린 시작
- TCP 연결이 시작되면 전송 가능한 버퍼의 양인 혼잡 윈도우 (CWND) 의 초기값을 작게 설정하여 전송
- 패킷이 정상적으로 도착할 떄마다 더 많은 패킷을 보내고, 이를 패킷 유실이 발생하기 전까지 반복하는 방식
- 초기에는 적은 패킷을 보내면서 곱셈 방식으로 전송 패킷의 크기를 빠르게 늘리는 방법
- 빠른 재전송
- 먼저 도착해야 하는 패킷이 도착하지 않고 다음 패킷이 도착한 경우에도 수신자가 일단 ACK 패킷을 보내는 방식
- 중간에 패킷이 하나 손실되면 송신자는 중복된 ACK 패킷을 통해 이를 감지하고 정상적으로 보내지 않은 패킷을 재전송
- 중복된 패킷을 3개 받으면 반드시 손실된 패킷 재전송
- 흐름 제어
- TCP 송신자가 데이터를 너무 빠르게 혹은 너무 많이 전송하여 수신자의 버퍼가 오버플로우되는 현상을 방지하는 기술
- 송신자가 데이터를 전송하는 속도를 애플리케이션 프로세스를 읽는 속도와 유사한 수준으로 만들어 트래픽 수신 속도를 송신 속도와 일치시키는 기술
- 느린 시작
HTTP 프로토콜
HTTP 최적화 기술
- HTTP/0.9 버전까지 클라이언트와 서버의 인터넷 통신 정상화, 가용성, 신뢰성 등 기능에 초점을 두었다면 HTTP/1.0 버전부터는 클라이언트와 서버 사이 요청과 응답을 빠르게 할 수 있는 연구가 진행됨
- 멀티호스트 환경으로 웹 환경이 변하면서 HTTP/1.1 버전부터 멀티호스트 기능과 클라이언트와 서버 사이에 TCP/IP 연결을 재사용하는 기능 추가
- HTTP/1.1 버전부터 적용된 연결 재사용 (persistent connection), 파이프라이닝 기법 (pipelining)이 연결 기반의 HTTP 최적화 기술
HTTP 지속적 연결
- HTTP 초기 버전에서는 통신을 통해 여러 오브젝트를 요청하고 응답하려면 3-way handshake 방식으로 TCP 연결을 맺음
- 웹 사이트에 멀티미디어 컨텐츠가 늘어나면서 TCP 연결 재사용 (keep-alive)이 필요하게 되어 등장한 것이 지속적 연결 기술
- HTTP 지속적 연결은 클라이언트와 서버가 TCP 상에서 한 번 연결되면 둘 사이의 연결이 완전히 끊어지기 전까지 맺어진 연결을 지속적으로 재사용하는 기술
- HTTP/1.0 기반에서 지속적 연결 사용을 원하는 클라이언트가 해당 기능을 지원하는 웹 서버에 HTTP 요청 헤더를 이용하여 지속적인 연결을 요청하기 시작
Connection: keep-alive
- HTTP/1.1 버전에서는 Connection 헤더를 사용하지 않아도 모든 요청과 응답이 HTTP 지속적 연결을 기본으로 지원
- 필요 없는 경우에는
Connection: close
헤더 요청을 통해 지속적 연결을 사용하지 않겠다고 전달 가능
- 필요 없는 경우에는
- 지속적 연결 사용 시 장점
- 단일 시간 동안 TCP 연결의 수를 줄여 서버의 CPU나 메모리 자원을 절약하고 네트워크 혼잡이나 지연을 줄임
- 복수 개의 HTTP 요청과 응답을 병렬로 동시에 처리하기 위한 HTTP 파이프라이닝 기술을 사용하려면 꼭 지원해야 함
- 지속적 연결 사용 시 단점
- 서버에 연결된 모든 클라이언트의 TCP 연결이 계속 늘어나면 자원이 고갈되어 더 많은 클라이언트가 접속할 때 대처 못할 상황 발생 가능
- HTTP/2 버전은 단일 TCP 연결을 통해 클라이언트와 서버 사이 응답 지연 없이 스트림 형태로 다수의 HTTP 요청과 응답을 주고받을 수 있는 멀티플렉싱 기술을 토대로 만들어짐
- HTTP/2 를 사용하면 지속적 연결을 고민할 필요가 없음
HTTP 파이프라이닝
- 기존에는 HTTP 요청과 응답이 여럿일 때 하나의 응답이 지연되면 나머지 요청과 응답이 모두 지연될 수 밖에 없는 구조
- HTTP 파이프라이닝은 먼저 보낸 요청의 응답이 없어도 다음 요청을 병렬적으로 전송하는 기술
- 중간에서 응답 지연이 발생하더라도 클라이언트는 먼저 서버 측의 응답을 받을 수 있어 전체적으로 빠른 웹 로딩이 구현되는 구조
DNS
- DNS 질의와 응답 성능이 나쁘면 웹 사이트 로딩에 영향을 줌
- 관리자는 자신이 운영하는 웹 사이트 호스트명의 DNS 질의 속도를 파악하고 개선해야 함
DNS의 작동 원리
- 로컬 DNS 서버로 질의
- 로컬 DNS : 사용자와 인접한 DNS
- 사용자가 PC 등에 수동으로 설정한 DNS의 IP일 수도 있고, PC가 DHCP 설정을 통해 사용하는 ISP의 인근 서버일 수도 있음
- 루트 DNS 서버로 질의
- 소유하지 않은 도메인 정보에 대한 질의를 받으면 로컬 DNS는 전체 도메인을 관장하는 루트 DNS에 www.example.com 도메인을 질의
- .com DNS 서버로 질의
- 로컬 DNS는 .com 도메인을 관장하는 .com DNS에 www.example.com 도메인을 질의
- example.com DNS 서버로 질의
- 로컬 DNS는 www.example.com 도메인을 관장하는 example.com DNS에 질의
- 위와 같이 계층형으로 나누어진 역할에 따라 순차적인 DNS 질의를 반복하여 값을 받아오는 프로세스 과정을 반복적 질의 (iterative query) 라고 함
- 로컬 DNS 서버들은 사용자가 이용 중인 ISP 업체나 DNS 전문 서비스 업체 등이 관리
- 루트 DNS 서버는 ICANN (Internet Corporation for Assigned Names and Number) 기관에서 관리
- 웹 서비스 운영 업체는 example.com 서버부터 관여해 DNS 전문 업체의 서비스를 받거나 분산된 DNS 서버를 직접 운영하는 방식으로 DNS 성능을 향상할 수 있음
사용 주인 다양한 도메인 확인 방법
- 최근 웹 사이트는 자신의 웹 서비스 컨텐츠 뿐 아니라 다른 웹 서비스의 다양한 컨텐츠를 호출하여 사용
- 브라우저의 개발자 도구 → Source 항목을 통해 하나의 웹 페이지에서 어떤 도메인들이 사용되고 있는지 파악할 수 있음
- 특정 업체의 서비스에서 문제가 발생하지 않도록 지속적으로 모니터링해야 함
- 사용 중인 특정 모듈 서비스 업체의 DNS 조회가 불가능하거나 느리다면 해당 모듈을 다운로드해 자신의 웹 서버에 업로드하여 사용하는 방법 등도 고려해야 함
- 자주 업데이트 여부를 확인하고 적용해야 한다는 단점
웹 성능을 최적화하는 도메인 운용 방법
- 직접 개발한 내부 서비스에 도메인 분할을 하고 싶다면 상위 도메인 (top level domain) 을 동일하게 해 DNS 질의를 최대한 적게 만드는 것을 권장
- 공통된 상위 도메인을 사용하는 서비스들은 도메인 질의를 담당하는 네임 서버에 캐싱된 정보를 재사용할 수 있음
- 동일한 상위 도메인을 사용하면 HTTPS 사용을 위한 SSL 인증서를 와일드카드 형식으로 하나만 생성해도 모든 도메인에 사용할 수 있음
- HTML의 DNS 프리패치 (prefetch) 기능을 사용하면 웹 페이지에 사용된 도메인들의 DNS를 조회하는 시간이 좀 더 빨라짐
- 프리패치는 하나의 웹 페이지에 다수의 도메인 호스트명이 섞여 있을 때 웹 문서 페이지를 여는 시점에 멀티스레드 방식으로 미리 DNS를 조회해 빠르게 IP 주소를 불러오는 기술
<link rel="dns-prefetch" href="//img.xxx.com">
브라우저
브라우저의 역사와 특징
- 초기 브라우저는 HTTP와 DNS 기술을 접목해 DNS로 사용자가 입력한 http:// 주소에서 접속할 웹 서버의 IP를 찾고 HTTP로 웹 서버에 접속해 컨텐츠를 가져오는 단순한 기능을 수행했음
- 현재 사용되는 최신 버전의 브라우저는 HTML, CSS, AJAX 등의 기술들을 대부분 지원함
- HTTP가 빠르게 웹 컨텐츠를 전달해도 이를 사용자에게 제공하는 브라우저가 빠르게 작동하지 않으면 웹 성능은 느려질 수밖에 없음
- 대체로 웹 성능 최종 테스트나 디버깅 작업은 브라우저를 통해 수행
내비게이션 타이밍 API
- 네비게이션 타이밍 API는 웹 사이트의 성능을 측정하는 데 사용할 수 있는 데이터를 제공
- 더 유용하고 정확한 종단 (end-to-end) 간 대기 시간 (latency) 정보 제공
- API는 window.performance 개체의 속성으로 사용 가능
네비게이션 타이밍 속성
- performance.timing 이벤트 속성은 페이지 로딩 흐름 순서에 따라 다음과 같이 정의
속성 | 이벤트 | 내용 |
---|---|---|
Prompt for unload | navigationStart | 이전 페이지가 unload 시작한 시점, 즉 이전 페이지의 모든 작업이 끝나고 새로운 페이지를 로드할 준비가 완료된 시점을 의미 이전 페이지가 없다면 fetchStart와 같은 값이 됨 |
unload | unloadStart | 이전 페이지와 현재 페이지가 동일 근원일 때 unload 이벤트가 시작하는 바로 직전 시점 이벤트가 없거나 동일 근원이 아닐 경우 값은 0 |
unloadEnd | 이전 페이지와 현재 페이지가 동일 근원일 때 unload 이벤트를 마친 바로 다음 시점 이벤트가 없거나 동일 근원이 아닐 경우 값은 0 |
|
redirect | redirectStart | HTTP에서 페이지 재전송이 수행될 때의 시작 시점 페이지 재전송이 없다면 0 |
redirectEnd | HTTP에서 페이지 재전송이 수행될 때의 마지막 페이지 재전송 응답의 마지막 바이트를 받은 시점 페이지 재전송이 없다면 0 |
|
App cache | fetchStart | HTTP 요청으로 새로운 리소스를 불러오기 시작하는 시점 HTTP를 요청하는 애플리케이션이 캐시를 지원한다면 캐시의 존재를 확인하는 시점 캐시가 없다면 HTTP 응답으로 리소스를 받아오기 시작하는 시점 |
DNS | domainLookupStart | 웹 사이트 도메인 DNS로 IP 검색을 시작하는 시점 해당 도메인에 연결된 TCP 세션이 남아 있어 도메인 조회가 필요 없거나 DNS 정보가 브라우저나 운영 체제에 캐싱되어 있거나 IP 주소로 접근하는 등 실제 DNS 조회가 발생하지 않으면 fetchStart와 동일한 값 |
domainLookupEnd | DNS 조회 종료 시점으로 DNS 조회가 발생하지 않았다면 fetchStart와 동일한 값 | |
TCP | connectStart | 브라우저가 웹 페이지 문서를 받기 위해 서버와 연결을 시도하는 시점 이미 서버와 연결되어 있거나 원하는 컨텐츠가 캐싱되어 있거나 로컬 리소스를 이용하여 TCP 연결이 필요 없는 경우라면 domainLookupEnd와 동일한 값 |
SecureConnectionStart | HTTPS로 접속할 때 브라우저가 웹 페이지 문서를 받기 위해 서버와 연결을 시도하는 시점 connectStart와 마찬가지로 서버와 이미 연결된 상태 등 별도 연결이 필요 없는 경우라면 직전 단계인 domainLookupEnd와 동일한 값 HTTPS를 사용하지 않는다면 0 |
|
connectEnd | 브라우저가 웹 페이지 문서를 받으려 연결했던 서버와 연결을 끊는 시점 이미 서버와 연결되어 있거나 원하는 컨텐츠가 캐싱되어 있거나 로컬 리소스를 이용하여 TCP 연결이 필요 없는 경우라면 domainLookupEnd와 동일한 값 |
|
Request | requestStart | 브라우저가 접속한 서버 또는 애플리케이션 캐시 시스템에 문서를 요청한 시작 시간 만약 첫 번째 요청이 실패하여 재요청을 하는 경우 새 요청 바로 직전 시간 |
Response | responseStart | 브라우저가 서버 또는 캐시 시스템으로부터 응답 데이터의 첫 번째 바이트를 받은 시간 |
responseEnd | 브라우저가 서버 또는 캐시 시스템으로부터 응답 데이터의 마지막 바이트를 받은 시간 | |
Processing | domLoading | 브라우저가 웹 페이지 문서를 만들기 시작하는 시점 |
domInteractive | 브라우저가 웹 페이지 문서 준비 상태를 interactive로 변경하는 시점 | |
domContentLoaded | 웹 페이지 문서에서 DOMContentLoaded 이벤트가 호출되는 시점 | |
domComplete | 웹 페이지 문서가 준비 상태를 complete 상태로 변경하는 시점 | |
onLoad | loadEventStart | 웹 페이지의 load 이벤트가 발생하는 시점 이벤트가 발생하지 않았다면 0 |
loadEventEnd | 웹 페이지의 load 이벤트가 완료된 시점 |
- navigation은 사용자가 어떻게 페이지를 탐색하는가를 조사하는 반면, timing 속성은 탐색과 페이지 로드 이벤트에 대한 데이터를 가지고 있음
내비게이션 타이밍 속성값 구하기
- window.performance.navigation 객체는 페이지 재전송 속성, 앞뒤 이동 버튼이나 URL이 어떤 페이지 로딩을 발생시키는지 (trigger) 확인하는 속성 저장
- window.performance.navigation.redirectCount : 페이지 내에서 재전송이 몇 번 발생했는지
- window.performance.navigation.type : 사용자가 해당 웹 페이지에 어떻게 접속했는지에 관한 정보
- 아래 테이블은 window.performance.navigation.type 속성 중 대표적인 4개 방식
상수 | 값 | 설명 |
---|---|---|
TYPE_NAVIGATE | 0 | 링크, 북마크, 폼 전송, URL 브라우저 타이핑 등의 방식으로 페이지 접속 |
TYPE_RELOAD | 1 | 브라우저의 새로고침 버튼을 통해 페이지 접속 |
TYPE_BACK_FORWARD | 2 | 뒤로가기 버튼을 통해 페이지 접속 |
TYPE_RESERVED | 255 | 그 외 방법으로 페이지 접속 |
- navigationStart 속성으로 사용자가 느끼는 페이지 로딩 시간 값을 구하는 예제 코드
<script>
function onLoad() {
let now = new Date().getTime();
let pageLoadTime = now - performance.timing.navigationStart;
console.log("User-perceived page loading time : " + pageLoadTime);
}
</script>
- 위 코드 실행 후 브라우저 개발자 도구 → Console 탭을 열면 페이지 로딩에 소요된 시간을 밀리초 단위로 확인 가능
- 내비게이션 타이밍 API에 포함된 각 속성값을 사용하면 다양한 성능 지표 얻기 가능
- 테스트에 사용한 클라이언트 기기 종류나 네트워크 종류에 상관 없이 connectTime 값이 항상 크다면 웹 서버를 좀 더 빠르게 네트워크에 연결할 수 있는 방법을 찾아야 함
웹에 날개를 달아주는 웹 성능 최적화 기법, 강상진, 윤호성 저, 루비페이퍼 출판