Node.js에서 Redis를 사용하여 캐싱 구현하는 방법

저자는 /dev/color를 Write for DOnations 프로그램의 일환으로 기부 대상으로 선택했습니다.

소개

대부분의 응용 프로그램은 데이터에 의존합니다. 이 데이터는 데이터베이스 또는 API에서 가져올 수 있습니다. API에서 데이터를 가져오려면 네트워크 요청을 API 서버로 보내고 데이터를 응답으로 반환합니다. 이러한 왕복 여행은 시간이 소요되며 응용 프로그램의 응답 시간을 증가시킬 수 있습니다. 게다가 대부분의 API는 특정 시간 프레임 내에서 응용 프로그램에 제공할 수 있는 요청 수를 제한합니다. 이 과정을 속도 제한이라고합니다.

이러한 문제를 해결하기 위해 데이터를 캐시하여 응용 프로그램이 API로 단일 요청을 보내고 이후의 모든 데이터 요청이 캐시에서 데이터를 검색하도록 할 수 있습니다. 데이터를 캐시하는 인메모리 데이터베이스인 Redis는 인기있는 도구입니다. Node.js에서 Redis에 연결하려면 node-redis 모듈을 사용할 수 있으며, 이 모듈은 Redis에서 데이터를 검색하고 저장하는 데 사용할 수 있는 메서드를 제공합니다.

이 자습서에서는 Express 애플리케이션을 구축하여 axios 모듈을 사용하여 RESTful API에서 데이터를 검색합니다. 다음으로, node-redis 모듈을 사용하여 API에서 가져온 데이터를 Redis에 저장하도록 앱을 수정합니다. 그런 다음 캐시 유효 기간을 구현하여 일정 시간이 경과한 후 캐시가 만료되도록 합니다. 마지막으로 Express 미들웨어를 사용하여 데이터를 캐시합니다.

필수 조건

자습서를 따르려면 다음이 필요합니다:

단계 1 — 프로젝트 설정

이 단계에서는 이 프로젝트에 필요한 종속성을 설치하고 Express 서버를 시작합니다. 이 튜토리얼에서는 다양한 종류의 물고기에 대한 정보를 포함하는 위키를 만듭니다. 프로젝트를 fish_wiki라고 부를 것입니다.

먼저 다음 명령어를 사용하여 프로젝트 디렉토리를 만듭니다:

  1. mkdir fish_wiki

디렉토리로 이동합니다:

  1. cd fish_wiki

npm 명령을 사용하여 package.json 파일을 초기화합니다:

  1. npm init -y

-y 옵션은 모든 기본값을 자동으로 수락합니다.

npm init 명령을 실행하면 다음 내용으로 디렉터리에 package.json 파일이 생성됩니다:

Output
Wrote to /home/your_username/<^>fish_wiki<^/package.json: { "name": "fish_wiki", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }

다음 패키지를 설치합니다:

  • express: Node.js를 위한 웹 서버 프레임워크입니다.
  • axios: API 호출에 유용한 Node.js HTTP 클라이언트입니다.
  • node-redis: Redis에 데이터를 저장하고 액세스할 수 있게 해주는 Redis 클라이언트입니다.

세 개의 패키지를 함께 설치하려면 다음 명령을 입력하세요:

  1. npm install express axios redis

패키지를 설치한 후에 기본 Express 서버를 생성합니다.

nano 또는 사용하는 텍스트 편집기로 server.js 파일을 만들고 엽니다:

  1. nano server.js

server.js 파일에서 Express 서버를 만들기 위해 다음 코드를 입력합니다:

fish_wiki/server.js
const express = require("express");

const app = express();
const port = process.env.PORT || 3000;


app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

먼저, 파일에 express를 가져옵니다. 두 번째 줄에서는 app 변수를 express의 인스턴스로 설정하여 get, post, listen 등의 메서드에 액세스할 수 있게 합니다. 이 튜토리얼에서는 getlisten 메서드에 중점을 둘 것입니다.

다음 줄에서는 서버가 청취할 포트 번호를 정의하고 할당합니다. 환경 변수 파일에서 사용 가능한 포트 번호가 없으면 기본값으로 포트 3000이 사용됩니다.

마침내 app 변수를 사용하여 express 모듈의 listen() 메서드를 호출하여 포트 3000에서 서버를 시작합니다.

