[Web.dev] Network

2022. 3. 19. 22:27Frontend

 

 

 

Overview

Web.dev에서 소개하는 Network에 대한 내용들을 정리합니다. 모든 내용들을 다 다루지는 않고, 개인적으로 중요하다고 생각하는 부분들을 추려서 중점적으로 정리했습니다. 자세한 내용들은 아래 Table of Contents의 링크를 통해 확인하실 수 있습니다.

 

 

Table Of Contents

네트워크 안정성을 측정하는 방법 #

캐싱 도구 상자의 옵션 #

 

캐싱 도구 상자의 옵션

 

 

Prevent unnecessary network requests with the HTTP Cache

불필요한 네트워크 요청을 피할 수 있는 첫 번째 방어선은 브라우저의 HTTP 캐시입니다. HTTP 캐시는 모든 브라우저에서 지원되며, 이를 사용하기 위해 개발자가 많은 작업을 할 필요가 없습니다. 실제로 "HTTP 캐시"라는 단일 API는 존재하지 않으며 다음의 API들에 대한 일반적인 명칭을 "HTTP 캐시"라고 명명합니다.

 

  • Cache-Control
  • ETag
  • Last-Modified

 

HTTP 캐시 작동 방식

 

 

브라우저가 제공하는 모든 HTTP 요청은 먼저 브라우저 캐시로 라우팅 되어 유효한 캐시 응답이 있는지를 확인합니다. 이 HTTP 캐시의 동작은 요청 헤더(Request Header)와 응답 헤더(Response Header)의 조합에 의해 제어됩니다.

 

 

Request Header

브라우저는 요청할때 항상 사용자 대신 헤더를 설정합니다. 대부분의 경우 이 부분에 대해서는 별도로 설정할 필요가 없습니다.

 

Response Header

HTTP 캐싱 설정에서 가장 중요한 부분은 웹 서버가 각 발신 응답에 추가하는 헤더입니다. 즉 리소스를 "관리"하는 쪽에서 해당 리소스를 어떻게, 얼마나 캐시 할 수 있는지를 알려주면 이를 사용해서 브라우저나 프록시 서버에 캐시 된 리소스를 사용할 것인지, 캐시를 invalidate 하고 새로운 리소스를 받아올 것인지를 결정합니다.

 

Cache-Control

  • 서버는 해당 지시문을 반환하여 브라우저 및 기타 중간 캐시가 개별 응답을 캐시하는 방법과 기간을 지정할 수 있습니다.
  • 해당 헤더를 생략하더라도 HTTP 캐싱이 비활성화되는 것은 아닙니다. (브라우저는 heuristic freshness라는 방법을 사용해서 HTTP 캐싱에 대한 default action을 정의합니다.)
  • 브라우저는 어떤 유형의 캐싱 동작이 주어진 유형의 내용에 가장 적합한지를 효과적으로 추측합니다.
If a response doesn’t have explicit freshness information like Expires or Cache-Control: max-age, HTTP still allows it to be cached using what’s called heuristic freshness. This means that the cache can guess a freshness lifetime, and it’s useful because servers often don’t assign an explicit freshness lifetime to responses, harming efficiency. Web caching never would have gotten started without it.

 

ETag

  • entity tag의 줄임말로 각 버전을 고유하게 나타낼 수 있는 문자열을 의미합니다.
  • 브라우저가 만료된 캐시 응답을 찾으면 작은 토큰(일반적으로 파일 내용의 해시값)를 서버로 보내서 해당 파일이 변경되었는지 확인할 수 있습니다.
  • 서버가 동일한 토큰을 반환하면 파일이 동일하므로 다시 다운로드하지 않고, 캐시 응답을 재사용합니다.

 

 Last-Modified

  • 문서의 최종 수정 일자, 즉 "날짜"를 의미합니다.
  • ETag와 비슷한 역할을 하지만 "시간기반 전략"을 사용한다는 점에서 차이가 있습니다.

 

 

 

Cache Control Deep Dive

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control

 

Cache-Control - HTTP | MDN

