[html 최적화] preload, prefetch 등에 대하여

먼저 쉬운 이야기부터 하자

Q. html 파일에서 외부 리소스를 불러오는 방법이 있는가?

A. 방법이 있다. <head> 내부에서 링크(link) 엘리먼트를 사용하면 외부 리소스를 불러올 수 있다. 링크는 말그대로 연결해 준다는 뜻이다. html파일과 외부 리소스를 연결해 준다. 연결된 리소스는 연결 즉시 자동으로 반영되기도 하고 내가 리소스를 직접 가져다가 써야 하는 경우도 있다. css의 경우는 링크하는 것 만으로도 자동으로 반영된다

A. 예를 들어 <link rel="stylesheet" href="/style.css" > 과 같은 포멧으로 사용할 수 있다. rel 어트리뷰트는 관계(relationship)의 약자인데 현재 문서와 연결할 리소스의 관계가 어떠한지를 기입한다. 무슨말이냐면 rel="stylesheet"이라고 기입된 코드는 html에서 해당 리소스를 스타일시트의 형태로 참고한다는 뜻으로 해석할 수 있다 또 다른 예인 <link rel="icon" href="favicon.ico">html에서 해당 리소스를 아이콘의 형태로 참고한다는 뜻이다. 이처럼 리소스의 타입을 지정하여 관계를 표기하는 것이 일반적이었다. 과거형을 쓰는 이유는 현재에는 타입에 한정짓지 않고 다른 방식으로도 관계를 표현하기 때문이다. 요즘에는 리소스를 처리하는 전략을 기입하여 관계를 지정하기도 한다.

Q. 리소스를 처리하는 전략이 무슨뜻인가?

A. 말 그대로 리소스를 어떤 방식으로 처리하는지를 나타낸다. 예를 들어 원래 다운받게 되는 타이밍보다 더 앞당겨 다운로드를 받을지, 아니면 리소스를 다운받아 미리 렌더링해놓고 대기하고 있을지 등 다양한 리소스 처리방식 중 하나를 선택할 수 있다. 리소스를 처리하는 전략관계라고 볼 수 없지만 표준 스펙이 그러하니 그냥 그려러니 하는것이 좋겠다.

Q. 말이 너무 추상적인데 리소스를 처리하는 전략에 구체적인 예가 있는가?

A. link의 rel 어트리뷰트에는 프리로드(preload) 라는 옵션을 줄 수 있다. 프리로드는 그 이름이 의미하는 대로 먼저 로드하겠다는 뜻이지만 먼저 로드한다는 말은 너무 추상적이라 크게 와닿지는 않는다
프리로드는 마치 css처럼 리소스를 빨리 다운로드 받고 사용하지는 않겠다는 뜻으로 이해하면 편하다. 비록 이 정의가 정확하지는 않더라도 감을 익히는데는 도움을 준다. 그리고 preload의 load사용하기 위한 모든 조건이 갖추어졌지만 아직 사용하지는 않은 상황을 뜻한다. 이게 내가 아는 가장 쉬운 설명이다.

하지만 위의 설명을 듣고도 의문점이 생길 수 있다

  1. css처럼 리소스를 빨리 다운로드 받는다는 말이 무슨말인가? 어떻게 다운로드 받는게 css처럼 다운로드 받는다는 건가?
  2. 어떤 자원을 css처럼 빨리 다운로드 받아야 하는가?

자 하나씩 설명해보자. 먼저 프리로드를 사용하지 않고 css를 다운로드 받는 코드를 준비했다

<html>
  <head>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
      rel="stylesheet"
    />
  </head>
  <script>
    alert("after head");
  </script>
  <body>
    1234567890
  </body>
</html>

크롬을 켜고 개발자 도구의 Network 탭으로 들어간 뒤에 위의 페이지를 로딩해 보면 아래와 같다

aaa

위의 사진은 alert창이 뜨는 시점을 캡쳐해두었다. 즉 위의 소스에서 alert은 head 엘리먼트가 끝나고 body 엘리먼트가 시작되기 중간 지점이다. 그런 상황에서 이미지 중간의 네트워크 요청란을 보면 boot...라는 항목이 보일것이다. bootstrap.min.css파일을 요청하는 대목인데 여기서 한가지 힌트를 얻을 수 있다. 즉 html 파싱이 채 끝나기도 전에 head 엘리먼트의 파싱이 종료된 상황에서 <link rel="stylesheet">를 확인하고 css파일의 다운로드 요청을 수행한 것이다. 즉 css의 다운로드 요청은 head부분의 파싱이 종료되자 마자 수행된다.