파일을 저장하고 닫습니다.

node 명령을 사용하여 server.js 파일을 실행하여 서버를 시작합니다:

  1. node server.js

콘솔에 다음과 유사한 메시지가 표시됩니다:

Output
App listening on port 3000

출력에서 서버가 실행되고 포트 3000에서 요청을 처리할 준비가 되었음을 확인할 수 있습니다. 파일이 변경될 때 Node.js가 서버를 자동으로 다시로드하지 않기 때문에 이제 CTRL+C를 사용하여 서버를 중지하여 다음 단계에서 server.js를 업데이트할 수 있습니다.

의존성을 설치하고 Express 서버를 생성한 후에는 RESTful API에서 데이터를 검색합니다.

단계 2 — 캐싱하지 않고 RESTful API에서 데이터 검색

이 단계에서는 이전 단계의 Express 서버를 기반으로 캐싱을 구현하지 않고 RESTful API에서 데이터를 검색하여 데이터가 캐시에 저장되지 않았을 때 어떤 일이 발생하는지를 보여줍니다.

먼저 텍스트 편집기에서 server.js 파일을 엽니다:

  1. nano server.js

다음으로, FishWatch API에서 데이터를 검색합니다. FishWatch API는 물고기 종에 대한 정보를 반환합니다.

server.js 파일에서 다음과 같이 API 데이터를 요청하는 함수를 정의하세요:

fish_wiki/server.js
const express = require("express");
const axios = require("axios");

const app = express();
const port = process.env.PORT || 3000;

async function fetchApiData(species) {
  const apiResponse = await axios.get(
    `https://www.fishwatch.gov/api/species/${species}`
  );
  console.log("Request sent to the API");
  return apiResponse.data;
}

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

두 번째 줄에서는 axios 모듈을 가져옵니다. 그 다음으로, species를 매개변수로 받는 비동기 함수 fetchApiData()를 정의합니다. 함수를 비동기로 만들기 위해 async 키워드를 접두사로 사용합니다.

함수 내에서는 axios 모듈의 get() 메서드를 사용하여 데이터를 검색할 API 엔드포인트를 지정합니다. 이 예제에서는 FishWatch API를 사용합니다. get() 메서드는 프로미스를 구현하므로 프로미스를 해결하기 위해 await 키워드를 접두사로 사용합니다. 프로미스가 해결되고 API에서 데이터가 반환되면 console.log() 메서드를 호출합니다. console.log() 메서드는 API로 요청을 보냈다는 메시지를 기록합니다. 마지막으로 API에서 데이터를 반환합니다.

다음으로, GET 요청을 수락하는 Express 라우트를 정의합니다. server.js 파일에서 다음 코드로 라우트를 정의하세요:

fish_wiki/server.js
...

app.get("/fish/:species", getSpeciesData);

app.listen(port, () => {
  ...
});

앞서 언급한 코드 블록에서 express 모듈의 get() 메서드를 호출하여 GET 요청만 수신하는 라우트를 만듭니다. 이 메서드는 두 개의 인수를 취합니다:

  • /fish/:species: Express가 청취할 엔드포인트입니다. 이 엔드포인트는 URL의 해당 위치에 입력된 모든 것을 캡처하는 :species 라우트 매개변수를 사용합니다.
  • getSpeciesData()(아직 정의되지 않음): 이 URL이 첫 번째 인자에서 지정한 엔드포인트와 일치할 때 호출되는 콜백 함수입니다.

이제 라우트가 정의되었으므로, getSpeciesData 콜백 함수를 지정합니다:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
}
app.get("/fish/:species", getSpeciesData);
...

getSpeciesData 함수는 express 모듈의 get() 메서드에 두 번째 인자로 전달되는 비동기 핸들러 함수입니다. getSpeciesData() 함수는 두 개의 인자를 사용합니다: 요청 객체응답 객체. 요청 객체에는 클라이언트에 관한 정보가 포함되어 있고, 응답 객체에는 Express에서 클라이언트로 보내는 정보가 포함되어 있습니다.

다음으로, getSpeciesData() 콜백 함수에서 fetchApiData()를 호출하여 API에서 데이터를 검색하는 코드를 추가합니다:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  results = await fetchApiData(species);
}
...

함수 내에서는 req.params 객체에 저장된 엔드포인트에서 캡처한 값을 추출한 다음, species 변수에 할당합니다. 다음 줄에서는 results 변수를 정의하고 undefined로 설정합니다.

