1. 서버컴포넌트의 등장이 불러온 새로운 캐싱 국면#
1) pages router 에서의 데이터#
기존 pages router 체제에서는 getServerSideProps나 getStaticProps같은 서버측에서만 실행되는 함수를 통해서 데이터를 불러오고 페이지에게 props로 넘겨주는 방식을 사용했었죠.
최상위에서 단 한 번의 데이터 요청만 발생하는 장점이 있었으나
페이지가 실제로 이 데이터가 필요한 자식 컴포넌트에게 넘겨주기 위해서는 tanstack-query같은 라이브러리로 클라이언트에 캐싱해두어 사용하지 않는 한 prop drilling 등 DX 차원의 문제가 있었습니다.
2) app router 에서의 데이터#
이제 App Router에서는 리액트의 서버 컴포넌트를 이용하여 컴포넌트별로 데이터를 직접 페칭하는 방식을 사용합니다.
이 방식은 컴포넌트가 독립적으로 데이터를 처리하도록 만들어 prop drilling을 줄일 수 있는 등 개발 편의성을 높였지만, 그 반향으로 아래와 같은 문제가 발생할 수 있는데요.
- 여러 컴포넌트, 예약함수 등에서 같은 용도의 데이터를 중복 요청 발생
- 중복된 데이터 요청이 많아질수록 그만큼 네트워크 비용이 증가
따라서 Next는 이를 개선하고자 Request Memoization과 Data Cache 기법을 제공합니다.
편리하게도 Next는 fetch API에 기본적으로 이러한 기능들을 옵션으로 넣을 수 있게끔 해두어 간편히 사용할 수 있습니다.
하지만 저는 supabase client를 사용하고 있는데 공식문서에서는 이렇게 fetch가 아닌 DB client를 사용할 때 각 캐싱 기법을 사용하기 위해서 요구하는 함수로 감싸야했습니다.
- 뒤늦게 캐싱 확인 작업을 하면서 안 사실인데 supabase client도 리퀘스트 메모이제이션이 자동으로 적용이 되더라구요..? 하단 캐싱 적용 목차에서 자세히 보겠습니다


요구하는 함수는 위와 같고 아래부터는 캐싱 기법에 대해 알아보겠습니다.
2. Request Memoization 알아보기#
1) 현재 피드 페이지의 개선점#
현재 피드 페이지에서는 모든 카테고리 데이터를 가져오는 fetchAllCategories API를 사용 중입니다.


generateStaticParams에서 all-categories를 slug로 내려주기 위해 한 번- all-categories로 UI를 그리는
컴포넌트에서 한 번
으로 동일한 데이터임에도 중복 요청이 발생하게 됩니다.
더군다나 피드 페이지는 동적인 경로를 n개 정도 특정하여 정적으로 빌드하므로 페이지의 개수만큼 요청이 증가하는데요.
즉, /category/[slug]의 동적 경로 페이지에서 generateStaticParams가 반환할 값이 4개라고 할 때
- generateStaticParams 자체에서 요청문 1번
- 정적으로 빌드될 페이지는 4개이므로, 각 페이지당 컴포넌트에서 요청문 4번
이렇게 총 5번이 호출되는 문제가 있습니다.
물론 정적으로 빌드할 피드 페이지 수가 4개 정도로 많지 않기도 하고, API가 런타임에서 수백 수천번 호출되는 것도 아니고, 당장은 slug가 몇 십개씩 추가될 일도 없어 빌드타임에 겨우 10번 미만 정도로 호출될 것이기에 크게 문제삼지 않을 수 있습니다.
그럼에도 slug가 100개까지도 증가할 수 있는 미래를 대비(?)하여 불필요한 요청은 최대한 줄여보자는 생각과 캐싱을 적극 도입하고자 하는 마음가짐으로 임해봅시다.
2) Request Memoization이란?#