A. 비슷하긴 하지만 조금씩 다르다. 먼저 html의 head 안에 삽입되어서 head 엘리먼트의 파싱이 종료되자 마자 다운로드가 실행되는 점은 같다. 하지만 다운로드의 우선순위가 다를 수 있다. 리소스는 그 리소스의 타입별로 고유의 우선순위가 부여된다. 이 우선순위는 바꿀 수 없으며 반드시 우선순위가 높은 순서대로 리소스 다운로드가 실행된다. css의 우선순위는 가장높음(highest)이라서 대부분의 경우 head의 파싱이 종료되자 마자 다운로드가 수행된다. 반면 이미지 파일은 우선순위가 낮음(low)이라서 우선순위가 높은 파일들이 많을 경우 바로 다운로드를 수행하지 않고 대기한다

Q. 우선순위가 뭔가? 우선순위가 높은 순서대로 다운로드 받는다고 했는데 한개씩 순서대로 다운받는가 ?

A. 이걸 이해하려면 웹브라우저의 연결수 제한의 개념을 이해해야 한다. 웹브라우저는 같은 도메인에서 동시에 6개의 다운로드가 가능하도록 설계되어 있다. 예를 들어 www.naver.com에서 이미지를 다운로드 받는다고 하면 동시에 6개의 이미지만 다운로드가 가능한 것이다. 만일 8개의 이미지를 다운로드 받는다고 하면 먼저 6개의 다운로드를 동시에 수행하고 그 중 하나가 끝나면 나머지 이미지를 다운로드 받는 방식으로 진행된다. 같은 도메인에서 6개를 초과하는 동시 다운로드는 수행될 수 없다.
사실 동시 다운로드 제한수는 웹브라우저와 웹브라우저의 버전별로 어느정도 차이가 있다. 본인이 사용하는 크롬 버전 95기준으로는 6개라서 동접제한이 6개라고 설명한 것이다.
자 방금 설명한 동접 제한을 바탕으로 우선순위를 설명해보자. 아래와 같은 코드가 있다

<
<html>
  <head>
    <!-- css 3개 -->
    <link
      rel="stylesheet"
      href="https://ssl.pstatic.net/sstatic/search/pc/css/sp_autocomplete_210318.css"
    />
    <link
      rel="stylesheet"
      type="text/css"
      href="https://ssl.pstatic.net/tveta/libs/assets/css/pc/main/min/main_topic_darkmode.min.css?20200601"
    />
    <link
      rel="stylesheet"
      type="text/css"
      href="https://ssl.pstatic.net/tveta/libs/assets/css/pc/main/min/rollingboard_imagerolling_350.min.css?20180427"
    />

    <!-- 스크립트 3개  -->
    <link
      rel="preload"
      as="script"
      href="https://ssl.pstatic.net/share/js/naver_sharebutton.js"
    />
    <link
      rel="preload"
      as="script"
      href="https://ssl.pstatic.net/static.gn/js/clickcrD.js"
    />
    <link
      rel="preload"
      as="script"
      href="https://ssl.pstatic.net/sstatic/au/s/pc/_common/ime/nhn.ime_search_140825.js"
    />

    <!--  이미지 4개  -->
    <link
      rel="preload"
      as="image"
      href="https://ssl.pstatic.net/static/pwe/nm/bn/bn_login_02_150128.png"
    />
    <link
      rel="preload"
      as="image"
      href="https://ssl.pstatic.net/static/blank.gif"
    />
    <link
      rel="preload"
      as="image"
      href="https://ssl.pstatic.net/static/common/gnb/one/sp_gnb_v14.png"
    />
    <link
      rel="preload"
      as="image"
      href="https://ssl.pstatic.net/mimgnews/image/420/2018/07/17/093447877_09.TY-01.jpg"
    />
  </head>
  <body>
    1234567890
  </body>
</html>

head부분에 link가 총 10개 있다. css 링크가 3개이고 스크립트 링크가 3개, 이미지 링크가 4개 있다. 그리고 이 링크의 서버는 모두 ssl.pstatic.net로 동일하다. 앞서 말한대로 크롬 95 기준으로 같은 서버에 가능한 동접수는 6개이다. 따라서 10개를 동시에 다운로드 받을 수 없으며 먼저 6개를 선택해야 한다. 자 6개를 선택해 보자
위에서 말한 대로 css파일은 우선순위가 가장높음(highest)이고 스크립트 파일은 높음(high)이며 이미지 파일은 우선순위가 낮음(low)이다. 따라서 우선순위는 아래와 같다

  1. 처음 선언한 css 파일
  2. 두번째 선언한 css파일
  3. 세번째 선언한 css파일
  4. 처음 선언한 스크립트 파일
  5. 두번째 선언한 스크립트 파일
  6. 세번째 선언한 스크립트 파일
  7. 처음 선언한 이미지 파일
  8. 두번째 선언한 이미지 파일
  9. 세번째 선언한 이미지 파일
  10. 네번째 선언한 이미지 파일