그 후에, species 변수를 인수로 사용하여 fetchApiData() 함수를 호출합니다. fetchApiData() 함수 호출은 약속을 반환하기 때문에 앞에 await 구문이 붙습니다. 약속이 해결되면 데이터가 반환되고, 그 데이터가 results 변수에 할당됩니다.

다음으로, 런타임 오류를 처리하기 위해 아래 코드를 추가합니다:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  try {
    results = await fetchApiData(species);
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}
...

런타임 오류를 처리하기 위해 try/catch 블록을 정의합니다. try 블록에서는 API에서 데이터를 검색하기 위해 fetchApiData()를 호출합니다.
오류가 발생하면 catch 블록에서 오류를 기록하고 “데이터를 사용할 수 없음” 응답과 함께 404 상태 코드를 반환합니다.

대부분의 API는 특정 쿼리에 대한 데이터가 없을 때 404 상태 코드를 반환하며, 이는 자동으로 catch 블록을 실행시킵니다. 그러나 FishWatch API는 해당 쿼리에 대한 데이터가 없을 때 빈 배열과 함께 200 상태 코드를 반환합니다. 200 상태 코드는 요청이 성공했다는 것을 의미하므로 catch() 블록이 절대로 트리거되지 않습니다.

catch() 블록을 트리거하기 위해 배열이 비어 있는지 확인하고 if 조건이 true로 평가될 때 오류를 throw합니다. if 조건이 false로 평가될 때는 데이터를 포함한 응답을 클라이언트에게 보낼 수 있습니다.

