본문 바로가기
Web Study/Next.js

Next.js로 SNS x.com 클론코딩하기 - 14

by 쿠리의일상 2024. 2. 20.

Next 의 캐시

https://nextjs.org/docs/app/building-your-application/caching

 

Building Your Application: Caching | Next.js

An overview of caching mechanisms in Next.js.

nextjs.org

공식문서 참고

넥스트13 이후의 앱 라우터부턴 서버 컴포넌트가 도입되기도 하면서, 상대적으로 프론트 서버에 부하가 많이 늘게 되었다고 합니다. 그리고 넥스트 자체에서도 성능 개선과 비용 절감을 위해 많은 정보를 캐싱하는 것이 디폴트라고 해요. 위의 문서 내용은 이러한 캐싱에 대한 메커니즘에 대해 알 수 있으니 읽어보시는 것을 추천드립니다. 이 방식들은 프론트엔드의 최적화와 관련 있기 때문에 개발이 끝난 뒤 고려해보시면 될 것 같습니다.

 

 

위처럼 총 4가지의 캐시 메커니즘이 있고, 도입하기 전 자세히 알아봐야겠죠?

 

Request Memoization

https://nextjs.org/docs/app/building-your-application/caching#request-memoization

 

Building Your Application: Caching | Next.js

An overview of caching mechanisms in Next.js.

nextjs.org

 

기존에 작성했던 코드에서 한 컴포넌트 안에서도 서버에 요청을 여러번 보낸 적이 있을 것이고, 동일한 요청임에도 불구하고 여러번 보낸 요청이 다중 요청처리가 되는 것일까 의문을 가진 적이 있으실 겁니다.

export async function generateMetadata({ params }: Props) {
  const user: User = await getUserServer({
    queryKey: ["users", params.username],
  });
  return {
    title: `${user.nickname} (${user.id}) / Z`,
    description: `${user.nickname} (${user.id}) 프로필`,
  };
}

export default async function Profile({ params }: Props) {
  const { username } = params;
  const session = await auth();
  const queryClient = new QueryClient();
  
  await queryClient.prefetchQuery({
    queryKey: ["users", username],
    queryFn: getUserServer,
  });
  await queryClient.prefetchQuery({
    queryKey: ["posts", "users", username],
    queryFn: getUserPosts,
  });
  
// ...
})

위의 예시를 보더라도 getUserServer 함수가 중복하여 처리되고 있음을 볼 수 있습니다. 이 함수 안에는 fetch API를 활용한 서버 요청이 들어있고요.

하지만 넥스트에선 이러한 중복 요청 처리를 아래처럼 자동적으로 한번만 보내게 됩니다. 그러므로 서버쪽에서 다중 요청이 되어 성능상 문제가 생기지 않을까 걱정없이 동일 요청을 여러 번 호출해도 무관합니다. 다만 request memoization의 기능은 한 페이지 내에서만 해당하는 사항이라는 점을 주의해야 합니다.

Request Memoization
페이지를 처음 렌더링할 때 존재하는 중복된 요청을 한번만 요청 처리해주는 것
Duration (캐싱 유지 기간) Revalidating(캐싱 갱신) Opting out(캐싱 안하는 법)
하나의 페이지 내에서만 해당
다른 페이지에서 요청이 들어오게 되면 캐시가 초기화되고 새롭게 요청을 처리하게 됨
매 페이지 요청마다 AbortController 클래스를 사용

const { signal } = new AbortController();
fetch(url, { signal });

 

 

Data cache

프론트 서버에서 백엔드 서버 혹은 DB 서버측에 보낸 요청을 얼마나 캐싱할 것인지, 즉 request memoization 과 달리 페이지와는 무관하게 동작합니다. 

fetch 요청 -> Data cache 확인 -> 캐싱된 요청이 있으면 -> return
fetch 요청 -> Data cache 확인 -> 캐싱된 요청이 없으면 -> data source 를 담아 Data cache에 저장 -> return

fetch 요청을 할 때 옵션으로 넣어주었던 { cache: 'no-store' } 의 의미가 바로 Data cache 를 사용하지 않겠다는 의미와 같다고 합니다. Data cache 를 사용하지 않는다는 의미는 곧 매 요청마다 항상 data source 를 읽어온다는 의미입니다.

export const getUserServer = async ({
  queryKey,
}: {
  queryKey: [string, string];
}) => {
  const [_1, username] = queryKey;
  const res = await fetch(`http://localhost:9090/api/users/${username}`, {
    next: { tags: ["users", username] },
    credentials: "include",
    headers: {
      Cookie: cookies().toString(),
    },
    cache: "no-store",
  });

  if (!res.ok) throw new Error("Failed to fetch data");

  return res.json();
};

 