위에서 1부터 6번까지가 동시에 다운로드 될 것으로 예상된다. 정말 그럴까 ? 아래는 크롬 개발자 도구의 네트워크 탭에서 워터폴을 표시한 내용이다

download_waterfall

예상대로 우선순위 상위 6개 파일인 css와 스크립트 파일들이 동시에 다운로드 되는것이 보인다. 우선순위가 낮은 이미지 파일의 다운로드는 6개 중 하나의 다운로드가 종료된 2.4초 이후에 수행되었다

Q. 그런데 왜 동접을 6개 파일로 제한하는가 ? 요즘같은 초고속 인터넷 환경에 그런 제약이 굳이 필요한가 ?

A. 사실 동접 제한은 클라이언트 입장에서 생각하면 이해하기 어렵다. 요즘 인터넷은 충분히 빨라서 6개가 아닌 60개 동시요청도 무리없이 수행할 수 있는 수준이다. 하지만 서버에 입장에서 생각해보라. 만일 다운로드 받을 이미지가 60개가 있어서 60개를 동시에 요청한다고 가정하자. 60은 6의 10배이고 이론상으로는 10배 많은 동시 요청이 발생할 수 있는 것이다. 이런 많은수의 요청이 서버에 과부하를 주어서 퍼포먼스 저하를 야기할 수 있기 때문에 동접을 제한한 것으로 추정된다

Q. 프리로드가 적용된 link가 css 요청하고 비슷하게 작동하는건 알겠다. 이런 프리로드를 구체적으로 어떤 상황에 적용할 수 있는가?

A. 프리로드가 흔하게 쓰이는 상황은 웹폰트를 다운로드하는 상황이다. 이 웹폰트를 적용하는 가장 흔한 방법은 css파일 내부의 폰트페이스(font-face)에서 외부 리소스로 읽어들이는 방식이다. 구체적인 코드의 예시를 들어보자

@font-face {
  font-family: "Bitstream Vera Serif Bold";
  src: url("https://developer.mozilla.org/@api/deki/files/2934/=VeraSeBd.ttf");
}

위의 코드는 베라 세리프(Vera Serif)라는 폰트를 불러온다. 이 코드가 실행되려면 아래와 같은 과정을 거친다.

  1. 먼저 css를 다운로드 받는다
  2. css를 파싱한다
  3. font-face에서 src를 발견하면 해당 url에서 리소스를 다운로드 받는다

위와 같은 과정은 크게 css다운로드 이후 -> 폰트 다운로드라는 두 단계로 나뉜다. 하지만 웹폰트를 적용할 때 얼마나 빨리 웹폰트가 웹페이지에 적용되는지는 중요한 문제다. 그러므로 이러한 과정을 더 빠르게 단축시키고 싶다. 가령 css와 폰트파일을 동시에 다운로드 받은 뒤에 css파일에서 다운로드 받은 폰트파일을 바로 적용시키면 css다운로드 이후 -> 폰트 다운로드 라는 2개의 스탭을 거치지 않아도 되지 않은가. 그렇다. 이게 프리로드의 존재목적이다. 프리로드를 사용하면 css와 폰트파일을 동시에 다운로드 받을 수 있다. 가령 아래와 같다

<!-- high 우선순위로 지정됨 -->
<link
  rel="preload"
  href="https://developer.mozilla.org/@api/deki/files/2934/=VeraSeBd.ttf"
  as="font"
  type="font/woff2"
  crossorigin="anonymous"
/>

<!-- highest 우선순위로 지정됨 -->
<link rel="stylesheet" href="/style.css" />

위의 코드는 다운받을 파일이 2개 뿐이므로 이들 두개는 동시에 다운로드가 진행된다. 그 결과 웹사이트 상에 폰트의 적용이 더욱 빨라질 수 있다. 이와 같은 웹 폰트는 하나의 예시일 뿐이며 css내에서 이미지를 다운받거나 비디오를 다운로드 받는 상황에도 똑같이 적용할 수 있다