이를 위해 아래 코드를 추가하십시오:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  ...
  try {
    results = await fetchApiData(species);
    if (results.length === 0) {
      throw "API returned an empty array";
    }
    res.send({
      fromCache: false,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}
...

API에서 데이터가 반환되면 if 문은 results 변수가 비어 있는지 확인합니다. 조건이 충족되면 throw 문을 사용하여 메시지가 API returned an empty array인 사용자 정의 오류를 throw합니다. 실행이 완료되면 실행이 catch 블록으로 전환되어 오류 메시지를 로그에 기록하고 404 응답을 반환합니다.

반면에, results 변수에 데이터가 있는 경우 if 문 조건은 충족되지 않습니다. 결과적으로 프로그램은 if 블록을 건너뛰고 응답 객체의 send 메서드를 실행하여 클라이언트에 응답을 보냅니다.

send 메서드는 다음과 같은 속성을 갖는 객체를 사용합니다:

  • fromCache: 이 속성은 데이터가 Redis 캐시에서 오는지 API에서 오는지를 판별하는 값을 허용합니다. 데이터가 API에서 오기 때문에 false 값을 할당합니다.

  • data: 이 속성은 API에서 반환된 데이터가 포함된 results 변수에 할당됩니다.

이 시점에서 완성된 코드는 다음과 같습니다:

fish_wiki/server.js

const express = require("express");
const axios = require("axios");

const app = express();
const port = process.env.PORT || 3000;

async function fetchApiData(species) {
  const apiResponse = await axios.get(
    `https://www.fishwatch.gov/api/species/${species}`
  );
  console.log("Request sent to the API");
  return apiResponse.data;
}

async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  try {
    results = await fetchApiData(species);
    if (results.length === 0) {
      throw "API returned an empty array";
    }
    res.send({
      fromCache: false,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}

app.get("/fish/:species", getSpeciesData);

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

모든 준비가 되었으므로 파일을 저장하고 종료하세요.

Express 서버를 시작합니다:

  1. node server.js

Fishwatch API는 많은 종을 허용하지만, 이 튜토리얼 전반에 걸쳐 테스트할 엔드포인트의 라우트 매개변수로는 red-snapper 물고기 종만 사용할 것입니다.

이제 로컬 컴퓨터에서 즐겨 사용하는 웹 브라우저를 실행하세요. http://localhost:3000/fish/red-snapper URL로 이동하세요.

참고: 원격 서버에서 튜토리얼을 따르고 있다면 포트 포워딩을 사용하여 브라우저에서 앱을 볼 수 있습니다.

Node.js 서버를 계속 실행한 상태에서 로컬 컴퓨터에서 다른 터미널을 열고 다음 명령을 입력하세요:

  1. ssh -L 3000:localhost:3000 your-non-root-user@yourserver-ip

서버에 연결한 후에는 로컬 머신 웹 브라우저에서 http://localhost:3000/fish/red-snapper로 이동하세요.

페이지가 로드되면 fromCachefalse로 설정된 것을 확인할 수 있어야 합니다.

이제 URL을 세 번 더 새로 고치고 터미널을 확인하세요. 터미널에는 브라우저를 새로 고친 횟수만큼 “Request sent to the API”가 로그됩니다.

초기 방문 후에 URL을 세 번 새로 고쳤다면 출력은 다음과 같을 것입니다:

Output
App listening on port 3000 Request sent to the API Request sent to the API Request sent to the API Request sent to the API

이 출력은 브라우저를 새로 고칠 때마다 API 서버로 네트워크 요청이 보내진다는 것을 보여줍니다. 만약 동일한 엔드포인트에 1000명의 사용자가 있는 애플리케이션이 있다면, API로 1000개의 네트워크 요청이 전송됩니다.

캐싱을 구현하면 API로의 요청이 한 번만 수행됩니다. 이후의 모든 요청은 캐시에서 데이터를 가져오므로 애플리케이션 성능이 향상됩니다.

지금은 Express 서버를 CTRL+C로 중지하세요.

이제 API에서 데이터를 요청하고 사용자에게 제공할 수 있으므로 Redis에서 API에서 반환된 데이터를 캐시합니다.

단계 3 — Redis를 사용하여 RESTful API 요청 캐싱

이 섹션에서는 API에서 데이터를 캐시하여 앱 엔드포인트에 대한 초기 방문만 API 서버에서 데이터를 요청하고 이후의 모든 요청이 Redis 캐시에서 데이터를 가져 오도록합니다.

server.js 파일을 엽니 다:

  1. nano server.js

server.js 파일에서 node-redis 모듈을 가져옵니다:

fish_wiki/server.js

const express = require("express");
const axios = require("axios");
const redis = require("redis");
...

동일한 파일에서 하이라이트 된 코드를 추가하여 node-redis 모듈을 사용하여 Redis에 연결합니다:

fish_wiki/server.js

const express = require("express");
const axios = require("axios");
const redis = require("redis");

const app = express();
const port = process.env.PORT || 3000;

let redisClient;

(async () => {
  redisClient = redis.createClient();

  redisClient.on("error", (error) => console.error(`Error : ${error}`));

  await redisClient.connect();
})();

async function fetchApiData(species) {
  ...
}
...

먼저 redisClient 변수를 정의하고 값을 undefined로 설정합니다. 그 후에는 이름이없는 셀프-호출 비동기 함수를 정의합니다. 이는 정의된 즉시 실행되는 함수입니다. 이름이없는 셀프-호출 비동기 함수는 괄호 (async () => {...})로 이름없는 함수 정의를 둘러싸는 것으로 정의됩니다. 셀프-호출로 만들려면 즉시 뒤에 다른 괄호 ()를 사용하여 정의를 따라야합니다. 이렇게하면 (async () => {...})()와 같이 보입니다.

함수 내에서 redis 모듈의 createClient() 메서드를 호출하여 redis 객체를 생성합니다. createClient() 메서드를 호출할 때 Redis가 사용할 포트를 제공하지 않았으므로 Redis는 기본 포트인 6379를 사용합니다.

또한 Node.js의 on() 메서드를 호출하여 Redis 객체에 이벤트를 등록합니다. on() 메서드는 두 개의 인수를 사용합니다: error와 콜백입니다. 첫 번째 인수 error는 Redis가 오류를 만났을 때 트리거되는 이벤트입니다. 두 번째 인수는 error 이벤트가 발생할 때 실행되는 콜백입니다. 콜백은 오류를 콘솔에 기록합니다.

마지막으로 connect() 메서드를 호출하여 기본 포트 6379에서 Redis와의 연결을 시작합니다. connect() 메서드는 프로미스를 반환하므로 해결하기 위해 앞에 await 구문을 사용합니다.

이제 응용 프로그램이 Redis에 연결되었으므로 초기 방문시 데이터를 Redis에 저장하고 이후의 모든 요청에 대해 캐시에서 데이터를 검색하도록 getSpeciesData() 콜백을 수정할 것입니다.

server.js 파일에 다음과 같이 코드를 추가하고 업데이트하십시오:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
     }
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    ...
  }
}
...

getSpeciesData 함수에서는 isCached 변수를 false 값으로 정의합니다. try 블록 내에서는 node-redis 모듈의 get() 메서드를 species를 인수로 사용하여 호출합니다. 이 메서드는 Redis에서 species 변수 값과 일치하는 키를 찾으면 해당 데이터를 반환하고, 반환된 데이터는 cacheResults 변수에 할당됩니다.

다음으로, if 문에서는 cacheResults 변수에 데이터가 있는지 확인합니다. 조건이 충족되면 isCache 변수가 true로 할당됩니다. 이후에는 JSON 객체의 parse() 메서드를 cacheResults를 인수로 사용하여 호출합니다. parse() 메서드는 JSON 문자열 데이터를 JavaScript 객체로 변환합니다. JSON이 파싱된 후에는 send() 메서드를 호출하는데, 이 메서드는 fromCache 속성이 isCached 변수로 설정된 객체를 인수로 사용합니다. 이 메서드는 클라이언트에 응답을 보냅니다.

node-redis 모듈의 get() 메서드가 캐시에서 데이터를 찾지 못하면 cacheResults 변수가 null로 설정됩니다. 결과적으로 if 문은 거짓으로 평가됩니다. 그럴 경우, 실행은 else 블록으로 건너뛰어 fetchApiData() 함수를 호출하여 API에서 데이터를 가져옵니다. 그러나 API에서 데이터가 반환되면 Redis에 저장되지 않습니다.

데이터를 Redis 캐시에 저장하려면 node-redis 모듈의 set() 메서드를 사용하여 저장해야 합니다. 이를 위해 다음과 같이 강조된 줄을 추가하십시오:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
      }
      await redisClient.set(species, JSON.stringify(results));
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    ...
  }
}
...