이러한 현상에 대한 개선책으로 Request Memoization이 있습니다.
컴포넌트, 페이지, 레이아웃, 일부 예약함수 등 어디서든 똑같은 API가 여러번 호출되었을 때 이를 묶어 단 한 번의 요청만 보내는 캐싱 방식입니다.
❗️ 다만 주의할 점! 리퀘스트 메모이제이션은 동일한 render pass에 한하여 발생하는 요청에 대해서만 적용됩니다.

동일한 render pass인 경우에만 리퀘스트 메모이제이션이 유효하다라는 말인데, 이 언급이 왜 중요하냐면 아래에서 볼 수 있듯

generateStaticParams는 페이지가 생성되기 전에 별도로 실행되기 때문입니다. 당연하긴 합니다.
generateStaticParams를 통해 페이지를 몇 개 생성할 지 미리 알아둬야 페이지를 생성하는 행위가 가능해지는 것이니까요.
즉, 피드 페이지에서 n개의 정적 페이지 빌드로 인해 발생하는 n번의 fetchAllCategories API 요청을 리퀘스트 메모이제이션으로 하나로 묶는 데에는 유효하나,
generateStaticParams에서 호출되는 fetchAllCategories API 요청까지는 하나로 묶지 못합니다.
페이지 생성과는 다른 render pass일 것이므로 별개로 보아 아무리 잘묶어도 적어도 2번의 요청은 발생한다는 것이죠
3. Data Cache 알아보기#

리퀘스트 메모이제이션이 동일한 render pass에 대해 중복된 API요청을 방지하고자 하는 것이라면,
데이터 캐싱은 목적 자체가 백엔드로부터 가져온 데이터 보존에 있기에 서버에 항상 남습니다.
따라서 데이터 캐싱은 설령 다른 render pass라고 하더라도 같은 API 요청이라면 1회로 줄일 수 있게 됩니다.
즉, 이론을 지금 제 상황에 대입하자면 빌드 시에 fetchAllCategories API 요청은
- 아무 캐싱기법을 사용하지 않았다면 5번
- 리퀘스트 메모이제이션을 붙였다면 2번
- 데이터 캐싱을 붙였다면 1번
이 발생할 것입니다.
4. 캐싱 기법 적용해보기#
1) 아무런 캐시도 없이 빌드해보자#

바로 빌드를 해보니 slug 개수만큼인 4개의 피드 페이지가 생성이 되었고, 그만큼의 API 요청횟수가 발생했는지 확인하기 위해 DB로그를 들어가보니..!

예상했던 결과인 DB 액세스가 5번이 아니라 2번만 로깅된 것을 볼 수 있습니다.
헛.. 당황스럽네요.
리퀘스트 메모이제이션같은 기능이 supabase client에도 자체적으로 있는 것인지, 아니면 내부적으로 fetch를 사용하여 Next가 이를 잡아 확장하고 있는 건지 리퀘스트 메모이제이션이 자동으로 적용된 모습입니다.

실제로 supabase client 라이브러리의 소스를 들어가보니 fetch를 사용하는 정황이 보이네요.
어떻게 이 메모이제이션을 제거할 수 있을지 고민해보았는데요. 공식문서에 AbortController를 쓰면 된다는 말도 있고해서 supabase client에 심어보는 등 이래저래 시도해보았으나 결국 해결책은 메모이제이션이라고 하는 매커니즘에 대한 돌파였습니다.
메모이제이션은 '같은 입력이면 같은 출력이 나온다'는 전제가 있어야 재사용할 수 있는 매커니즘입니다.
따라서 요청문 header에 요청마다 다른 값을 넣게 되면? 각 요청은 다른 입력이 되지 않을까? 라는 생각이 들더군요.

위처럼 코드를 수정한 후 다시 빌드를 한 후 DB로그를 들여다보니..!