그리고 프리로드 사용시에 주의할 점은 프리로드로 다운받는 것 만으로는 해당 리소스를 직접적으로 사용하지 않는다는 점이다. 이 프리로드된 파일은 css등의 다른 리소스에서 추가적으로 호출되어야 한다. 만일 해당 자원을 호출하지 않으면 웹브라우저에서 경고 메시지를 보낸다. 크롬의 개발자 도구를 열면 이렇게 프리로드로 호출하고 나서 사용되지 않는 리소스에 대한 경고창을 확인할 수 있다

NOSTATE PREFETCH 스크린샷

위의 내용을 해석하면 리소스가 프리로드 되었지만 몇초이내에 windows의 로드 이벤트에서 사용되지 않았다. 아마도 as값을 잘못 기입한거 같으니까 as 값이 적절한지 확인해. 그게 아니라면 이게 당신이 의도해서 프리로드한 리소스가 맞는가? 한번 확인해봐라
이런 경고창은 두가지 의미에서 이롭다. 첫째로 내가 작성한 코드가 의도한대로 동작하지 않았다는 것을 알려준다. 즉 웹브라우저가 디버거 역할을 한 것이다.
둘째로는 리소스가 프리로드 되고 사용되지 않음으로서 다른 리소스의 다운로드 우선순위가 밀려날 수 있으므로 퍼포먼스 최적화에 악영향을 주니 만일 당신이 사용하지 않는 파일은 프리로드를 걷어내라는 최적화 지침을 내려준다. 이런 지침을 참고하여 개발하면 크롬의 개발자 도구를 훌륭한 디버깅 툴로 이용할 수 있다

또 다른 전략 : 프리커넥트 (preconnect)

프리커넥트를 직역하면 서버와 미리 연결해 놓는다는 뜻이다. 서버와 연결이라는 과정이 생소할 수 있는데 간단히 말하면 필요한 데이터를 받기 위한 선행과정이다. 자원을 서버에 요청할 때 요청한다고 바로 받아지는건 아니고 연결이라는 과정이 필요하다

이건 미리 서버와 미리 연결을 완료해 놓고 필요하면 TCP 핸드쉐이킹 같은 연결과정을 생략한 채 바로 다운받을 수 있도록 준비해 놓겠다는 뜻이다

프리커넥트는 프리로드와는 다르다. 프리로드는 특정한 자원을 다운로드 한다. 그러나 프리로드는 특정한 자원을 다운로드 하지 않는다. 그저 서버와 미리 연결을 해놓을 뿐이다.

의아하지 않은가. 왜 바로 다운로드를 받지 않고 연결만 해 놓을까? 그건 어떤 데이터를 받아올지 아직 결정되지 않은 상황이기 때문이다. 유저가 요청할 리소스의 URL이 상황에 따라서 변경되는 상황이 있다. 프리커넥트는 그런 상황에 쓴다

예를 들어보자. 구직자인 당신은 원서지원 사이트인 원서지원.com에 접속했다. 어떤 기업에 원서를 지원하려 하는데 삼성에 지원할 수도 있고 엘지에 지원할 수도 있다. 만일 삼성에 지원한다면 양식모음.com/form/samsung에서 제공하는 지원양식을 다운받아야 한다. 그리고 엘지에 지원한다면 양식모음.com/form/lg 라는 URL에서 지원양식을 다운받아야 한다. 자 이런 상황은 지원자가 구체적으로 어떤 회사에 지원할지 예측할 수 없다. 이 때 프리커넥트를 사용하지 않았다고 가정해 보자. 만일 삼성 지원자가 양식 다운받기 버튼을 클릭해서 양식모음.com/form/samsung에 접속한다. 이 때 다음과 같은 프로세스가 진행된다

  1. 양식모음.com에 대응하는 DNS를 룩업한다
  2. 룩업한 dns를 기반으로 TCP 3웨이 핸드쉐이킹을 수행한다
  3. tcp 커넥션이 완료되면 보안을 위해 TLS 핸드쉐이킹을 수행한다
  4. 요청한 url에서 데이터를 다운로드 받는다

이런 과정으로 진행된다. 즉 마지막에 데이터를 다운로드 받기 전까지 3가지 선행과정을 필요로 한다 그런데 위의 3가지 과정은 단지 서버와 연결하는 과정이다. 즉 구체적인 URL을 몰라도 서버 주소만 안다면 연결할 수 있다. 즉 위의 예에서는 양식모음.com에 접속하는 것만으로도 연결을 구축할 수 있다. 자 만일 이러한 요청을 먼저 구축한 상태에서 삼성 지원자가 양식 다운받기 버튼을 클릭하면 어떤 프로세스가 진행될까?

  1. 요청한 url에서 데이터를 다운로드 받는다