else 블록 내에서 데이터를 가져온 후에는 node-redis 모듈의 set() 메서드를 호출하여 데이터를 Redis에 species 변수의 값에 해당하는 키 이름으로 저장합니다.

set() 메서드는 두 개의 인수를 취하는데, 이는 키-값 쌍입니다: speciesJSON.stringify(results)입니다.

첫 번째 인수인 species는 데이터가 Redis에 저장될 키입니다. species는 당신이 정의한 엔드포인트에 전달된 값으로 설정됩니다. 예를 들어, /fish/red-snapper를 방문하면 speciesred-snapper로 설정되어 Redis에 키로 사용됩니다.

두 번째 인수인 JSON.stringify(results)는 키의 값입니다. 두 번째 인수에서는 results 변수를 인수로 사용하여 JSONstringify() 메서드를 호출합니다. 이 메서드는 JSON을 문자열로 변환합니다. 이것이 바로 앞서 node-redis 모듈의 get() 메서드를 사용하여 캐시에서 데이터를 검색할 때 cacheResults 변수를 인수로 사용하여 JSON.parse 메서드를 호출한 이유입니다.

이제 완전한 파일은 다음과 같습니다:

fish_wiki/server.js
const express = require("express");
const axios = require("axios");
const redis = require("redis");

const app = express();
const port = process.env.PORT || 3000;

let redisClient;

(async () => {
  redisClient = redis.createClient();

  redisClient.on("error", (error) => console.error(`Error : ${error}`));

  await redisClient.connect();
})();

async function fetchApiData(species) {
  const apiResponse = await axios.get(
    `https://www.fishwatch.gov/api/species/${species}`
  );
  console.log("Request sent to the API");
  return apiResponse.data;
}

async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
      }
      await redisClient.set(species, JSON.stringify(results));
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}

app.get("/fish/:species", getSpeciesData);

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

파일을 저장하고 종료한 후에는 node 명령을 사용하여 server.js를 실행합니다:

  1. node server.js

서버가 시작되면 브라우저에서 http://localhost:3000/fish/red-snapper를 새로 고칩니다.

fromCache가 여전히 false로 설정된 것을 알 수 있습니다:

이제 페이지를 다시 새로 고침하여 이번에는 fromCachetrue로 설정되었는지 확인하십시오:

페이지를 다섯 번 새로 고침하고 터미널로 돌아갑니다. 다음과 유사한 출력이 표시됩니다:

Output
App listening on port 3000 Request sent to the API