의도했던 결과인 5건의 로그가 발견되었습니다!
supabase client 자체적인 캐싱인건지, 아니면 Next에 의해 확장된건지 당황스러웠는데 메모이제이션의 특징을 생각해서 회피해본게 잘 들어먹혔군요
2) 리퀘스트 메모이제이션을 써보자#
이제는 정말 리퀘스트 메모이제이션을 의도적으로 적용해보겠습니다.
헤더 세팅을 없애고 빌드를 해보면..!


예상했던 generateStaticParams에 의한 1건, 페이지 빌드단에서 리퀘스트 메모이제이션되어 1건의 로그까지 총 2건을 볼 수 있습니다.

참고로 제일 위에서 보았듯 명시적으로 react의 cache 함수로 요청문을 감싸서 리퀘스트 메모이제이션을 할 수도 있습니다. Next도 fetch 내부에서 이 cache 함수를 쓴 것이라고 하는데요.
저는 지금 supabase client가 의외로(?) 리퀘스트 메모이제이션을 지원해주므로 사용하지 않았으나 다른 DB 클라이언트를 사용하는 분들은 참고 부탁드립니다.
3) 데이터 캐싱까지 써보자#

역시 fetch가 아닌 supabase client처럼 DB 클라이언트를 사용할 때 Next는 데이터 캐싱 사용을 돕기 위해 unstable_cache라는 기능을 제공합니다.
최근 use cache라는 지시자 방식도 있던데, 나온지 얼마 안 된 터라 더 안정적이어보이는 unstable_cache를 사용해보겠습니다. (이름은 unstable인데 그나마 stable해서 사용하는 상황이 아이러니하네요 ㅎㅎ)

빌드를 하고 DB 로그를 보니 의도대로 1번의 로그만 남은 것을 볼 수 있습니다.
원했던 결과를 얻었습니다.
5. 리퀘스트 메모이제이션 더 자세히 보기#
적용은 완료했으나 마지막으로 살펴볼 것이 있습니다.
위에서 저희는 의아한 것을 발견했었죠. 분명히 supabase client에 아무 것도 한 것이 없었음에도 자동으로 리퀘스트 메모이제이션이 적용된 현상이었습니다. 그 원인을 파악하고자 리퀘스트 메모이제이션쪽 소스코드를 분석해보았는데요.

중복 fetch 제거 관련 파일로 보이는 dedupe-fetch.ts를 보니 위에서도 말했듯 내부적으로 React.cache로 리퀘스트 메모이제이션을 구현한다는 것의 증거(빨간 박스)를 볼 수 있고,
AbortController의 signal로 리퀘스트 메모이제이션을 제거할 수 있다는 등의 정황(파란 박스)이 여기서 보이네요

하단에 존재하는 이 부분이 중복 페칭을 하나로 묶는 곳으로 보입니다.
cloneResponse가 리퀘스트 메모이제이션의 핵심 로직으로 보이는데요. 들어가서 보면

여기서 기존에 캐시된 페칭의 응답을 만들어 변수에 할당하여 사용하는 것을 볼 수 있습니다.

이러한 중복 요청 제거 함수를 patch-fetch.ts쪽 코드에서 globalThis.fetch에 재할당하여 fetch API를 확장합니다.

그리고 app-render.ts 부분에서 이 patchFetch 함수를 호출하여 로직이 적용되게 됩니다.
이쪽 소스코드 전반을 해석하기 위한 난이도가 좀 있지만 결국 중요하게 보면 되는 것은 NextJS가 리퀘스트 메모이제이션 기능을 붙인 fetch를 저희가 사용하게 될 네이티브 fetch에 global로 재할당하고 있다는 것입니다.
따라서 저희가 아무런 작업 없이도 fetch만 사용하면 리퀘스트 메모이제이션을 누릴 수 있는 것이었고, 내부적으로 fetch를 사용하는 supabase client가 자동으로 리퀘스트 메모이제이션을 갖게 된 이유가 여기 있었군요.
내용이 좀 길어졌네요. 다음 시간에는 역시 블로그를 위해 했던 작업으로 SEO를 직/간접적으로 개선할 수 있는 작업들에 대해 작성해보려고 합니다 :)