이게 전부다. 즉 연결에 필요한 모든 과정이 생략된다. 이것은 빠르며 빠른 반응은 유저에게 만족감을 준다
결론을 말하자면 유저가 요청할 엔드포인트가 분명하지는 않지만 서버 주소는 확실한 경우 프리커넥트를 사용하여 속도상의 이득을 볼 수 있다. 이렇게 미리 서버와 연결해 놓으면 유저가 필요한 데이터를 요청할 때 즉각적인 응답을 보장할 수 있다. 프리커넥트는 이러한 상황에 쓰인다

그리고 여담이지만 web.dev는 프리커넥트를 이렇게 광고하고 있다 chrome.com 은 중요한 출처에 사전 연결하여 Time To Interactive 를 거의 1초 개선했습니다
1초라. 웹에서 1초는 큰 시간이다. 위의 말이 맞다면 프리커넥트를 사용할 가치는 충분하다

DNS 프리패치 (dns-prefetch)

DNS 프리패치는 프리커넥트와 비슷하다. 하지만 프리커넥트의 열화 버전이라고 생각하면 좋다. 프리커넥트가 dns 룩업 -> TCP 핸드쉐이킹 -> TLS 핸드쉐이킹 이라는 3단계를 거치는 반면 dns 프리패치는 dns 룩업 의 1단계만을 수행한다. 즉 DNS 프리패치를 수행하고 나서 추가로 TCP핸드쉐이킹과 TLS핸드쉐이킹을 별도로 수행해야 하므로 리소스를 받아오는 시간이 프리커넥트 보다는 느리다.

그러면 이렇게 느린 dns 프리패치를 왜 쓰는가? 클라이언트 입장에서만 보면 dns-프리패치를 사용할 이유는 없어보인다. 하지만 서버의 관점에서 생각해보라. 프리커넥트는 TCP 핸드쉐이킹과 TLS 핸드쉐이킹을 추가로 수행한다. 이런 핸드쉐이킹을 수행하는 과정에서 서버의 CPU와 램 등의 자원이 소비된다. 이것이 서버의 자원낭비로 이어질 수도 있다. 이해가 어려울 텐데 예를 들어보자.

어떤 리소스를 빠르게 다운로드 받으려고 프리커넥트를 걸었다고 가정해 보자. 그런데 이 리소스는 그렇게 빠르게 다운로드 받을 필요는 없는 자원이다. 그런 상황에서 동접자가 10만인 사이트라면 10만개의 커넥션이 생성된다. 이것이 서버에 과부하를 주어서 먼저 급하게 수행되어야 할 프로세스에 악영향을 줄 수도 있다.
실제로 아마존닷컴은 프리커넥트 대신 dns 프리패치 전략을 구사한다. 위에서 언급했듯이 서버의 과부하를 막기 위함이다. 아래 코드는 amazon.com에 접속하여 개발자 도구로 확인할 수 있다

<link rel="dns-prefetch" href="https://images-na.ssl-images-amazon.com" />
<link rel="dns-prefetch" href="https://m.media-amazon.com" />
<link rel="dns-prefetch" href="https://completion.amazon.com" />

위의 코드에서 세번째 라인을 주목하자. completion.amazon.com은 검색어 자동완성에 쓰이는 URL이다. 아마존 메인페이지에서 검색어를 입력하면 해당 URL에 리퀘스트를 날려서 예상되는 자동완성 문자열을 받아온다.
자 그런데 이런 자동완성 기능이 반드시 쓰이는 기능인가? 그렇지 않다. 사람들이 아마존에 검색해서 꼭 상품을 검색하리라는 보장은 없다. 마이 페이지에 들어가서 재구매를 할 수도 있는것이고 오늘의 핫딜 페이지를 서핑할 수도 있는것이고 브라우징 히스토리를 열람해서 내가 이전에 둘러봤던 상품 페이지로 이동할 수도 있다. 즉 아마존에 접속한 모든 사람들이 검색어 입력을 하는 것은 아니다.
이런 상황에서 모든 유저들이 completion.amazon.com에 프리커넥트를 요청한다면 어떻게 될까? 해당 사이트는 수백만건의 TCP핸드쉐이크와 TLS핸드쉐이크를 하느라 자원을 소비할 것이고 먼저 급하게 처리되어야 할 리퀘스트 요청이 지연될 수 있다. 실제로 상품을 검색하는 유저가 이런 지연을 겪는다면 그것은 불쾌한 경험이다. 이처럼 모든 유저가 URL을 요청할 거라는 보장이 없는 상황에서는 무분별한 프리커넥트의 사용이 서버 퍼포먼스의 저하를 야기할 수 있다.
즉 DNS 프리패치는 아래와 같은 상황에서 사용할 수 있다

  1. 과중하게 트래픽이 몰리는 서버
  2. 클라이언트가 서버에 요청을 할지 말지가 분명하지 않은 상황