이제 Request sent to the API 메시지는 여러 번의 URL 새로 고침 후에도 한 번만 기록되었습니다. 이전 섹션에서는 각 새로 고침마다 메시지가 기록되었습니다. 이 출력은 서버로 하나의 요청만 보내졌음을 확인하고, 이후에는 데이터가 Redis에서 가져온다는 것을 확인합니다.

데이터가 Redis에 저장되어 있는지 추가로 확인하려면 CTRL+C를 사용하여 서버를 중지하십시오. 다음 명령을 사용하여 Redis 서버 클라이언트에 연결하십시오:

  1. redis-cli

red-snapper 키 아래의 데이터를 검색하십시오:

  1. get red-snapper

다음과 유사한 출력이 나타납니다(간결성을 위해 편집함):

Output
"[{\"Fishery Management\":\"<ul>\\n<li><a...3\"}]"

출력에는 /fish/red-snapper 엔드포인트를 방문할 때 API가 반환하는 JSON 데이터의 문자열 버전이 표시되어 API 데이터가 Redis 캐시에 저장되었음을 확인합니다.

Redis 서버 클라이언트를 종료하십시오:

  1. exit

이제 API에서 데이터를 캐시할 수 있으므로 캐시 유효성도 설정할 수 있습니다.

단계 4 — 캐시 유효성 구현

데이터를 캐싱할 때 데이터가 얼마나 자주 변경되는지를 알아야 합니다. 일부 API 데이터는 분 단위로 변경되고, 다른 것들은 시간, 주, 월 또는 년 단위로 변경됩니다. 적절한 캐시 기간을 설정하여 응용 프로그램이 최신 데이터를 사용자에게 제공할 수 있도록 합니다.

이 단계에서는 Redis에 저장해야 하는 API 데이터의 캐시 유효성을 설정합니다. 캐시가 만료되면 응용 프로그램이 최신 데이터를 검색하기 위해 API에 요청을 보냅니다.

캐시의 올바른 만료 시간을 설정하려면 API 설명서를 참고해야 합니다. 대부분의 문서에서 데이터가 얼마나 자주 업데이트되는지 언급할 것입니다. 그러나 문서에서 정보를 제공하지 않는 경우도 있으므로 추측해야 할 수도 있습니다. 다양한 API 엔드포인트의 ‘last_updated’ 속성을 확인하여 데이터가 얼마나 자주 업데이트되는지 확인할 수 있습니다.

캐시 기간을 선택한 후에는 이를 초 단위로 변환해야 합니다. 이 튜토리얼에서는 캐시 기간을 3분 또는 180초로 설정합니다. 이 샘플 기간을 사용하여 캐시 기간 기능을 테스트하기가 더 쉽습니다.

캐시 유효 기간을 구현하려면 ‘server.js’ 파일을 엽니다:

  1. nano server.js

다음 코드를 추가합니다:

fish_wiki/server.js
const express = require("express");
const axios = require("axios");
const redis = require("redis");

const app = express();
const port = process.env.PORT || 3000;

let redisClient;

(async () => {
  ...
})();

async function fetchApiData(species) {
  ...
}

async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
      }
      await redisClient.set(species, JSON.stringify(results), {
        EX: 180,
        NX: true,
      });
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}

app.get("/fish/:species", getSpeciesData);

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

‘node-redis’ 모듈의 ‘set()’ 메서드에서 세 번째 인수로 다음 속성을 갖는 객체를 전달합니다:

  • EX: 초 단위의 캐시 기간 값을 허용합니다.
  • NX: true로 설정하면 ‘set()’ 메서드는 Redis에 이미 존재하지 않는 키만 설정합니다.

파일을 저장하고 나가십시오.

Redis 서버 클라이언트로 돌아가서 캐시 유효성을 테스트하십시오:

  1. redis-cli

Redis에서 red-snapper 키를 삭제하십시오:

  1. del red-snapper

Redis 클라이언트를 종료하십시오:

  1. exit

이제 node 명령으로 개발 서버를 시작하십시오:

  1. node server.js

브라우저로 돌아가서 http://localhost:3000/fish/red-snapper URL을 새로고침하십시오. 다음 세 분 동안 URL을 새로고침하면 터미널 출력이 다음 출력과 일관되어야합니다:

Output
App listening on port 3000 Request sent to the API