Data cache
한번 보낸 요청을 프론트 서버가 얼마동안 기억할 것인가의 여부
Duration (캐싱 유지 기간) Revalidating(캐싱 갱신) Opting out(캐싱 안하는 법)
별도의 변경 사항이 없으면 계속 유지 새로 고침(재요청 처리됨)

직접 캐싱 변경 처리
{ next: { tags: [ ... ] } } 로 지정한 tag를 revalidateTag([ ... ]) 를 호출할 때마다 캐싱 갱신
또는 revalidatePath() 를 사용하여 해당 주소에 속하는 모든 캐싱을 갱신하는 방법도 있음 


일정 시간 설정 
{ next: { revalidate: 3600 } } -> 1시간 동안 캐싱된 동일한 값만 가져오게 되다가 1시간 후 갱신
{ cache: 'no-store' } -> 항상 새로운 값을 요청

이렇듯 Data cache 는 한번 요청되어 캐싱된 정보가 별도의 처리가 없으면 계속 가지고 있게 되므로 항상 최신 정보로 유지되어야 하는 페이지, 예를 들어 Z.com 에서 구현했던 게시글 목록 등은 no-store 로 지정해주는 것이 좋습니다.

하지만 블로그 글 같은 경우는 한번 게시하면 내용을 수정하는 경우가 잦지 않기 때문에 수정하는 경우만 revalidateTag/revalidatePath 등을 사용하여 캐싱을 갱신해주는 등의 처리를 해주면 됩니다.

 

라이브러리 캐싱 핸들링
// page.js
export const dynamic = 'force-dynamic'

page 파일 안에 위의 코드를 넣어주면 라이브러리에서 백엔드 서버로 오는 요청들에 대한 캐싱을 받지 않겠다는 의미가 됩니다.

 

 

Full Route Cache

Full Route Cache
미리 만들어둔 페이지(정적 페이지)들을 얼마간 캐싱할 것인가
Duration (캐싱 유지 기간) Revalidating(캐싱 갱신) Opting out(캐싱 안하는 법)
유지 데이터 캐시가 수정되면 갱신

재배포 시
{ cache: 'no-store' }
dynamic = 'force-dynamic'
revalidate = 0
위의 설정 경우 무의미 함

Dynamic Function에 해당하는 cookies, useSearchParams, headers, searchParams 등이 사용되는 페이지에선 매번 리렌더링이 되므로 캐싱이 될 수 없음

Data cache 와 밀접한 연관이 있어서 영향을 많이 받게 됩니다. 기본적으로 Full Route cache 는 정적 페이지에 의미있는 캐싱 전략입니다. 

 

 

Router cache

클라이언트에서 동작하며 컴포넌트별로 캐싱하는 방식

예를 들어 layout.js 과 page.js 에서 page 의 내용이 달라진다고 layout 쪽 내용은 그대로인데 함께 바뀌는 건 비효율적일 것입니다. 이런 경우에 사용되는 캐싱 전략입니다. 

Router cache
미리 만들어둔 페이지(정적 페이지)들을 얼마간 캐싱할 것인가
Duration (캐싱 유지 기간) Revalidating(캐싱 갱신) Opting out(캐싱 안하는 법)
유지 새로고침

시간 후 자동 갱신
static : 5min -> Dynamic Function 을 쓰지 않고 Data cache 를 쓰는 경우
dynamic: 30s -> static 이외의 경우 모두
* 다만 dynamic 이지만 static 처럼 쓰고 싶은 경우엔 prefetch 속성을 true로 넣어주면 됨 
사용하지 않는 방법은 없고, 기본으로 static 설정 됨

현재 구현한 Z.com 에서 로그아웃 후 곧바로 로그인을 하면 그 전 로그인한 계정 정보로 보이는 버그가 존재합니다. 이 버그는 라우터 캐시때문에 일어나는 현상인데, static의 경우 30s 후 갱신이기 때문에 레이아웃이 아직 static 이기 때문에 발생했습니다. 이 경우는 로그아웃할 때 router.refresh() 처리를 해줌으로써 해당 버그를 없애줄 수 있습니다.

 

 

캐싱 방식을 요약하자면

  • request memoization
    • 페이지별 동일한 fetch 요청은 한번만 추려서 보내줌
  • data cache
    • 프론트에서 백엔드/DB 서버에 요청을 보낸 후 받은 응답 결과를 캐싱(페이지와는 무관)
  • full router cache
    • 정적 페이지에서 활용될 수 있는 미리 캐싱해둔 페이지를 얼마나 캐싱하는가
  • router cache
    • 컴포넌트별 캐싱