만일 서버의 CPU나 램 등의 자원이 너무 풍부해서 연결 요청이 얼마가 되든지 퍼포먼스의 하락이 걱정되지 않는 상황이라면 DNS 프리패치를 사용할 이유는 없다. 이런 상황에서는 프리커넥트가 권장된다

프리패치 (prefetch)

프리패치는 미리 다운로드한다는 뜻이다. html파일, 스크립트 파일, css 등의 파일을 미리 다운로드 해놓는다. 문제는 미리 다운로드같은 적당한 용어가 사람들을 헷갈리게 한다. 미리 다운로드한다는 말이 무슨뜻인가?

결론만 말하면 다음 페이지에서 사용할 리소스를 미리 다운로드 한다는 뜻이다. 미리 다운로드 받은 자원은 다음 페이지에서 자원을 요청할 때 캐쉬로 사용할 수 있다. 예를 들어 다음 페이지에 <link rel="stylesheet" >처럼 css 파일을 요청하는 코드가 있을 때, 지금 페이지에서 미리 css 파일을 다운로드 받아놓는다. 그리고 그 다운받은 파일을 메모리 어딘가에 저장해놓는다. 그러면 다음 페이지로 이동했을 때 굳이 서버에서 css를 가져올 필요 없이 메모리에 저장해놓은 캐쉬를 가져와서 사용하면 된다. 이게 프리패치다

자 그러면 한번 생각해보자. 다음 페이지에서 사용할 데이터를 가져오는 작업의 우선순위가 높아야하는가? 아니다. 낮아도 된다. 왜냐하면 다음페이지로 넘어가기 전 까지만 다운로드 받으면 되기 때문이다. 당장 급하게 사용할 자원이 아니라는 뜻이다. 더군다나 프리패치의 우선순위가 높아서 더 먼저 처리해야 하는 리소스의 순위가 밀려나면 현재 페이지의 렌더링이 느려진다. 이것은 사용자에게 불쾌한 경험이다. 따라서 프리패치의 우선순위가 최저(lowest)인 것은 납득할만한 일이다

그런데 몇가지 궁금증이 생길 수 있다.

잠깐만, 유저가 다음 페이지로 넘어갈지 안넘어갈지 어떻게 알지? 그 페이지만 보고 그냥 접속을 꺼버릴 수도 있는 거잖아?
그리고, 다음 페이지로 연결된 링크가 한개가 아니고 수십개인 상황에서는 어떤 페이지로 넘어갈 지 예측할수도 없는데 이런 상황에서는 프리패치를 어떻게 하지?

자 결론부터 말하면 이건 확률게임이다. 유저가 다음페이지로 넘어갈지 여기서 접속을 종료할지는 알 수 없다. 독심술사도 아니고 그런걸 알 방법은 없다. 그러므로 그저 높은 확률을 찍을 뿐이다. 만일 통계상 유저가 다음 페이지로 넘어갈 확률이 95% 정도라면 프리패치를 할 가치는 충분하다. 나머지 5%의 유저가 페이지를 이탈하여 그저 트래픽 낭비로 이어질지라도 95% 유저의 쾌적환 UX경험이 이를 상쇄한다.
그러면 당신은 반문할 것이다.

유저가 다음 페이지로 넘어갈 확률이 몇퍼센트 이상일 때 프리패치를 하는것이 좋지? 70%이상인가? 60% 이상인가?

이것은 정답이 없으며 제공하는 서비스의 종류에 따라 달라진다. 만일 다음 페이지의 로딩이 느려도 상관없다면 프리패치를 전혀 하지 않아도 된다. 그런 페이지가 있냐고? 있다. 환불 페이지나 구독 취소 페이지 같은 페이지들은 기업 입장에서 페이지 로딩이 빨라야 할 이유가 없다
반면 다음 페이지의 로딩속도가 기업의 이윤과 직결되는 중요한 페이지라서 0.01초라도 빨리 수행되어야 하는 서비스라면? 이런 경우는 유저가 다음 페이지로 넘어갈 확률이 50% 미만인 경우라도 무조건 프리패치를 하는 것이 기업의 이윤에 도움이 될 것이다

