코드노트

CacheStorage 사용 기록, 라이브러리 없이 HTTP 캐싱 정리 본문

Code note/codenote

CacheStorage 사용 기록, 라이브러리 없이 HTTP 캐싱 정리

코드노트 2023. 7. 19. 01:48

LocalStorage, sessionStorage는 사용해봤지만 CacheStorage는 사용해볼 경험이 없었다.

현재 원티드 프리온보딩 인턴십에 참여하면서 매주 과제를 하고 있는데

4주차 과제에서 라이브러리 없이 API호출별로 로컬에 캐싱을 구현하는게 구현 목표중에 있었다.

검색을 할때마다 검색어에 맞는 요청URL별로 응답값을 캐싱하여야 한다.

 

지금까지 여러 프로젝트를 하면서 axios, react-query를 사용하면서 캐싱이 되도록 사용했었는데 라이브러리 없이...?

방법을 찾던 중 CacheStorage를 사용하여서 캐싱시키는 방법을 사용하기로 했다.


여기서 알고 넘어가야하는 것

HTTP 캐싱

- 이전에 가져온 리소스들을 재사용함으로써 네트워크 트래픽을 줄여 리소스를 보여주는데 필요한 시간을 줄일 수 있다.

- HTTP 캐싱을 활용하면 웹사이트가 조금 더 빠르게 반응 하도록 만들 수 있다.

 

현재 과제에서 사용하는 요청 예제

HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 401
Content-Type: application/json; charset=utf-8
ETag: W/"191-7DnIoxk/oktj4DgKFpQhLQmCm7M"
Expires: -1
Keep-Alive: timeout=5
Pragma: no-cache
Vary: Origin, Accept-Encoding
X-Content-Type-Options: nosniff
X-Powered-By: Express

Cache-control : no-cache

- 캐싱 제어에서 헤더에 들어가는 값은 no-cache로 캐시를 사용하지 않고 있다. 그렇기에 프론트 측에서 따로 설정을 해야겠다.

 


CacheStorage

- 왜 저장소는 CacheStorage인가?

- 앞에서 말했다시피 local, session storage 들이 많이 있지만 사용할수 있는 크기도 많이 작기도 하고 검색어별로 응답받은 데이터를 같이 캐싱시켜야하기 때문에 cacheStorage가 적합하다고 판단하였다.

 

CacheStorage를 사용하면서도 고려해야할 것들이 몇가지 있었다.

- 중복된 캐시 항목 : 검색어 params를 포함한 URL으로 설정하여 같은 검색어면 CacheStorage에 있는 값을 사용하도록 해결

- 캐시 만료 기간 : 만료기간을 설정하여 만료여부를 계산하여 만료된 캐시라면 네트워크에 재요청

- 만료된 캐시 항목의 정리 : delete메서드를 사용하여 캐시된 데이터를 지울 수 있었다. 그러나 CacheStorage의 전체 내역들을 순회하여 확인하여 만료여부를 체크후 지워야하는 과정이 검색할때마다 이루어지는것은 효율이 있지 않다고 판단하였다.

이번 코드에서는 정리하는 코드는 제외시켰다.

 

사용한 메서드

Caches.open(request, options) : 지정된 요청과 옵션을 사용하여 캐시 스토리지를 열고 관리하는 Promise를 반환

Caches.match(request, options) : 객체의 첫 번째 일치하는 요청과 응답으로 확인되는 Promise를 반환

Chche.put(request, options) : 지정된 요청과 응답을 사용하여 캐시에 새로운 항목을 추가하는 Promise를 반환

 

 

⚙️ 구현 로직

  static async getSearchByQuery(query: string) {
    const url = `${BASE_URL}sick?q=${query}`;
    const cache = await caches.open(this.searchCacheStorage);

    return await this.getValidResponse(cache, url, query);
  }
  
    private static async getValidResponse(cache: Cache, url: string, query: string) {
    const cacheResponse = await caches.match(url);
    return cacheResponse && !this.isCacheExpired(cacheResponse)
      ? await cacheResponse.json()
      : await this.getFetchResponse(cache, url, query);
  }

- query는 검색어 params와 같게 하여 url을 키값 사용

- caches.open()은 Promise를 반환하기 때문에 await를 사용하여 비동기적으로 캐시를 열고 결과값을 cache변수에 할당

- getValidResponse 메서드를 통해서 현재 저장된 캐시중에서 해당 url과 같은 캐시된 데이터가 있는지를 확인

* 없다면 undefined를 반환

- 만약 같은 값이 있고 isCacheExpired(만료기간 검사)에도 해당하지 않는다면 캐시된 데이터 사용 그렇지 않으면 네트워크 요청

 

  private static async getFetchResponse(cache: Cache, url: string, query: string) {
    const fetchResponse = await getSearchData(query);
    if (fetchResponse.data.length !== 0) {
      this.setCacheResponse(cache, url, fetchResponse);
    }
    return fetchResponse;
  }
  
    static async setCacheResponse(cache: Cache, url: string, data: any) {
    const cacheResponse = new Response(JSON.stringify(data));
    const newResponse = await this.getResponseWithFetchDate(cacheResponse);
    cache.put(url, newResponse);
  }

- getFetchResponse 는 네트워크 요청 메서드

- 네트워크 요청 후 응답 데이터를 cache시켜야하는데 만약 데이터가 없다면 return 시키도록 설계

 

  private static async getResponseWithFetchDate(fetchResponse: Response) {
    const cloneResponse = fetchResponse.clone();
    const newBody = await cloneResponse.blob();
    const newHeaders = new Headers(cloneResponse.headers);
    newHeaders.append(HEADER_FETCH_DATE, new Date().toISOString());

    return new Response(newBody, {
      status: cloneResponse.status,
      statusText: cloneResponse.statusText,
      headers: newHeaders,
    });
  }
  
  private static isCacheExpired(cacheResponse: Response) {
    const fetchDate = new Date(
      cacheResponse.headers.get(HEADER_FETCH_DATE)!
    ).getTime();
    const today = new Date().getTime();
    return today - fetchDate > ONE_DAY_MILISECOND;
  }

- 만료시간은 저장될때 헤더에 현재시간을 함께 저장시키도록 설정

- 그후 현재시간과 저장한 시간을 비교하여 만료기간이 지났다면 새로운 데이터를 요쳥하도록 설정


이렇게 라이브러리없이 CacheStorage를 사용하여 캐싱을 시킬 수 있는것도 좋은 경험이였던것 같다.

라이브러리를 무작정 사용하는것도 좋지않고 네트워크 요청을 줄이는게 좋다고 생각을 했었는데

서버에서 들고오는 데이터들이 자주 변하지 않는 데이터들이라면 중복된 값들을 계속해서 서버에서 들고오는 것보다 cacheStorage를 사용하는것도 좋은 방법중에 하나라는 것을 알게 되었다.

 

 

참고자료

- Velog-[Cache API] 프론트엔드의 자체적인 HTTP caching 구현 및 만료 일자 지정하기 (feat. 서버의 response headers를 변경할 수 없을 때)

- 웹 서비스 캐시 똑똑하게 다루기 - toss

- MDN Cache

- MDN HTTP 캐싱