Next.js 14 App Router를 적용한 프로젝트에서 SWR을 통해 서버 데이터 캐싱 동작을 구현했는데, 무한스크롤 구현 시 사용되는 useSWRInfinite
훅에서 개선사항을 발견했다.
발생한 이슈
서버 컴포넌트 → 무한스크롤 리스트에 대해 0과 1페이지에 대한 데이터를 받아와 SWR 캐시에 넣어 해당 페이지들에 대한 api 요청을 막고자 했으나
클라이언트 컴포넌트 → 네트워크 탭에서 해당 페이지들에 대해 API 요청을 보내고 있는 것으로 확인했다.
정리하자면, 서버 컴포넌트에서 0과 1페이지에 대한 데이터를 받아오고, 이를 클라이언트 컴포넌트로 내려서 초기 데이터로 사용한다(fallback). 이때 fallback으로 내려 SWR에 캐시되고 있음에도 0과 1 페이지에 대한 요청을 다시 보낸다. 찾아보니 SWR 구현이 그렇게 되어 있음.. 제어할 수 있는 옵션도 존재하지 않아 개선이 필요했다. useSWRInfinite
훅에서만 발생되는 문제임
useSWRImmutable
훅을 참고해서 useSWRInfinite
호출 시에 config
값으로 revalidate
하는 옵션들을 제거했다.
const options: SWRInfiniteConfiguration = {
revalidateFirstPage: false,
parallel: true,
initialSize: 2,
fallbackData: initialData,
revalidateOnMount: false, // 추가
revalidateIfStale: false, // 추가
revalidateOnFocus: false, // 추가
revalidateOnReconnect: false, // 추가
};
const { data, size, setSize, isLoading, mutate, isValidating, ...rest } = useSWRInfinite<
PageContent<T>,
FetchError
revalidateOnMount
옵션에 의해서 처음 렌더링될 때 0과 1 페이지에 대한 api 요청을 보내지 않았다. 그래서 해결된 줄 알았는데, 스크롤을 내려 2번 페이지에 대한 요청을 보낼 때 0과 1에 대한 요청도 줄줄이 보낸다..
SWR Middleware 적용
useSWRInfinite
훅이 어떤 식으로 구현되어있는지 확인하기 위해 https://github.com/vercel/swr/blob/main/src/infinite/index.ts 소스코드를 참고하던 중에 middleware
를 사용할 수 있음을 알게 되었고, 이를 적용해 훅 호출 전에 서버컴포넌트에서 fetch된 데이터인지 확인하는 로직을 추가해야겠다는 생각을 갖게 되었다.
미들웨어는 SWR hook을 받고 hook의 실행 전후에 로직을 실행할 수 있다.
// src/lib/infiniteMiddleware.ts
'use client';
import { SWRHook, useSWRConfig } from 'swr';
export const infiniteMiddleware = (useSWRNext: SWRHook) => {
const { fallback, cache } = useSWRConfig();
return (key: any, fetcher: any, config: any): any => {
const extendedFetcher = (...args: any[]) => {
const path = args[0];
if (!cache.get(path) && fallback[path]) {
return fallback[path];
} else {
return fetcher(...args);
}
};
const swr = useSWRNext(key, extendedFetcher, config);
return swr;
};
};
이제 무한스크롤 리스트에서 서버,클라이언트 간 중복 fetch가 발생하지 않는다.
추가적으로 다음 페이지를 불러오는 과정 중에 추가 스크롤 동작이 일어나면 동일 요청에 대해 중복 fetch가 발생하고 있어서 아래 조건을 추가해 useSWRInfiniteScroll
을 수정했다.
useEffect(() => {
if (!inView || !data || isValidating) return;
setSize((size) => size + 1);
}, [inView, isValidating]);