Vanilla JS 데이터 캐시 서비스

5518 단어 programmingjavascript
안녕하세요 팀,

얼마 전에 저는 해결해야 할 흥미로운 작업에 직면했고 오늘은 제가 찾은 것을 공유하고 싶습니다. 일부 데이터를 로드해야 하는 서비스가 있다고 상상해 봅시다.

또한 이 서비스의 데이터 검색 방법을 활용하는 여러 "고객"이 있으며 처음에는 모든 것이 잘 작동하지만 Developers Console을 열었을 때 얼마나 많은 중복 데이터 요청이 있는지 궁금했습니다!

다음은 해당 서비스의 초기 구현입니다(좀 더 일반적으로 다시 작성했습니다).

// let's assume we have some network slowness there...
const delayedDataFetch = () => new Promise((resolve) =>
  setTimeout(() => resolve([1, 2, 3]), 2000)
);

class SomeService {
  async getData() {
    return await delayedDataFetch();
  }
}

// ...
// emulation of parallel requests for the same data
function main() {
  const service = new SomeService();
  await Promise.all([
    service.getData(1),
    service.getData(2)
  ]).then(
    ([res1, res2]) => {
      // receives correct data
      console.log(`1. ${JSON.stringify(res1)}`);
      // receives correct data once again, 
      // from the second call to delayedDataFetch
      // and I want to get rid of that second data call to server
      console.log(`2. ${JSON.stringify(res2)}`);
    }
  );
}


글쎄, 내 첫 번째 생각은 그러한 데이터에 대한 약간의 캐시를 구현하는 것이 었습니다.

const CACHE_EXPIRATION_TIME_MS = 5000;

class SomeService {
  constructor() {
    this.cache = {
      isLoading: false,
      data: null,
      // current timestamp plus expiration delay
      expire: 0
    };
  }

  // added sequenceId for more clarity
  async getData(sequenceId) {
    console.log(`Received ${sequenceId} request`);

    // if cache is expired and isLoading is false 
    // - initiate data update from server
    if (this.cache.expire < new Date().getTime() && !this.cache.isLoading) {
      this.cache.isLoading = true;

      this.cache = {
        isLoading: false,
        data: await delayedDataFetch(),
        expire: new Date().getTime() + CACHE_EXPIRATION_TIME_MS
      };
    }

    console.log(`Response ${sequenceId} with data`);
    return this.cache.data;
  }
}


효과가있다! 적어도 네트워크 탭에서 볼 수 있듯이 네트워크 요청 수가 하나로 줄었습니다. 이 코드를 repo에 제출할 수 있을 것 같습니다.

하지만... 잠시만요, 응용 프로그램의 다른 부분에서 오류가 발생하기 시작했습니다. 여기서 주요 문제는 오류가 때때로 다르다는 것입니다. 빠른 검색은 나에게 새로운 문제를 제공합니다. 새 코드는 때때로 null 데이터를 반환합니다.

따라서 이 간단한 접근 방식으로는 문제가 완전히 해결되지 않았습니다. getData 메서드의 이 코드를 자세히 살펴보겠습니다.

  // a first request switch isLoading to true, and...
  if (this.cache.expired < new Date().getTime() && !this.cache.isLoading) {
    this.cache.isLoading = true;
    // ... do something
  }

  // the second one simply receives empty data... 
  console.log(`Response ${sequenceId} with data`);
  return this.cache.data;
}


잡았다! 찾았다! 그러나 "고객"이 기다려야 한다고 어떻게 말할 수 있습니까? 물론 RxJS fe.e.로 전환할 수는 있지만 크기나 번들을 늘리고 싶지는 않으며 더 바닐라 접근 방식을 사용하여 목표에 도달하고 싶었습니다.

두 번째 응답에 또 다른 Promise를 반환하기 시작했습니다.

  // a first request switch isLoading to true, and...
  if (this.cache.expire < new Date().getTime() && !this.cache.isLoading) {
    this.cache.isLoading = true;
    // ... do something
  }

  if (this.cache.isLoading) {
    // it should return something to client in such cases, 
    // which "something" should be resolved back to refreshed data
    console.log(`Response ${sequenceId} with unresolved promise`);
    return new Promise(resolve => ???);
  }

  // the second one simply receives empty data... 
  console.log(`Response ${sequenceId} with data`);
  return this.cache.data;
}


어떻게 해결할까 고민하다가 플래쉬가 머리를 밝혀주네요 :-) - 간단한 PUB/SUB가 필요해요!
추가 구독자 풀을 SomeService 클래스에 추가하기로 결정하고 이름을 SomeServiceWithDataCache로 변경했습니다.

class SomeServiceWithDataCache {
  constructor() {
    this.cache = {
      isLoading: false,
      expire: 0,
      data: null
    };
    this.cacheSubscriptions = [];
  }


그에 따라 getData 메서드의 코드를 변경합니다.

  async getData(sequenceId) {
    console.log(`Received ${sequenceId} request`);
    if (this.cache.expire < new Date().getTime() && !this.cache.isLoading) {
      this.cache.isLoading = true;

      this.cache = {
        isLoading: false,
        data: await delayedDataFetch(),
        expire: new Date().getTime() + CACHE_EXPIRATION_TIME_MS
      };

      // once we receive data - iterate over subscribers pool, 
      // and resolve each with received data, and finally drop
      // subscriptions
      await Promise.all(
        this.cacheSubscriptions.map((res) => res(this.cache.data))
      ).then(() => (this.cacheSubscriptions = []));
    }

    if (this.cache.isLoading) {
      console.log(`Response ${sequenceId} with unresolved promise`);
      // push the promise resolve into subscribers pool
      return new Promise((resolve) => this.cacheSubscriptions.push(resolve));
    }

    console.log(`Response ${sequenceId} with data`);
    return this.cache.data;
  }
}


작동 방식 - 클라이언트로부터 첫 번째 데이터 요청을 수신하고 내부 데이터 캐시가 만료되었음을 발견하고 데이터 로드를 시작합니다. 두 번째 요청의 경우 해결되지 않은 Promise를 반환하고 확인자 기능을 구독자 풀에 넣습니다. 서버에서 데이터를 받으면 구독자 풀을 반복하고 실제 데이터로 확인자를 호출합니다. 그게 다야.

CodeSandBox - https://codesandbox.io/s/simple-data-caching-service-cds83d에서 솔루션에 대한 전체 코드를 찾을 수 있습니다.

좋은 웹페이지 즐겨찾기