문제는 다음 페이지로 이동하는 링크가 여러개인 경우인데 이것도 확률게임이다. 사이트 관리자는 특정 유저가 어느 페이지로 이동할 가능성이 높은지 확률 모델을 구축할 수 있다. 이 확률에 근거하여 가장 방문 가능성이 높은 페이지의 리소스를 미리 프리로딩하는 전략이 가장 보편적이다.
물론 이런 방법이 정답은 아니다. 만일 유저가 가장 방문 가능성이 높은 페이지가 아니라 2번째로 방문 가능성이 높은 페이지를 방문한다면 프리패치의 덕을 볼 수 없다. 그러므로 방문 확률이 높은 1, 2, 3번째 페이지의 리소스를 통째로 프리패치하는 전략도 생각해볼만 한것이다.

여기서 한가지 의문이 들 것이다. 방문 가능성이 높은 페이지를 구체적으로 어떻게 알아내지. 어떤 알고리즘을 써야하나? 누가 미리 만들어놓은 라이브러리가 있지 않을까?
그렇다. guess.js 는 확률에 기반하여 방문페이지를 예측하는 라이브러리다. 상세 주소는 https://github.com/guess-js/guess이니 관심이 있으면 참조해 보는것이 좋겠다

그 외의 대중적인 전략은 호버링 전략이다. 유저가 특정 링크에 마우스를 올려놓는 행위를 호버링이라고 하는데 이렇게 호버링을 하는 상황을 이벤트리스너로 탐지하여 프리패치를 수행하는 전략이다. 이것은 next.js에서 수행하는 기법인데 만일 모든 페이지의 링크가 버튼으로만 이루어져 있는 사이트라면 백퍼센트에 육박하는 히트 레이트를 보여준다. 다만 유저가 링크에 마우스를 올려놓고 클릭하는 행위가 매우 찰나에 이루어진다면 그 짦은 시간동안 프리패치가 온전히 수행되지 않을수도 있다. 즉 모든 리소스를 미리 로딩해 놓고 페이지가 이동되자 마자 바로 짠하고 보여주기는 어려울 수도 있다. 이처럼 프리패치 전략에는 모두 장단점이 있어서 어느 하나를 고집하기는 어려운 것이다

프리랜더 (prerender)

프리랜더는 말 그대로 미리 랜더링 한다는 뜻이다. 대략적인 문법은 <link rel="prerender" href="/이동할_페이지의_URL"> 처럼 사용한다.
프리랜더는 프리패치와 같은 컨셉이다. 다음 페이지에 사용할 리소스를 미리 다운로드 받아서 캐쉬로 사용하겠다는 전략이다. 하지만 프리패치에서 한단계 진화된 버전인데 받아온 리소스를 먼저 렌더링한다
이 렌더링이라는 작업을 오해하기 쉬운데 프리랜더 전략으로 리소스를 가져온다고 하더라도 웹브라우저 상에서는 가시적인 아무런 변화가 없다. 그래서 뭐가 렌더링이 됐다는 건가?하고 의아해 할 수 있다. 정상적인 반응이다. 이 렌더링 작업은 백그라운드에서 진행되기 때문에 눈에 보이지 않는다. 백그라운드에서 새로운 탭을 생성하여 그 탭에서 html을 파싱하여 돔트리를 만들고 렌더 트리를 만들고 필요하다면 자바스크립트까지 실행한다. 이렇게 미리 다음 페이지를 렌더링 한 상황에서 유저가 다음 페이지로 이동하는 버튼을 클릭하면 어떤 일이 벌어질까? 이미 렌더링 되어있는 렌더 트리를 모니터상에 페인팅하면 될 뿐이다.
즉 일반적인 페이지 이동에 비해서 웹브라우저에서 처리해야 하는 프로세스가 줄어든다. 일반적인 페이지 이동시에 웹브라우저는 아래와 같은 프로세스를 거친다

  1. 유저가 다음페이지로 이동 버튼을 클릭한다
  2. 다음페이지의 URL에서 html 파일을 다운로드받는다
  3. html을 파싱한다
  4. 파싱한 html을 해석하여 dom 트리와 cssom 트리를 생성한다
  5. dom트리와 cssom 트리를 합병하여 렌더트리를 생성한다
  6. 렌더트리와 웹브라우저의 창크기를 기반으로 픽셀을 생성한다
  7. 여러 레이어로 분리된 픽셀을 합병하여 화면상에 드러나는 부분만 비트맵을 생성한다
  8. 비트맵 정보를 OS 커널을 통하여 GPU 하드웨어에 전송한다
  9. GPU에서 모니터로 픽셀 정보를 전송한다
  10. 모니터가 픽셀을 뿌린다

