Next.js 13 App Router에서의 캐싱

윤병현·2024년 7월 24일
0

FeedB

목록 보기
8/10

캐싱이란?

캐싱(Caching)은 자주 요청되는 데이터나 연산 결과를 임시 저장해 두고, 이후에 동일한 요청이 있을 때 저장된 데이터를 재사용하여 응답 속도를 높이고 서버 자원을 절약하는 기술입니다. 이는 웹 개발에서 매우 중요한 성능 최적화 전략 중 하나입니다.

피드비 프로젝트는 Next.js 14버전 App Router 방식을 사용해 진행하고 있습니다.

Next.js App Router에 캐싱 특징은 아래와 같습니다.

1. 자동 캐싱

App Router는 서버 컴포넌트의 데이터를 자동으로 캐싱할 수 있습니다. 이 기능은 자주 변경되지 않는 데이터를 캐시하여 응답 속도를 높입니다. 예를 들어, 사용자가 자주 조회하는 블로그 게시글이나 제품 정보 등을 캐싱할 수 있습니다.

2. 캐싱 옵션

개발자는 각 컴포넌트나 데이터 페칭 로직에 대해 캐싱 옵션을 설정할 수 있습니다. 넥스트.js는 revalidate 설정을 통해 캐시된 데이터를 주기적으로 갱신할 수 있습니다. 이는 데이터가 일정 시간 동안 유효하도록 설정하고, 그 이후에는 새로운 데이터를 가져오도록 하는 방식입니다.

3. 브라우저 캐싱

Next.js는 정적 자산(이미지, CSS, JavaScript 파일 등)을 브라우저 캐시에 저장하도록 최적화할 수 있습니다. 이를 통해 사용자가 웹사이트를 재방문할 때 로딩 속도가 빨라집니다.

위와 같은 내용을 학습하고 피드비 프로젝트에서 발생한 이슈를 해결해보았습니다.

🚨 이슈 발생

사진을 보시면 새로고침을 했을 경우 프로젝트 리스트 데이터를 새로 불러와 컴포넌트가 생성되고 있습니다.
근데 맨 첫번 째 게시물이 처음에는 없다가 뒤늦게 나타나는 문제를 보실 수 있을겁니다.

🤔 원인 분석

위 문제는 Next에서 제공해주는 자동 캐싱 기능 때문에 일어난 문제입니다.

 const projectListQuery = projectQueryKeys.list({ page: 1, size: 16 });

  const { data, fetchNextPage } = useInfiniteQuery({
    queryKey: projectListQuery.queryKey,
    queryFn: ({ pageParam = 1 }) => projectApi.getProjectList({ ...projectState, page: pageParam }),
    initialPageParam: 1,
    getNextPageParam: lastPage => {
      const { customPageable } = lastPage;
      if (customPageable.hasNext) {
        return customPageable.page + 1; // 다음 페이지 번호 반환
      }
      return undefined; // 더 이상 페이지가 없으면 undefined 반환
    },
  });

현제 프로젝트 리스트 데이터는 React-Query로 불러오고 데이터를 캐싱하고 있습니다.

근데 Next.js와 React Query는 데이터 캐싱이 서로 다른 방식으로 이루어지기 때문에 캐싱된 데이터를 각각 업데이트 해줘야합니다.

넥스트.js App Router의 캐싱

서버 캐싱
서버 컴포넌트에서 데이터를 가져올 때 서버 측에서 캐싱을 할 수 있습니다. 예를 들어, 빌드 시점에 데이터를 페칭하고 이를 정적으로 캐싱하거나, revalidate 속성을 사용해 주기적으로 데이터를 갱신할 수 있습니다.

React Query의 캐싱

클라이언트 메모리 캐싱
React Query는 클라이언트 측에서 데이터 요청 결과를 캐시합니다. 기본적으로 브라우저 메모리에 저장되며, 특정 키에 따라 데이터를 관리합니다.

현재 클라이언트 메모리 캐싱된 데이터는 React Query에서 제공해주는 queryClient.invalidateQueries 메서드를 이용해서 프로젝트 리스트 데이터에 변경 사항이 생기면 캐싱된 데이터를 무효화 시켜주는 작업은 되어있는 상태입니다.

그럼 이제 Next.js가 자동으로 해준 서버 캐싱을 무효화만 시켜주면 해결되는 문제입니다.

🔆 해결 방법

Next.js에는 서버 캐싱을 무효화 시켜줄 방법이 두 가지가 있습니다.

1. revalidatePath

revalidatePath(path: string, type?: 'page' | 'layout'): void;

revalidatePath 함수는 두개의 인자를 넣어줘야합니다. 첫 번째 인자는 revalidatePath로 어떤 URL 즉 어떤 경로에 있는 캐싱된 데이터를 무효화할 것인지 사용자가 정하여 넣어주면 됩니다.
두번째는 정해진 경로에서 page 범위까지 데이터를 무효화할 것인지 아니면 layout까지 데이터를 무효화할 것인지 사용자가 결정하여 넣어주면 됩니다.