세 분이 지나면 브라우저에서 URL을 새로고침하십시오. 터미널에서 “Request sent to the API”가 두 번 기록된 것을 볼 수 있어야합니다.

Output
App listening on port 3000 Request sent to the API Request sent to the API

이 출력은 캐시가 만료되었고 API에 대한 요청이 다시 이루어졌음을 보여줍니다.

Express 서버를 중지할 수 있습니다.

이제 캐시 유효성을 설정할 수 있으므로 다음에는 미들웨어를 사용하여 데이터를 캐시합니다.

단계 5 — 미들웨어에서 데이터 캐시

이 단계에서 Express 미들웨어를 사용하여 데이터를 캐시합니다. 미들웨어는 요청 객체, 응답 객체 및 실행 후에 실행되어야 하는 콜백에 액세스할 수 있는 함수입니다. 미들웨어 이후에 실행되는 함수도 요청 및 응답 객체에 액세스할 수 있습니다. 미들웨어를 사용할 때 요청 및 응답 객체를 수정하거나 사용자에게 더 빨리 응답을 반환할 수 있습니다.

어플리케이션에서 미들웨어를 사용하여 캐싱하려면 getSpeciesData() 핸들러 함수를 수정하여 API에서 데이터를 가져와 Redis에 저장해야 합니다. Redis에서 데이터를 찾는 모든 코드를 cacheData 미들웨어 함수로 이동합니다.

/fish/:species 엔드포인트를 방문하면 먼저 미들웨어 함수가 실행되어 캐시에서 데이터를 검색합니다. 데이터를 찾으면 응답을 반환하고 getSpeciesData 함수는 실행되지 않습니다. 그러나 미들웨어가 캐시에서 데이터를 찾지 못하면 getSpeciesData 함수를 호출하여 API에서 데이터를 가져와 Redis에 저장합니다.

먼저 server.js를 엽니다:

  1. nano server.js

다음으로 강조 표시된 코드를 제거합니다:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
      }
      await redisClient.set(species, JSON.stringify(results), {
        EX: 180,
        NX: true,
      });
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}
...

getSpeciesData() 함수에서 Redis에 저장된 데이터를 찾는 모든 코드를 제거합니다. 또한 getSpeciesData() 함수에서는 API에서 데이터를 가져와 Redis에 저장하기 때문에 isCached 변수도 제거합니다.

코드를 제거한 후에는 하이라이트된 부분과 같이 fromCachefalse로 설정하여 getSpeciesData() 함수가 다음과 같이 보이도록 합니다:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  try {
    results = await fetchApiData(species);
    if (results.length === 0) {
      throw "API returned an empty array";
    }
    await redisClient.set(species, JSON.stringify(results), {
      EX: 180,
      NX: true,
    });

    res.send({
      fromCache: false,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}
...

getSpeciesData() 함수는 API에서 데이터를 검색하여 캐시에 저장하고 사용자에게 응답을 반환합니다.

다음으로 Redis에 데이터를 캐싱하는 미들웨어 함수를 정의하기 위해 다음 코드를 추가합니다:

fish_wiki/server.js
...
async function cacheData(req, res, next) {
  const species = req.params.species;
  let results;
  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      results = JSON.parse(cacheResults);
      res.send({
        fromCache: true,
        data: results,
      });
    } else {
      next();
    }
  } catch (error) {
    console.error(error);
    res.status(404);
  }
}

async function getSpeciesData(req, res) {
...
}
...

cacheData() 미들웨어 함수는 세 개의 인수, req, res, 그리고 next를 사용합니다. try 블록에서는 함수가 species 변수의 값이 Redis에 해당 키로 저장된 데이터를 확인합니다. 데이터가 Redis에 있으면, 이를 반환하고 cacheResults 변수에 설정합니다.

다음으로, if 문은 cacheResults에 데이터가 있는지 확인합니다. 데이터가 true로 평가되면 results 변수에 저장됩니다. 그 후 미들웨어는 send() 메소드를 사용하여 fromCache 속성이 true로, data 속성이 results 변수로 설정된 객체를 반환합니다.

그러나, if 문이 false로 평가되면, 실행이 else 블록으로 전환됩니다. else 블록 내에서 next()를 호출하며, 이는 그 이후에 실행되어야 할 다음 함수로 제어를 전달합니다.