The Cache-Control HTTP header field holds directives (instructions) — in both requests and responses — that control caching in browsers and shared caches (e.g. Proxies, CDNs).

developer.mozilla.org

  • Caching directives are case-insensitive. However, lowercase is recommended because some implementations do not recognize uppercase directives.
  • Multiple directives are comma-separated.
  • Some directives have an optional argument.

 

Cache Control에는 다양한 옵션들이 있지만 중요하게 자주 사용되는 몇 가지 옵션들 위주로 살펴보려고 합니다. 자세한 내용은 위의 공식 문서를 참고해주세요.

 

 

max-age

  • indicates that the response remains fresh until N seconds after the response is generated.
Note that max-age is not the elapsed time since the response was received; it is the elapsed time since the response was generated on the origin server.

 

max-age를 보고 "캐시를 사용하지 않고 오리진 서버에 데이터를 요청해야겠다"라고 판단하는 것은 "브라우저"이며, max-age 값은 "리소스 서버"에서 브라우저에게 Response Header의 값으로 전달합니다. 브라우저는 해당 값을 보고 캐시를 사용할지, 원본 리소스를 가져와 캐시를 갱신할지를 판단할 "기준"으로 사용합니다.

 

 

Response Header에는 추가적으로 "Age"라는 헤더가 명시될 수 있습니다. 이는 해당 리소스가 원 서버에서 바로 넘어온 것이 아니라 중간에 CloudFront와 같이 프록시 서버를 통해서 넘어오는 경우 리소스가 해당 프록시 캐시 내에 "얼마나 머물렀는지"를 알려주는 정보입니다. 실제로 Origin Server와의 시간 델타를 해소하기 위해서 사용하며, 다음과 같이 사용됩니다.

 

  • age 캐시를 명시하면 max-age에서 해당 값을 deduct한 값을 갖습니다.
  • 실제로 qanda.ai의 asset들은 Cloudfront 프록시에 캐시 되며, 아래와 같은 값을 갖습니다.
  • 해당 값을 해석하면 다음과 같습니다
    • 해당 리소스를 요청한 시점에 오리진 서버 (해당 Asset을 가지고 있는 서버) 에서 max-age=36000, public값을 갖는 Cache-Control 헤더를 넣어줍니다. 이 값을 보고 브라우저는 해당 리소스를 받은 지 36000초 뒤에 캐시를 만료시키고 리소스 서버에 새로운 값을 요청합니다.
    • 하지만 실제로 이 요청은 오리진 서버가 아닌 중간의 프록시 캐시(Cloudfront)에서 가져온 것이며, 브라우저가 이 리소스를 요청했을 당지 해당 리소스는 Cloudfront에 2273초가량 머물러 있었습니다. 이는 실제로 다른 요청에 의해 해당 리소스가 Cloudfront에 2273초 캐시 되어 있었다는 의미이며, 따라서 해당 리소스가 Cloudfront에서 만료되어야 하는 시점은 36000 - 2273초 이후입니다. 
    • 따라서 브라우저는 자체적으로 해당 max-age 값을  36000 - 2273초로 계산하며, 이 시간이 지나면 캐시를 만료하고 새로 요청합니다. (이 경우 Cloudfront에 요청하게되며, Cloudfront도 해당 값을 만료시키고 원본 서버에서 새로운 리소스를 받아와 캐시를 갱신합니다.)

 

 

no-cache

  • indicates that the response can be stored in caches, but the response must be validated with the origin server before each reuse, even when the cache is disconnected from the origin server.
  • Note that no-cache does not mean "don't cache". no-cache allows caches to store a response but requires them to revalidate it before reuse. If the sense of "don't cache" that you want is actually "don't store", then no-store is the directive to use.
  • 풀어서 설명하면 "캐시를 하지 않는" 옵션이 아니라 "캐시를 사용하되 사용할 때마다 해당 리소스가 최신인지를 검증하는" 옵션으로 이해할 수 있습니다.

 

no-store

  • indicates that any caches of any kind (private or shared) should not store this response.

 

