Next.js는 서버 사이드 렌더링(SSR)과 정적 사이트 생성(SSG)을 하나로 묶은 매우 성공적인 웹 프레임워크입니다. SSG는 CDN 캐싱 덕분에 웹사이트 로딩 속도를 높여 주고, SSR은 SEO와 동적 데이터 처리에 강점을 발휘합니다.
서버 사이드 렌더링은 풀스택 애플리케이션을 손쉽게 만들 수 있게 해주는 훌륭한 기능입니다. 하지만 조금만 방심해도 Next.js 웹사이트의 성능은 쉽게 저하될 수 있습니다. 이 글에서는 Redis를 활용해 Next.js의 API 호출 속도를 개선하는 방법을 다룹니다. 그에 앞서, 더 간단한 성능 개선 방법부터 짧게 살펴보겠습니다.
SWR로 API 호출 최적화하기
SWR은 매우 똑똑한 데이터 페칭(fetching) 라이브러리입니다. HTTP RFC 5861에서 정의한 stale-while-revalidate(HTTP 캐시 무효화 전략)를 사용합니다. SWR로 API를 호출하면 캐시된 데이터를 즉시 반환한 뒤, 비동기적으로 최신 데이터를 가져와 UI를 갱신합니다. 데이터 신선도에 대한 허용 수준에 따라 refreshInterval 옵션도 설정할 수 있습니다.
const { data: user } = useSWR('/api/user', { refreshInterval: 2000 })
위 코드에서는 user API가 2초마다 갱신됩니다.
Redis를 활용한 서버 사이드 캐싱
SWR은 간단하면서도 효과적이지만, 서버 사이드 캐싱이 꼭 필요한 경우도 있습니다.
- 클라이언트 사이드 캐싱은 개별 클라이언트의 성능을 개선합니다. 하지만 클라이언트 수가 많아지면 서버 측 리소스에 부하가 집중되고, 결국 클라이언트 측 성능에도 악영향을 미칩니다.
- 호출량 제한(quota)이 있는 외부 API를 사용한다면 서버 측에서 API 사용량을 통제해야 합니다. 그렇지 않으면 수많은 클라이언트가 순식간에 API 한도를 소진해 버립니다.
- 동적 입력값을 바탕으로 서버에서 계산·조회·가공하는 리소스라면 클라이언트 사이드 캐싱은 큰 도움이 되지 않습니다.
예제 프로젝트: 코로나19 트래커
이번 프로젝트에서는 Javier Aviles의 Covid API를 사용해 오늘 확진자 수가 가장 많은 상위 10개국을 조회합니다. 데모 웹사이트와 소스 코드를 함께 확인해 보시길 권합니다.
Covid API의 응답을 Redis로 캐싱하면 다음 두 가지 효과를 얻을 수 있습니다.
- 응답 속도 대폭 향상: 데모 페이지에서 확인할 수 있듯이 Covid API를 직접 호출하면 수백 밀리초가 걸리지만, Redis에서 데이터를 가져올 때는 1~2밀리초면 충분합니다.
- 외부 API 보호: 과도한 요청으로 Covid API에 부담을 주지 않습니다.
API 코드
아래 코드는 먼저 API 결과가 Redis에 캐시되어 있는지 확인합니다. 캐시가 없다면 Covid API에서 전체 국가 목록을 가져와 당일 확진자 수 기준으로 정렬한 뒤, 상위 10개국을 Redis에 저장합니다. 이때 "EX" 60 옵션을 함께 지정하는데, 이는 해당 키를 60초 후에 만료(삭제)하겠다는 의미입니다.
import Redis from "ioredis";
let redis = new Redis(process.env.REDIS_URL);
export default async (req, res) => {
let start = Date.now();
let cache = await redis.get("cache");
cache = JSON.parse(cache);
let result = {};
if (cache) {
console.log("loading from cache");
result.data = cache;
result.type = "redis";
result.latency = Date.now() - start;
return res.status(200).json(result);
} else {
console.log("loading from api");
start = Date.now();
return fetch("https://coronavirus-19-api.herokuapp.com/countries")
.then((r) => r.json())
.then((data) => {
data.sort(function (a, b) {
return b.todayCases - a.todayCases;
});
result.data = data.splice(1, 11);
result.type = "api";
result.latency = Date.now() - start;
redis.set("cache", JSON.stringify(result.data), "EX", 60);
return res.status(200).json(result);
});
}
};
UI 코드
UI는 간단한 React 코드로 구성되어 있으며, SWR을 사용해 API에서 데이터를 가져옵니다.
export default function Home() {
function refresh(e) {
e.preventDefault();
window.location.reload();
}
const { data, error } = useSWR("api/data", fetcher);
if (error) return "An error has occurred.";
if (!data) return "Loading...";
return (
<div className={styles.container}>
<Head>
<title>Covid Tracker</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<h1 className={styles.title}>Covid Tracker</h1>
<p className={styles.description}>
Top 10 countries with the most cases today
</p>
<div className={styles.grid}>
<div className={styles.card} onClick={refresh}>
<table className={styles.table}>
<thead>
<tr>
<th>Country</th>
<th>Today Cases</th>
<th>Today Deaths</th>
</tr>
</thead>
<tbody>
{data.data.map((item) => (
<tr>
<td>{item.country}</td>
<td>{item.todayCases}</td>
<td>{item.todayDeaths}</td>
</tr>
))}
</tbody>
</table>
<br />
<em>
Loaded from {data.type} in <b>{data.latency}</b> milliseconds.
Click to reload.
</em>
</div>
</div>
</main>
<footer className={styles.footer}>
This is a sample project for the blogpost.
<a
href="https://blog.upstash.com/nextjs-caching-with-redis"
target="_blank"
rel="noopener noreferrer"
>
Speed up your Next.js application using Serverless Redis for caching.
</a>
</footer>
</div>
);
}