사용 예시

"use server";

import { revalidatePath } from "next/cache";

export async function revalidatePathAction(url: string, type: "page" | "layout" = "page") {
  revalidatePath(url, type);
}

저는 따로 util 함수로 빼어 다른 곳에서도 사용할 수 있도록 했습니다.
revalidatePathAction 함수를 선언해 매개변수로 URL과 type을 받을 수 있게하였습니다.

이제 URL에 "/main"을 전달하고 type에는 "page"를 전달하여 메인 페이지에 캐싱된 데이터를 초기화하여 발생했던 이슈를 해결할 수 있습니다.

그렇지만 이 방법에 대한 단점이 하나 있습니다.

단점

프로젝트 리스트 데이터 무효화 시키면 되는데 revalidatePath 함수를 사용하면 메인 페이지에 캐싱된 데이터 모두가 무효화처리 되어 갱신이 필요하지 않는 데이터까지 초기화가 되면서 이런 데이터까지 다시 캐싱되는 비효율적인 작업이 이루워지게 됩니다.

그래서 저는 두번째 방법으로 이슈를 해결했습니다.

2. revalidateTag

revalidateTag(tag: string): void;

revalidateTag가 해주는 역할은 revalidatePath와 비슷합니다. 차이점은 revalidatePath은 페이지나 레이아웃 단위로 캐싱된 데이터를 무효화 시켜주지만 revalidateTag는 캐싱된 데이터에서 사용자가 무효화 싶은 데이터 태그를 찾아 무효화 시켜주는 것입니다.

이 방법은 React-query에서 데이터를 Key로 관리하는 것과 비슷하다고 봅니다.

그러면 API 통신으로 가져온 데이터에 태그를 붙여줘야하는데 어떻게 해줄까요??

태그 지정

fetch(url, { next: { tags: [...] } });

fetch를 이용해서 데이터를 불러올 때 태그를 지정할 수 있습니다.

사용 예시

"use server";

import { revalidatePath, revalidateTag } from "next/cache";

export async function revalidatePathAction(url: string, type: "page" | "layout" = "page") {
  revalidatePath(url, type);
}

export async function revalidateTagAction(tag: string) {
  revalidateTag(tag);
}

일단 먼저 revalidateTagAction를 선언하고 인자값으로 사용자가 원하는 태그를 받아 revalidateTag함수를 사용할 수 있도록 해주었습니다.

  async function get<R>(url: string, params?: Record<string, any>, headers?: HeadersInit, tags = [""]) {
    const urlParams = new URLSearchParams(params).toString();
    const response = await fetch(`${BASE_URL}${url}?${urlParams}`, {
      headers,
      next: {
        tags: [...tags],
      },
    });
    const result: R = await response.json();
    return result;
  }

다른 사람들도 사용할 수 있게 지정할 태그를 매개변수로 받아서 지정할 수 있도록 해주었습니다.

getProjectList: async ({
    page = 1,
    size = 12,
    limit = 0,
    searchString = "",
    projectTechStacks = [],
    sortCondition = "RECENT",
  }: ProjectListParams) => {
    return await httpClient().get<ProjectResponseType>(
      "/projects",
      {
        sortCondition,
        projectTechStacks,
        searchString,
        page,
        size,
        limit,
      },
      HEADER.applicationHeaders,
      ["pojectList"]
    );
  },

그런 다음에 프로젝트 리스트를 불러오는 로직에 pojectList라는 태그 이름을 지정하여 넘겨주었습니다.

import { revalidateTagAction } from "../_utils/revalidationAction";

async function MainPage() {
  revalidateTagAction`("pojectList");
  
  {'''}
   
  return (
    <HydrationBoundary state={dehydratedState}>
      <main className="mx-auto my-16 grid w-[1200px] grid-cols-[230px_minmax(976px,_1fr)] grid-rows-[100px_minmax(800px,_1fr)]">
        <SelectStack />
      </main>
    </HydrationBoundary>
  );
}

export default MainPage;

마지막으로 메인페이지가 랜더링이 될 때 revalidateTagAction 함수를 실행시켜 사용자가 항상 최신 데이터만 볼 수 있도록 해주었습니다.

그리고 이 내용을 팀원들과 공유하여 프로젝트 게시물이 생성될 때 캐싱된 데이터를 무효화 시킬 수 있도록 할 예정입니다.

해결

참고 - Next 공식 문서
https://nextjs.org/docs/app/api-reference/functions/revalidatePath
https://nextjs.org/docs/app/api-reference/functions/revalidateTag

profile
프론드엔드 개발자

0개의 댓글

관련 채용 정보