private, public

  • The private response directive indicates that the response can be stored only in a private cache (e.g. local caches in browsers).
  • The public response directive indicates that the response can be stored in a shared cache.

 

ETag & Last-Modified Deep Dive

https://developer.mozilla.org/ko/docs/Web/HTTP/Conditional_requests

 

HTTP 조건부 요청 - HTTP | MDN

영향을 받는 리소스들을 검사기 값을 이용해 비교함으로써, HTTP는, 성공인 경우라도, 요청의 결과가 변경될 수 있는 조건부 요청의 컨셉을 가지고 있습니다. 그런 요청들은 캐시 컨텐츠와 쓸

developer.mozilla.org

 

검사기

모든 조건부 요청들은 서버 상에 저장되어 있는 리소스가 특정 버전과 일치하는지를 검사하려고 합니다. 이를 위해서 조건부 요청은 리소스의 버전을 명시할 필요가 있으며, 여기서 버전을 뜻하는 값을 “검사기"라고 합니다. 검사기는 ETag와 Last-Modified 가 사용됩니다.

 

강한 검사

리소스를 비교할 때 Byte 단위로 완전히 일치하는지를 검사한다. 즉, 강한 검사를 사용하면 어떤 경우에도 데이터 무손실을 보장합니다. 

기본적으로 HTTP는 강한 검사를 사용하고, 약한검사를 사용할 수 있는 경우 이를 명시합니다. (eTag에 'W/'가 접두사로 붙는다)

 

약한 검사

문서의 내용이 바이트 단위로 완전히 일치하지는 않지만 어느 정도의 수준에서 “유사"한 경우 두 문서의 버전이 동일하다고 간주합니다. (어느 정도까지 유사하다고 판단할지는 명확한 기준은 없으며 ETag 체계를 세우는 사람이 결정합니다.) 예를 들어 기존의 페이지와 푸터 내의 날짜 정도만 다른 경우 경우에 따라 약한 검사 내에서는 동일하다고 간주할 수 있습니다.

 

조건부 헤더

If-Match, If-None-Match

  • 원격지 리소스의 ETag가 헤더에 있는 값들과 일치하는지 여부를 가지고 판단합니다.

If-Modified-Since, If-Unmodified-Since

  • 원격지 리소스의 Last-Modified 가 헤더에 주어진 값보다 오래되었는지 여부를 가지고 판단합니다.

 

Service Worker Caching

서비스 워커를 사용하면 조금 더 세밀한 캐싱 전략을 수행할 수 있습니다. (Network - 2에서 더 자세히 다룰 예정입니다.) 실제로 HTTP 캐시가 Cache-Control, ETag등과 같이 헤더로 브라우저에게 "이렇게 하는 게 어떨까?" 하고 알려주는 식으로 동작한다면, 서비스 워커 캐시는 Javascript & Cache API를 사용하여 "무엇을 캐시 할 것인지", "어떻게 캐시 할 것인지"에 대한 세분화된 제어를 제공합니다.

 

상위 수준에서 브라우저는 리소스를 요청할 때 아래의 캐싱 순서를 따릅니다. 주목할 만한 점은 브라우저나 HTTP Cache보다 서비스워커 캐시를 우선한다는 점입니다. 즉, HTTP 캐시 전략을 수행하기 전에 서비스 워커 캐시 전략이 작동 중이라면 서비스 워커 캐시 전략을 우선한다는 의미입니다.

 

 

 

 

 

실제로 서비스워커 캐시는 다음과 같이 자바스크립트를 사용하여 구현할 수 있습니다.

 

https://github.com/mdn/sw-test/blob/gh-pages/sw.js#L19

 

GitHub - mdn/sw-test: Service Worker test repository. This is a very simple demo to show basic service worker features in action

Service Worker test repository. This is a very simple demo to show basic service worker features in action. - GitHub - mdn/sw-test: Service Worker test repository. This is a very simple demo to sho...

github.com

 

const addResourcesToCache = async (resources) => {
  const cache = await caches.open('v1');
  await cache.addAll(resources);
};

