웹 사이트 최적화
제이콥 닐슨의 웹 사이트인 “useit.com”은 웹 사이트의 반응 시간을 다음과 같이 평가한다.
- 0.1초: 사용자의 동작에 해당 기능이 바로 반응한다고 느끼는 시간
- 1초: 불필요하게 오래 기다리지 않았다고 느끼는 시간. 0.2~1초의 시간은 컴퓨터가 동작하는 시간으로 이해할 수 있는 시간이다. 시간이 1초 이상 걸리면 컴퓨터의 동작에 이상이 생겼다고 생각하게 된다.
- 10초: 사용자가 집중력을 잃지 않는 최대 시간
이 평가에 따르면 웹 페이지의 반응 시간이 1초 이내여야 사용자 경험에 긍정적인 영향을 미친다. 웹 사이트의 기능이 점점 복잡해지고 많아지면서 느린 반응 속도 때문에 이용 중인 서비스를 떠나 다른 유사한 서비스를 이용하게 만드는 사이트가 있는 반면, 빠른 반응 속도로 웹 사이트를 탐색하는데 큰 불편함이 없는 서비스를 제공하는 사이트도 있다.
많은 포털과 서비스 업체에서는 이러한 사용자의 기대에 부응하기 위해 여러 가지 방법을 시도했다. 그러던 중에 웹 페이지 성능 최적화라는 개념이 생겨났다. 웹 서비스의 반응 속도를 개선할 수 있는 가장 효과적인 방법은 네트워크 속도의 개선이지만 이는 단기간에 이루기 힘든 과제이며 인터넷 서비스를 제공하는 업체에서는 불가능한 부분이다. 대신 반대로 네트워크 사용량을 줄이고 브라우저에서 웹 페이지의 실행 속도를 높여 웹 페이지의 반응 속도를 개선하고 있다.
브라우저 동작 방식을 기반으로 한 최적화
웹 페이지 최적화는 브라우저가 네트워크와 통신하는 과정은 물론 브라우저에서 페이지를 표시하는 과정에서의 응답 속도를 개선하는 것이다. 브라우저가 어떻게 동작하는지 이해해야 올바른 개선 방향을 잡을 수 있다.
사용자는 원하는 웹 사이트로 이동하기 위해 다음의 작업을 수행하고 브라우저의 동작 방식이다.
- 브라우저의 주소창에 주소를 입력하고 Enter 키를 누르거나 링크를 클릭한다.
- 브라우저는 서버에 요청을 보내 페이지를 구성하는 자원을 다운로드한다.
- 다운로드 받은 자원을 이용해 요청 결과를 화면에 보여준다.
이때 브라우저가 어떤 단계로 동작하는지, 단계별로 얼마나 시간이 걸리는지 정의한 연구 활동이 W3C의 네비게이션 타이밍(Navigation Timing) 명세다. 네비게이션 타이밍 명세의 가장 큰 목적은 정확한 단계별 소요 시간을 측정하는 것이지만 브라우저의 처리 단계를 정확하게 정의했다는 데도 큰 의미가 있다.
네비게이션 타이밍 명세에서 브라우저가 사용자의 요청을 처리하는 순서를 정리한 프로세싱 모델에서 웹 페이지 최적화와 관련 있는 단계를 정리한 것이다.
서비스 이동 단계 -> 리다이렉트 단계 -> 애플리케이션 캐시 확인 단계 -> 네트워크 통신 단계 -> 브라우저 처리 단계
- 서비스 이동 단계: 사용자가 웹 서비스를 이용하다 다른 주소로 이동할 때 브라우저가 제일 먼저 실행하는 단계. 프로세싱 모델의 Prompt for unload
- 리다이렉트 단계: 사용자가 요청한 URL에서 다른 URL로 다시 보내는 단계. 프로세싱 모델의 redirect
- 애플리케이션 캐시 확인 단계: 브라우저의 캐시에 데이터가 있는지 확인하는 단계. 프로세싱 모델의 App Cache
- 네트워크 통신 단계: 브라우저가 네트워크와 통신해서 웹 페이지와 구성 요소를 다운로드 하는 단계. 프로세싱 모델의 DNS, TCP, Request, Response 등
- 브라우저 처리 단계: 다운로드한 웹 페이지와 구성 요소로 웹 페이지를 화면에 그리는 단계. 프로세싱 모델의 Processing, onLoad
서비스 이동 단계
서비스 이동 단계는 웹 서비스를 이용하다가 다른 주소로 이동할 때 브라우저가 제일 먼저 실행하는 단계다. 이 단계의 작업은 다른 주소로 이동하기 전에 보고 있던 페이지에서 실행하는데, 모두 브라우저 내부에서 처리하기 때문에 브라우저 성능과 직결된다.
웹 서비스를 이용하면 페이지가 표시될 때 우리도 모르게 이번트가 할당되고, 이때 메모리를 조금씩 사용한다. 그런데 이 메모리를 더 이상 사용하지 않을 때, 즉 페이지를 떠날 때는 메모리를 해제해야 한다. 이 메모리 해제
작업이 서비스 이동 단계에서 실행하는 작업 가운데 하나다.
필요 없는 메모리 해제를 담당하는 가비지 컬렉션 기능
이 제대로 동작하지 않으면 브라우저가 응답 없음 상태가 되거나 실행 속도가 급격히 느려진다. 웹 페이지에서 동시에 많은 변수가 생성되고 처리되는 동안 브라우저에서 허용한 임계치를 넘었을 때 가비지 컬렉션이 동작하는데 가비지 컬렉션이 동작하면 스크립트 실행이 중단된다. 가비지 컬렉션이 완료되기 전까지는 스크립트가 동작하지 못해 페이지 느려지는 것이다.
서비스 이용 단계의 작업은 브라우저 내부에서 자동으로 실행한다. 만약 별도로 처리하려면 beforeunload 이벤트를 활용할 수 있다.
리다이렉트 단계
쉽게 볼 수 있는 리다이렉트는 SNS에서 사용하는 단축 URL이다. SNS의 특성상 입력하는 글자 수에 제한이 있다. 쿼리가 긴 URL을 그대로 SNS에 입력하면 글자 수 제한 때문에 원하는 내용을 다 입력하지 못한다. 이때 단축 URL을 사용하면 SNS의 글자 수 제한을 피할 수 있다. 단축 URL로 접속하면 원본 URL로 리다이렉트 된다.
리다이렉트가 발생하면 상태 코드로 301이나 302를 반환하는데, 이는 HTTP 요청을 완수하기 위해서 추가로 뭔가를 해야 한다는 의미다. 이런 이유로 리다이렉트가 발생하면 어떤 자원도 다운로드 하지 않으며 브라우저에 일시적으로 빈 페이지가 보인다. 그렇기 때문에 특정한 목적으로 리다이렉트를 사용하는 것은 어쩔 수 없지만 의도치 않게 또는 실수로 리다이렉트가 발생한다면 웹 페이지의 성능 향상을 위해 바로 잡아야 한다.
리다이렉트와 관련해서 개발자가 흔히 저지르는 실수 가운데 하나는 URL 뒤에 슬래시를 넣지 않는 것이다. 주소창에 ‘naver.com’을 입력하고 Enter키를 누르면, 주소창에 입력한 주소가 https://www.naver.com/'
으로 바뀌면서 리다이렉트 된다. 이 과정을 개발자 도구로 확인해보면 302 redirect 가 발생하는 것을 확인할 수 있다. a 태그의 href 속성에 주소 뒤에 슬래시를 붙여 301, 302 로 인한 리다이렉트가 발생하지 않도록 막을 수 있다. (307 도 발생하고는 하는데 302와 의미하는바가 같다.)
두번째로 실수하는 부분은 웹 페이지를 이용한 리다이렉트다. 흔히 메타태그를 이용하는데 다음 코드는 1초 후에 페이지가 이동하는 코드다.1
<meta http-equiv="refresh" content="1; url=https://www.naver.com/">
이 코드에는 두 가지 성능 문제가 있다. 첫 번째는 이 코드를 실행시킬 별도의 페이지를 거쳐야 리다이렉트 된다는 점이다. 두 번째는 최종 페이지에 도착했을 때 캐시가 설정된 리소스임에도 불구하고 조건부 GET 요청이 이뤄진다는 점이다.
조건부 GET 요청은 브라우저에 캐싱된 리소스를 사용하기 전에 해당 리소스를 사용해도 되는지 서버에 물어 보는 것이다. 이때 헤더의 If-Modified-Since
정보를 이용해 서버에 요청을 보낸다. 리소스가 수정되지 않았다면 304 Not Modified
코드를 받아 캐시에 있는 리소스를 사용한다. 자원이 수정됐다면 서버로부터 리소스를 다운로드 한다.
캐시의 만료 날짜를 설정
했다면 서버의 확인을 거치지 않고 바로 캐싱된 리소스를 사용할 수 있는데, 메타태그로 리다이렉트하면 조건부 GET 요청으로 불필요한 서버 통신이 발생해 응답 속도가 느려진다.
웹 페이지의 주소뿐 아니라 이미자와 스타일시트, 자바스크립트와 같이 페이지를 구성하는 요소도 리다이렉트 될 수 있다. 301 이나 302 상태 코드가 발생하는 요소를 찾고 의도치 않게 리다이렉트가 발생하고 있다면 성능 향상을 위해서 바로 잡아야 한다.
애플리케이션 캐시 확인 단계
리다이렉트 작업을 마치고 HTTP 요청을 처리하기 위한 준비를 마쳤다면 브라우저는 먼저 서버로 요청을 보낸다. 서버에서 응답이 오면 개별 요소(이미지, 스타일시트, 자바스크립트 등)가 사용자 PC에 있는지 캐시 데이터를 찾는다. 캐시 데이터의 종류에는 쿠키, 이미지, 스크립트, 스타일시트 등이 있다.
캐시 데이터가 있는 이유는 무엇보다도 사용자가 동일한 페이지를 다시 방문했을 때 브라우저와 서버 사이에 통신을 하지 않고 캐시에 있는 자원을 사용하겠다는 것이다. 성능 향상을 위해 가장 효과적인 방법은 브라우저와 서버 사이의 통신을 최대한으로 줄이는 것이다. 그러므로 다시 방문한 사용자에게 좀 더 빠른 응답 속도를 제공하려면 애플리케이션 캐시를 잘 활용해야한다.
캐시를 다룰 때는 개별 요소들이 정말 유효한지, 사용해도 되는 것인지 검증하는 것이 중요하기 때문에 몇 가지 용어와 규칙이 필요하다.
용어 | 설명 |
---|---|
Last-Modified | 서버에서 저장하고 있는 마지막 수정 날짜 예: Last-Modified Sun, 27 Jan 2020 17:00:00 GMT |
If-Modified-Since | 브라우저에서 저장하고 있는 마지막 수정 날짜 예: If-Modified-Since Sun, 27 Jan 2020 17:00:00 GMT |
Expires | 만료 날짜. 언제까지 유효한지 미리 지정해 놓은 시간 정보로, 특정 날짜까지는 사용할 수 있다는 정보. 예: Expires 17:00:00 Sunday, December 27, 2020 GMT |
Cache-Control | Expires 정보의 한계를 극복하려고 HTTP/1.1 에서 소개된 캐시 설정 추가 정보 |
max-age | 캐싱할 시간. Cache-Control 정보 가운데 하나로 캐싱할 시간을 초 단위까지 정할 수 있다. max-age 정보는 클라이언트 시간과 관계 없이 현재부터 며칠, 몇 년 등으로 지정할 수 있다. |
자원 요소를 다운로드할때 브라우저가 처리하는 과정이다.
Expires 정보 확인 (정보 없음)-> If-Modified-Since 정보 확인(정보 있음) -> If-Modified-Since 정보와 Last-Modified 정보 비교
- Expires 정보가 있고 기한이 남아 있으면 캐시에 저장된 요소를 사용한다.
- If-Modified-Since가 없으면 서버에 요소를 요청한다.
- If-Modified-Since 정보가 있으면 Last-Modifed 정보를 비교하고 날짜가 같으면 캐시에 저장된 요소를 사용하라는 응답 304 Not Modifed 을 보내주고, 날짜가 다른 경우 서버의 요소를 전송한다.
사용자 PC에 캐시 파일이 있고 Expires 정보가 유효하다면 해당 파일을 서버에 요청하지 않고 캐시에 저장된 파일을 이용한다. 이러면 동적인 이미지 등을 제외한 나머지 정적인 요소는 별도로 서버에 요청하지 않기 때문에 로딩 속도를 크게 향상시킬 수 있다.
네트워크 통신 단계
네트워크 비용을 줄이는 첫 번째 방법은 Expires 설정이나 Cache Control 속성을 이용해 사용자 웹 페이지에 다시 왔을 때 캐시를 사용하는 방법이다. 두 번째 방법은 스타일시트나 자바스크립트와 같이 파일을 합쳐서 서비스해도 문제가 없는 리소스를 합쳐 하나의 링크로 제공해 요청 횟수를 줄이는 방법이다.
DNS (DNS Lookup)
호스트 이름으로 IP 주소를 조회한 시간이다. 보통 20~120ms 정도 소요된다. 이 작업이 완료되지 않으면 어떤 자원도 다운로드 하지 않는다. 브라우저에서 IP 주소를 조회할 때 동일한 호스트로 요청하는 요소는 다시 IP 주소를 확인하지 않아 시간이 추가로 걸리지 않는다. 그럼 하나의 호스트로 모든 리소스를 호출하면 시간을 줄일 수 있을 것 같지만, 대신 호스트별 동시 연결 기능은 포기해야 한다.
보통 이미지의 크기가 크고 개수가 많은 이미지 검색 서비스를 개발할 때 이런 부분을 많이 고려한다. 한 번에 보이는 이미지의 개수가 많을 때 호스트를 여러 개 만들어 동시에 다운로드하면 성능을 높일 수 있다고 생각하지만 실제로는 고려할 사항도 많다. DNS Lookup 단계에서 걸리는 시간도 무시할 수 없다. 때문에 여러 가지 가정을 세워서 테스트한 다음 최적의 호스트 개수를 찾아야한다.
Request(Send)
웹 서버와 TCP 연결이 이뤄지면 원하는 정보를 서버에 보내는 시간이다. 주로 요청 헤더 정보를 보내는 시간이 여기에 해당한다. Send 시간은 파일 올리기와 같이 보내야 할 정보가 많을수록 오래 걸린다.
일반적인 상황에서 Send 시간을 최적화하는 방법은 쿠키 정보를 포함하지 않는 별도 호스트를 사용해 서비스하는 것이다. 쿠키 정보가 필요 없는 파일(이미지, 스타일시트, 자바스크립트)에는 별도 호스트를 적용해 헤더 정보에서 쿠키 정보를 없애는 것이다.
Response(Receive)
서버에서 보내는 응답 메시지를 받는 시간이다. 이 단계에서 성능을 개선하는 방법으로는 데이터 크기를 줄이기 위해 Gzip과 같은 파일 압축 기술을 사용하는 방법이 있다. 주석이나 공백을 없애 코드의 양을 줄이고, 코드의 양을 줄인 파일을 Gzip 등으로 압축해서 파일의 크기를 줄인다. Gzip으로 압축하면 원본 파일의 약 30% 정도로 크기가 줄어든다.
브라우저 처리 단계
프로세싱 모델의 Processing과 onLoad 에 해당하는 브라우저 처리 단계는 서버에서 받은 HTML과 이미지, 스타일시트를 조합해 사용자가 실제로 보는 화면을 만드는 단계다. 복잡한 UI를 개발할 때는 인라인 형태의 이벤트 바인딩 방법보다 이벤트 핸들러로 바인딩하는 방법을 사용한다. 이벤트 핸들러로 이벤트를 바인딩하려면 최우선 조건이 해당 DOM이 존재해야 한다. 모든 DOM이 존재하는 바로 그 시점이 DOMContentLoaded 이벤트나 onload 이벤트가 발생하는 시점이다.
브라우저의 DOM 처리 절차에 따른 이벤트 발생 순서
domLoading -> domInteractive -> domContentLoaded -> domComplete -> onload
onload 이벤트는 DOM 에서 기본으로 제공하는 이벤트로 문서에 있는 모든 이미지, 스타일시트, 자바스크립트 등이 모두 다운로드 될 때마다 발생한다. 이와 달리 DOMContentLoaded 이벤트는 기본적으로 DOM 생성에만 관련돼있다. 이미자나 다른 요소를 다운로드 하는것과 관계없이 DOM이 로딩되고 난 직후에 발생한다.
많은 양의 이벤트를 바인딩해야 하고 이미지나 스타일시트의 개수가 많은 페이지를 개발한다면 onload 이벤트보다는 DOMContentLoaded 이벤트를 이용하는게 좋다.
참조: 자바스크립트 성능 이야기