개요
예전부터 Next.js의 Data Cache가 File System에 저장되는 방식의 한계를 느껴왔고, 이에 대한 내용을 몇 차례 정리해둔 바 있습니다. Next.js 팀 역시 이 문제를 인지하고 있었고, 최근에는 내부의 Cache Handler를 커스텀할 수 있는 옵션과 간단한 예제를 공식적으로 제공하기 시작했습니다.
하지만 이 예제는 최소한의 기능만을 제공하기 때문에, 실제 현업에서 사용하기에는 부족한 점이 많았습니다. 기존 캐싱 방식을 유지하면서도 다양한 상황에 대응할 수 있도록 확장 가능한 라이브러리를 만들고자 했습니다. 그 결과물이 바로 next-payload-handler
입니다.
next-payload-handler 소개
해결하고자 한 문제
이 라이브러리는 "File System 기반 저장 방식"의 한계를 해결하는 데서 출발했습니다. 서버 내부가 아닌 외부의 공용 캐시 저장소(Redis, GCS 등) 에 데이터를 저장함으로써, 모든 서버가 동일한 캐시 데이터에 접근할 수 있게 됩니다.
이로 인해 revalidateTag
기능을 활용한 정확하고 빠른 캐시 무효화가 가능해지고, 캐시 동기화 이슈도 자연스럽게 해결됩니다.
핵심 컨셉
1. 기존 캐싱 방식과 동일한 동작
Next.js의 예제는 Map
객체를 기반으로 단순한 get
, set
, revalidateTag
기능만 제공합니다. 하지만 실제 내부 구현은 훨씬 더 복잡하며, 성능 최적화를 위한 로직이 포함되어 있습니다.
예를 들어 revalidateTag
는 단순히 Map
을 순회하며 key를 삭제하는 방식이지만, Next.js 내부에서는 tagsManifest
라는 객체에 revalidatedAt
을 기록해두고, 이후 get
시점에 이를 비교해 stale 여부를 판단하는 구조입니다.
이를 기반으로 next-payload-handler
도 동일한 구조와 성능을 유지하도록 설계했습니다.
- 예제
- NextJS 내부 코드
➡️ 예제 코드와 달리, O(N) 순회 없이 O(1)로 revalidate 가능하며, 내부 로직과 동일한 방식으로 동작합니다.
2. 공용 저장소를 활용한 캐시 동기화
가장 중요한 컨셉 중 하나는, 하나의 공용 저장소를 모든 서버가 함께 사용하는 구조입니다. 이를 통해 서버 수와 무관하게 항상 최신 캐시 데이터를 공유할 수 있으며, 외부에서 데이터를 무효화하더라도 즉시 반영됩니다.
이론상 key-value 형식으로 저장될 수 있다면 공용 저장소로 사용할 수 있습니다. 현재는 라이브러리는 Redis와 GCS(Storage)를 지원하며, 추가 저장소 확장도 고려하고 있습니다. 각 방식의 자세한 컨셉은 Redis를 이용한 공유와 GCS를 이용한 공유 포스트을 통해 확인할 수 있습니다.
- A, B, C 어떤 서버에 접근해도 하나의 저장소에서 캐시데이터를 받아온다.
확장 기능
1. 캐시 키 커스터마이징
Next 내부에서 캐시 무효화를 할 때는 revalidateTag()
를 쓰면 되지만, CMS처럼 외부 시스템에서 캐시를 무효화하려면 복잡한 설정이 필요합니다.
- CMS에서 Next App으로 무효화할 tag를 query string에 담아 요청 전송
- Next App에선 CMS에서 요청을 받기 위해 CORS와 Access-Control-Allow-* header 추가
- query string을 파싱해 무효화할 tag 추출
- revalidateTag 함수 실행
캐시 무효화를 위해 너무 많은 코드를 추가해야 합니다. 이를 개선하기 위해, Next App를 거쳐 무효화하는 방식이 아닌, CMS에서 직접 캐시를 무효화하는 방법을 생각했습니다. ( CMS를 이용한 Next fetch 캐시 핸들링 글 참고 )
이 방법이 가능하려면, 캐시 키에 접근할 수 있어야합니다. 하지만 Data Cache의 key는 내부에서 생성되고 해싱되기 때문에, 외부에서 key를 정확히 매칭하기 힘듭니다.
이를 해결하기 위해, Next의 fetch
를 확장한 patchFetch
를 제공하며, 여기에 커스텀 캐시키를 지정할 수 있습니다. 커스텀한 키를 CMS에서 곧바로 제거함으로써 무효화를 할 수 있습니다. 캐시 키를 커스텀하는 방법에 대해선 tags를 이용한 캐시키 핸들링하기을 참고해주세요.
➡️ 커스텀 키 없이도 기존의 revalidateTag
방식도 그대로 사용할 수 있습니다.
2. 요청별 저장소 선택
클라우드 스토리지를 이용한 Next.js fetch cache 관리 글에서, Redis와 Storage를 공용 저장소로 사용했을 때의 장단점을 비교해봤습니다.
Redis는 빠르지만 휘발성이고, GCS는 안정적이지만 속도가 느릴 수 있습니다. 각각의 장단점을 활용하기 위해, 요청 단위로 캐시 저장소를 선택할 수 있도록 지원합니다.
3. 캐시 최대 크기 설정
Data Cache의 최대 Max Size는 2MB입니다. 이 제한 때문에, 많은 개발자들이 고통받고 있는데요, 저 역시 이 문제 때문에 BFF에서 응답 데이터의 사이즈를 줄이기 위해 노력했던 기억이 생각납니다.
라이브러리에서는 config 단계에서 cacheMaxSize 옵션을 이용해 커스텀할 수 있습니다.
4. 네임스페이스를 통한 서비스 구분
CI/CD 또는 MSA 환경에서는 서로 다른 서비스가 하나의 캐시 저장소를 공유할 수 있습니다. 이때 캐시 키 충돌을 방지하기 위해 네임스페이스를 설정할 수 있습니다. customKey 혹은 해싱된 key의 prefix로 붙게 됩니다.
➡️ 같은 cacheKey
를 사용해도 내부적으로는 service-A:custom-key
, service-B:custom-key
형태로 구분됩니다.
마무리
이 라이브러리를 만들게 된 계기는 단순히 기술적인 필요 때문만은 아닙니다. 예전부터 File System 기반 캐시 방식의 한계를 느끼며 관련 글들을 작성해왔고, 그 고민의 연장선에서 직접 해결책을 만들고 싶었습니다.
단순히 문제를 우회하는 코드가 아닌, Next.js가 지향하는 캐싱 구조를 그대로 따르면서도 확장 가능하고 일관성 있는 방식으로 캐시를 다룰 수 있는 구조가 필요하다고 생각했습니다.
그래서 이 라이브러리는 다음과 같은 방향성을 가지고 설계되었습니다.
- 내부 동작 원리를 해치지 않으면서도 확장 가능해야 한다.
- 멀티 인스턴스 환경에서의 일관된 캐시 동기화를 가장 우선순위로 삼는다.
- 외부 시스템(CMS 등)과의 연동도 쉽게 이루어져야 한다.
- 성능과 유지보수 측면에서 무리가 없어야 한다.
더 자세한 예제 및 설정 방법은 GitHub 저장소를 참고해주세요. 라이브러리를 사용하는 과정에서 발견한 문제, 혹은 더 나은 아이디어가 있다면 언제든지 이슈나 PR로 공유해 주시면 정말 감사하겠습니다.