const putInCache = async (request, response) => {
  const cache = await caches.open('v1');
  await cache.put(request, response);
};

const cacheFirst = async ({ request, fallbackUrl }) => {
  // First try to get the resource from the cache
  const responseFromCache = await caches.match(request);
  if (responseFromCache) {
    return responseFromCache;
  }
  // Next try to get the resource from the network
  try {
    const responseFromNetwork = await fetch(request);
    // response may be used only once
    // we need to save clone to put one copy in cache
    // and serve second one
    putInCache(request, responseFromNetwork.clone());
    return responseFromNetwork;
  } catch (error) {
    const fallbackResponse = await caches.match(fallbackUrl);
    if (fallbackResponse) {
      return fallbackResponse;
    }
    // when even the fallback response is not available,
    // there is nothing we can do, but we must always
    // return a Response object
    return new Response('Network error happened', {
      status: 408,
      headers: { 'Content-Type': 'text/plain' },
    });
  }
};

self.addEventListener('install', (event) => {
  event.waitUntil(
    addResourcesToCache([
      '/sw-test/',
      '/sw-test/index.html',
      '/sw-test/style.css',
      '/sw-test/app.js',
      '/sw-test/image-list.js',
      '/sw-test/star-wars-logo.jpg',
      '/sw-test/gallery/bountyHunters.jpg',
      '/sw-test/gallery/myLittleVader.jpg',
      '/sw-test/gallery/snowTroopers.jpg',
    ])
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    cacheFirst({
      request: event.request,
      fallbackUrl: '/sw-test/gallery/myLittleVader.jpg',
    })
  );
});

 

 

(Additional Study) How Nextjs Caching Strategy Works

Nextjs에서 getServerSideProps를 사용해서 사용자에게 커스텀 된 페이지를 적용하는 경우가 있습니다. 사용자의 정보가 포함된 HTML 페이지를 내려주는 경우, 해당 페이지는 프록시 서버에 캐시 되면 안 되기 때문에 기본적으로 Nextjs에서 getServerSideProps를 사용하는 경우 docs의 Response Header를 다음과 같이 설정합니다. 

 

 

"private" 옵션이 적용되어 있기 때문에 Cloudfront와 같은 캐시 서버를 사용하더라도 해당 문서는 캐시되지 않습니다. 또한 no-cache, no-store 옵션이 적용되어 있기 때문에 브라우저도 해당 페이지를 캐시 하지 않으며 따라서 해당 문서는 항상 오리진 서버에서 받아오게 됩니다. (이것이 Cloudfront를 사용하더라도 HTML Docs는 항상 Miss From Cloudfront응답이 나타나는 이유입니다.)

 

https://github.com/vercel/next.js/blob/canary/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts#L402

 

GitHub - vercel/next.js: The React Framework

The React Framework. Contribute to vercel/next.js development by creating an account on GitHub.

github.com

 

그렇다면 getServerSideProps를 사용해서 생성한 Nextjs SSR 페이지는 어떤 방식으로 캐시를 수행해야 할까요? 어떻게 보면 당연하지만, 답은 "docs"이외의 것들을 캐시한다 입니다. HTML 문서는 유저 커스텀한 데이터를 포함하고 있기 때문에 항상 오리진 서버에서 가져오지만 해당 docs가 reference하고 있는 static assets (js, images, videos, css 등)들은 유저 커스텀한 데이터가 아니기 때문에 캐시가 가능합니다.

 

 

 

 

 

따라서 docs를 오리진 서버에서 가져온 후에, 해당 docs가 link하고 있는 값들을 캐시에서 빠르게 가져옴으로써 전체 페이지의 로딩 속도를 개선할 수 있습니다. 

 

 

 

 

반응형

'Frontend' 카테고리의 다른 글

[Nextjs] How getInitialProps Works  (0) 2022.03.28
Strict Mode  (0) 2022.03.27
[Web.dev] Accessibility (2)  (0) 2022.03.19
[Web.dev] Accessibility (1)  (0) 2022.03.06
[Web.dev] Chrome DevTools  (0) 2022.02.28