이것이 웹페이지 이동시에 일어나는 일반적인 과정이라면 이미 프리랜더링 되어있는 상태에서는 위의 2-5 스탭이 스킵된다. 즉 아래와 같다

  1. 유저가 다음페이지로 이동 버튼을 클릭한다
  2. 이미 생성된 렌더트리와 웹브라우저의 창크기를 기반으로 픽셀을 생성한다
  3. 여러 레이어로 분리된 픽셀을 합병하여 화면상에 드러나는 부분만 비트맵을 생성한다
  4. 비트맵 정보를 OS 커널을 통하여 GPU 하드웨어에 전송한다
  5. GPU에서 모니터로 픽셀 정보를 전송한다
  6. 모니터가 픽셀을 뿌린다

이것이 전부다. 두말할 것 없이 빠르다.
즉 프리랜더는 다음과 같은 상황에서 수행할 수 있다

  1. 유저가 다음 페이지로 이동할 가능성이 높은 경우
  2. 동시에 다음 페이지의 화면을 빠르게 보여줘야 하는 경우

이런 프리랜더가 퍼포먼스 면에서는 우수하지만 메모리를 많이 잡아먹는다는 단점이 있다. 크로미움 계열 웹브라우저에서 프리랜더는 약 100~150메가 가량의 메모리를 소비한다. 메모리가 넉넉한 데스크탑 환경이라면 메모리에 의한 디메리트가 크게 신경쓸 수준은 아니다. 하지만 메모리 크기가 적은 모바일 환경에서는 상당히 걸림돌이 될 수 있다.
그래서 크롬 웹브라우저는 메모리 크기가 일정수준 이하인 디바이스에서 프리랜더를 수행하지 않는다. 프리랜드(prerender) 키워드가 있더라도 프리랜더 대신 프리커넥트(preconnect)를 수행한다. 이러한 로우엔드 디바이스의 프리랜더링 문제는 아직까지도 해결되지 않고 있다. 이것은 메모리 크기라는 하드웨어적인 한계이므로 앞으로도 극복 방안이 없을 것으로 보인다.

구글 크롬팀은 이러한 메모리 문제 때문에 곪머리를 앓고 있던 도중 노 스테이트 프리패치(nostate prefetch)라는 개념을 고안해 냈다.

Q. 노 스테이트 프리패치(nostate prefetch)가 뭔가?

A. 노스테이트 프리패치는 프리랜더링과 개념적으로는 거의 유사하지만 메모리를 덜 잡아먹는 것을 목표로 설계되었다. 이는 최대 45메가의 메모리를 소비한다.
그렇다면 노 스테이트 프리패치가 메모리 사이즈가 적은 로우엔드 디바이스에서도 작동할까? 그렇지는 않다. 노 스테이트 프리패치가 작동하기 위한 두가지 조건이 있다

  1. 유저의 장치가 로우엔드 디바이스가 아닐 것
  2. 유저가 LTE나 5G등의 이동통신 네트워크에 접속한 상태가 아닐 것. 이 조건이 붙은 이유는 유저가 원치 않는 데이터 사용을 막기 위함이다. 만일 유저가 원치 않는 상황에서 무분별하게 프리패치를 수행했다가 요금 폭탄이라도 맞으면 누가 책임을 질 것인가. 그러므로 5G등의 환경에서는 노스테이트 프리패치가 작동하지 않는다

(상세는 여기를 참고할 것)

Q. 노 스테이트 프리패치(nostate prefetch)를 어떻게 사용할 수 있는가?

A. 아래 스탭을 따라하시오

  1. 크로미움 계열의 웹브라우저에서 URL란에 chrome://flags/를 입력한다
  2. 검색란에 nostate라고 검색한다. 그러면 크롬 버전 95.0.4638.69 기준으로 아래와 같은 항목이 뜬다

NOSTATE PREFETCH 스크린샷

Enable NoStatePrefetch on Navigation Predictor Isolated Prerenders라는 항목이 보이는데 아래 설명란에 보면 고립된 프리랜더에서 Nostate Prefetch를 활성화한다라고 적혀있다. 고립된 프리랜더라는건 프리랜더가 외부와 리소스 공유를 하지 않는다는 의미로 보인다. 이 옵션을 활성화하면 <link rel="prerender"> 라고 적혀있는 리소스 요청문을 실행할 때 노스테이트 프리패치 방식으로 작동한다. 즉 전통적인 프리렌더링 대비 메모리 사이즈를 절감할 수 있다

— 끝

홈으로