개요

Server Action을 이용해 Revalidate를 실행한다면, Next 내부의 서버리스 함수를 통해 식별할 수 있는 tag나 page를 이용해 해당하는 캐시를 재검증할 것입니다.

하지만 외부에서 데이터를 관리하는 CMS의 경우엔 어떨까요? CMS에서 revalidate를 하는 방법의 간단한 아키텍처와 next에서 제안하진 않았지만 새로운 관점으로 접근한 POC 경험을 공유해 보려 합니다.

CMS를 이용한 On-Demand Revalidate

Next 내부의 서버리스 함수를 이용하는 건 동일합니다. 다만 CMS에서 데이터가 업데이트한 시점을 알 필요가 있기 때문에 Next의 Route Handler를 이용합니다. 순서는 다음과 같습니다.

  1. CMS에서 업데이트 시 Next 서버로 식별할 수 있는 tag나 page 정보를 담아 요청을 보낸다.
  2. Next 서버는 들어오는 요청을 통해 tag나 page를 식별 후 revalidate 함수를 실행해 캐시를 업데이트한다.

아키텍처는 다음과 같습니다.

Redis를 사용한다. Next에서 revalidate를 해야 할까?

앞서 글을 통해, 멀티 인스턴스 환경에서 캐시 동기화를 위해 Redis를 이용해 핸들링했습니다. 여기서 든 생각은 굳이 위의 과정들을 거칠 필요가 있을까? CMS에서 업데이트를 할 때 Redis 캐시를 제거하면 되지 않을까라는 생각을 하게 되었습니다.

아키텍처는 다음과 같습니다. Next 서버와 CMS가 공통으로 Redis에 접근하고 있다면, CMS에서 곧바로 캐시를 제거, Next 서버는 자연스럽게 최신의 데이터를 얻을 수 있습니다.

  • CMS에서 곧바로 캐시를 제거
  • Next 캐시 데이터 요청. 캐시가 제거되었기 때문에 Miss가 발생하고 최신 DB를 가져온다.

실제로 Next 내부에선 revalidate 실행 시 cache를 제거하진 않고 캐시가 생성된 시점의 revalidatedAt을 이용해 제어합니다.

ex) {"version":1,"items":{"post-1":{"revalidatedAt":1709465182364},"allPosts":{"revalidatedAt":1709465182364}}}...

fetch 캐시 key는 어떻게 생성되는가

CMS에서 Redis 캐시를 제거하려면 먼저 해당 key을 알아내야 합니다. 저희는 이 캐시의 식별자인 tag나, page를 알고 있을 뿐 이 캐시의 key를 알고 있진 않습니다. 이 캐시는 어떻게 생성될까요? Next 내부 구현체인 incremetal-cache 파일의 fetchCacheKey 함수를 통해 알 수 있습니다.

fetchCacheKey

코드 양이 길지만, 분석을 통해 저희가 필요로 하는 코드의 양을 다음과 같이 줄일 수 있었습니다.

1async fetchCacheKey(
2 url: string,
3 init: RequestInit | Request = {}
4 ): Promise<string> {
5 const MAIN_KEY_PREFIX = 'v3';
6 const bodyChunks = [];
7 const cacheString = JSON.stringify([
8 MAIN_KEY_PREFIX,
9 this.fetchCacheKeyPrefix || '', // fetchCacheKeyPrefix 지정하지 않았다면 빈 문자열 ''
10 url,
11 init.method,
12 typeof (init.headers || {}).keys === 'function'
13 ? Object.fromEntries(init.headers as Headers)
14 : init.headers,
15 init.mode,
16 init.redirect,
17 init.credentials,
18 init.referrer,
19 init.referrerPolicy,
20 init.integrity,
21 init.cache,
22 bodyChunks,
23 ])
24
25 return crypto1.createHash('sha256').update(cacheString).digest('hex');
26 }

일반적으로 edge 환경이나, fetch option의 body가 없는 경우 동일할 거라 생각됩니다. 각 환경에 맞춰 정리가 필요해 보입니다.

파라미터들을 분석해 보면 다음과 같습니다.

  1. url: fetch 함수의 url ex) http://localhost:3000/post/3
  2. init: fetch 함수의 option ex) { headers: {}, method: 'GET' .... }

key 검증 후 캐시

제거 검증을 해봐야겠죠. 먼저 http://localhost:4000/posts/1, { method: 'GET' } 요청의 캐시 key는 다음과 같습니다.

  • next에서 생성한 CacheKey 그리고 필요한만큼 추출한 알고리즘을 이용해 생성한 key는 다음과 같습니다.
  • custom 알고리즘으로 생성한 CacheKey

동일한 key를 얻었으니 CMS 서버단에서 Redis에 접근, 해당 캐시를 제거합니다.

1async deleteClientCache({ apiUrl }) {
2 // e32d0baa5a55389433be32b584c1016a439a888a54f70ebe5f25fde5fae39236
3 const cacheKey = customFetchCacheKey(apiUrl);
4
5 try {
6 await this.cache.remove(cacheKey);
7 console.log('Cache deleted successfully');
8 } catch (error) {
9 console.error('Cache deletion error:', error);
10 throw error;
11 }
12 }

개선점

1. HTTP 요청 수를 줄일 수 있다.

기존의 아키텍처에선 2번의 요청이 발생합니다.

  1. CMS update 시 Next Route Handler로 요청
  2. Next 내부의 서버리스 함수를 이용한 Cache purge 요청 개선된 경우, CMS update 시 redis에 접근해 key를 제거합니다.

2. Cache를 CMS에서만 핸들링할 수 있다.

저번 글의 개선 사항으로 Route Handler를 이용한 revalidate 방식에서, 누구나 url을 통해 캐시를 revalidate 시킬 수 있다는 점을 고려했습니다. 따라서 CMS에서 Route Handler로 요청을 보낼 때 header에 인증 코드를 심어 Next 서버에서 인증을 진행하려 했습니다.

하지만 이젠 그 과정이 생략되어 CMS에서 직접 Redis에 연결 후 캐시를 제거합니다. 추가적으로 위와 같은 상황을 고려하지 않아도 됩니다.

tag만 알 수 있다면 누구나 Revalidate를 시킬 수 있다.

localohst:3000/api/revalidate?tag=xxx

3. 두 서비스 연결을 위한 불필요한 코드 제거

두 서비스 간 요청하는 과정이 생략되었기 때문에 상당 부분 코드가 제거됐습니다.

  • CORS 설정: 서로 간의 요청을 주고받기 위한 설정이 생략됩니다.
  • Next Route Handler 아키텍처: Revalidate를 위한 코드가 생략됩니다.
  • Route Handler 보안 코드: 2번 과정인 아무나 Revaldate 할 수 없는 로직에 생략됩니다.

한계

이 방식이 POC 단계에 그친 이유는 fetchCacheKey를 생성하는 알고리즘이 언제 어떻게 변경될지 모르기 때문입니다. Next 버전을 업데이트할 때마다 이 로직이 변경사항이 있는지 체크를 해야 할 겁니다. 그 만한 가치가 있더라도 캐시 key에 직접 접근하는 방법은 보안적으로 위험하지 않을까? 하는 우려가 있습니다.

혹은 fetchCacheKey 생성 알고리즘을 customizing 할 수 있는 기능을 제공해 주면 가능하지 않을까요? Github ideas에 제안을 해봤지만 조금 더 지켜볼 필요가 있을 것 같습니다. 🥲

참고

  1. Next fetchCacheKey 함수 구현 부분
  2. Youtube - Next.js App Router Caching: Explained!