next()가 호출될 때 cacheData() 미들웨어가 getSpeciesData() 함수로 제어를 전달하도록 하려면, express 모듈의 get() 메소드를 다음과 같이 업데이트하십시오:

fish_wiki/server.js
...
app.get("/fish/:species", cacheData, getSpeciesData);
...

get() 메소드는 이제 두 번째 인수로 cacheData를 사용합니다. 이는 Redis에 캐시된 데이터를 찾고 발견되면 응답을 반환하는 미들웨어입니다.

지금은 /fish/:species 엔드포인트를 방문할 때, cacheData()가 먼저 실행됩니다. 데이터가 캐시되어 있으면 응답을 반환하고 요청-응답 주기가 여기서 끝납니다. 그러나 캐시에서 데이터를 찾을 수 없으면 getSpeciesData()가 호출되어 API에서 데이터를 검색하고 캐시에 저장한 다음 응답을 반환합니다.

전체 파일은 이제 다음과 같이 보일 것입니다:

fish_wiki/server.js

const express = require("express");
const axios = require("axios");
const redis = require("redis");

const app = express();
const port = process.env.PORT || 3000;

let redisClient;

(async () => {
  redisClient = redis.createClient();

  redisClient.on("error", (error) => console.error(`Error : ${error}`));

  await redisClient.connect();
})();

async function fetchApiData(species) {
  const apiResponse = await axios.get(
    `https://www.fishwatch.gov/api/species/${species}`
  );
  console.log("Request sent to the API");
  return apiResponse.data;
}

async function cacheData(req, res, next) {
  const species = req.params.species;
  let results;
  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      results = JSON.parse(cacheResults);
      res.send({
        fromCache: true,
        data: results,
      });
    } else {
      next();
    }
  } catch (error) {
    console.error(error);
    res.status(404);
  }
}
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  try {
    results = await fetchApiData(species);
    if (results.length === 0) {
      throw "API returned an empty array";
    }
    await redisClient.set(species, JSON.stringify(results), {
      EX: 180,
      NX: true,
    });

    res.send({
      fromCache: false,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}

app.get("/fish/:species", cacheData, getSpeciesData);

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

파일을 저장하고 종료하십시오.

캐싱을 제대로 테스트하려면 Redis에서 red-snapper 키를 삭제할 수 있습니다. 이를 위해 Redis 클라이언트로 이동하십시오:

  1. redis-cli

red-snapper 키를 제거하십시오:

  1. del red-snapper

Redis 클라이언트를 종료하십시오:

  1. exit

이제 server.js 파일을 실행하십시오:

  1. node server.js

서버가 시작되면 브라우저로 돌아가서 http://localhost:3000/fish/red-snapper를 다시 방문하십시오. 여러 번 새로 고쳐보세요.

터미널에는 API로 요청이 전송되었다는 메시지가 기록될 것입니다. cacheData() 미들웨어는 다음 세 분 동안 모든 요청을 처리할 것입니다. 4분 동안 URL을 무작위로 새로 고칠 경우 다음과 같이 출력됩니다:

Output
App listening on port 3000 Request sent to the API Request sent to the API

이 동작은 이전 섹션에서 애플리케이션이 작동한 방식과 일관됩니다.

이제 미들웨어를 사용하여 Redis에서 데이터를 캐시할 수 있습니다.

결론

이 기사에서는 API에서 데이터를 가져 와서 클라이언트에게 응답 데이터로 반환하는 응용 프로그램을 구축했습니다. 그런 다음 초기 방문에서 API 응답을 Redis에 캐시하고 모든 후속 요청에 대해 캐시에서 데이터를 제공하도록 앱을 수정했습니다. 그 캐시 지속 기간을 일정 시간이 경과 한 후에 만료되도록 수정 한 다음 미들웨어를 사용하여 캐시 데이터 검색을 처리했습니다.

다음 단계로, node-redis 모듈에서 제공되는 기능에 대해 자세히 알아보기 위해 Node Redis 문서를 살펴볼 수 있습니다. 또한이 자습서에서 다루는 주제를 더 깊이 파고들기 위해 AxiosExpress 문서를 읽을 수 있습니다.

Node.js 기술을 계속 구축하기 위해 Node.js로 코딩하는 방법 시리즈를 참조하십시오.

Source:
https://www.digitalocean.com/community/tutorials/how-to-implement-caching-in-node-js-using-redis