Full Route Cache 풀 라우트 캐시

하영·2024년 10월 6일
1

Next.js

목록 보기
8/19

Full Route Cache 풀 라우트 캐시

풀 라우트 캐시

풀라우트 캐시가 진행되는 방식은 다음과 같다.

  1. /a 라는 요청이 처음 들어오면 fetch api 를 통해 데이터가 있는지 찾는다.
  2. 메모이제이션에 저장된 값이 있는지 보고 캐싱된 데이터가 없다면 skip 하여 백엔드에 요청한다.
  3. 백엔드 서버에서는 요청한 값을 제공하고 이를 풀 라우트 캐시 한다.
  4. 또 다시 접속요청이 들어왔을 때 풀 라우트 캐시에서 캐싱된 데이터가 있는 걸 확인 후 HTML 을 바로 내보낸다.

👍 한 번 렌더링 해두면 재접속 시 빠른 로딩 속도로 데이터를 불러올 수 있어 효율적인 방식이다.


01. Next.js 에서의 Static Page, Dynamic Page

어떤 기능을 사용하느냐에 따라 자동으로 나뉘는데 풀 라우트 캐시가 적용되는 건 Static Page 즉, 정적 페이지이다.

👩🏻‍💻 Dynamic Page 로 설정되는 기준

“특정 페이지가 접속 요청을 받을 때마다 매번 변화가 생기거나, 데이터가 달라질 경우” 설정된다.

아래 조건을 만족하지 않으면 모두 Static Page로 기본값으로 설정된다.

🚨 서버 컴포넌트만 해당! 클라이언트 컴포넌트는 페이지 유형에 영향 미치지 않음!!!!


1. 캐시되지 않는 Data Fetching을 사용할 경우

async function Comp(){
	const res = await fetch("...", { cache: "no-store"});
	return <div>"캐시되지 않은 페이지"</div>;
}

2. 동적 함수(cookie, header, queryString)을 사용하는 컴포넌트가 있을 때

// 1. cookies
import { cookies } from "next/headers";

async function Comp(){
	const cookieStore = cookies();
	cosnt theme = cookieStore.get("theme");
	
	return <div>동적 함수 cookies 페이지</div>
}

// 2. headers
import { headers } from "next/headers";

async function Comp(){
	const headersList = cookies();
	cosnt authorization = headersList.get("authorization");
	
	return <div>동적 함수 headers 페이지</div>
}

// 3. queryString

async function Page({searchParams} : {searchParams : { q : string } }){
	const q = searchParams.q;
	
	return <div>동적 함수 searchParams 페이지</div>

💡 주의!

Static Page : 동적함수 NO! 데이터 캐시 YES! → 풀 라우트 캐시

Dynamic Page : 동적함수가 하나라도 있거나, 둘다 없을 경우

무조건 Static Page가 좋은 것은 아니다! 되도록이면 적용하면 좋지만 항상 Dynamic Page가 안티패턴이거나 느린 것은 아니다.


👩🏻‍💻 Static Page revalidate

Static Page에도 revalidate를 적용할 수 있다. 서버가 가동한 이후에 revalidate로 설정한 시간이 지나면 데이터 캐시가 상하게 된다. 그렇다는 뜻은 Page에 있는 데이터도 상한 데이터라는 것!

이럴 때는 데이터 캐시 뿐만 아니라 Page 자체도 새로 업데이트 해줘야한다.

  1. 우선 stale 데이터를 먼저 보여준다.
  2. 그 다음 백엔드 서버에서 업데이트 된 revalidate data 를 다시 SET 해서 데이터 캐시에 담는다.
  3. 풀라우트 캐시에 revalidate로 업데이트 된 데이터를 SET 하여 보여준다.

02. 풀 라우트 캐시 실습하기

"use client";

import { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import style from "./serachbar.module.css";

export default function Searchbar() {
  const router = useRouter();
  const searchParams = useSearchParams(); //🚨build 타임에는 검색어가 없어서 build 오류 발생!
  const [search, setSearch] = useState("");

  const q = searchParams.get("q");

  useEffect(() => {
    setSearch(q || "");
  }, [q]);
  
  //... 생략

yarn build 를 해서 실행시켜보면 아래와 같은 오류가 발생하게 된다.

오류 문구 : Generating static pages (0/6) [ ] ⨯ useSearchParams() should be wrapped in a suspense boundary at page "/search".

해석 : 정적페이지에서의 useSearchParamssuspense 경계 안에 감싸져야만 한다.

해결방법 : 이 useSearchParams가 사용되고 있는 페이지를 렌더링 시 완전히 배제되도록 하면 된다. → Suspense 사용


👩🏻‍💻 Suspense 사용하기

import { ReactNode, Suspense } from "react";
import Searchbar from "../../components/searchbar";

export default function Layout({ children }: { children: ReactNode }) {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        {/* ✅ useSearchParams가 사용된 페이지를 Suspense 리액트 내장함수로 감싸주기 */}
        <Searchbar /> {/* useSearchParams 사용된 컴포넌트 */}
      </Suspense>
      {children}
    </div>
  );
}

Suspense 라는 리액트 내장 컴포넌트를 사용해서 useSearchParams가 사용된 컴포넌트를 감싸주면 Searchbar 컴포넌트는 build 시 완전히 배제되어 오류를 막을 수 있다.



🚧 Dynamic Page 를 Static Page로 바꿔보기

현재 page들 중에서 Dynamic page 일 필요가 없는 Home 페이지를 Static page로 바꿔보려고 한다.

앞에서 설명했듯이 되도록 Static page로 만드는 것이 효율적이기 때문에! Home 에 있는 모든 코드들을 수정작업해볼 것이다.

1. root의 layout.tsx

import "./globals.css";
import Link from "next/link";
import style from "./layout.module.css";
import { BookData } from "@/types";

async function Footer() {
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_API_SERVER_URL}/book`,
    { cache: "force-cache" } // 🚧 매번 새로운 데이터를 가져올 필요없으므로 cache 수정
  );

  if (!response.ok) {
    return <footer>제작 @winterlood</footer>;
  }
  const books: BookData[] = await response.json();
  const bookCount = books.length;

  return (
    <footer>
      <div>제작 @winterlood</div>
      <div>{bookCount}개의 도서가 등록되어 있습니다.</div>
    </footer>
  );
}

export default function RootLayout({ // 🚧 RootLayout 컴포넌트에는 동적함수 없음! -> Footer check 하기
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <div className={style.container}>
          <header>
            <Link href={"/"}>📚 ONEBITE BOOKS</Link>
          </header>
          <main>{children}</main>

          <Footer />
        </div>
      </body>
    </html>
  );
}

2. with-searchbar 폴더의 layout.tsx

import { ReactNode, Suspense } from "react";
import Searchbar from "../../components/searchbar";

export default function Layout({ children }: { children: ReactNode }) {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <Searchbar />
      </Suspense>
      {children}
    </div>
  );
}

앞에서 이미 수정해준 부분이기도 하고 fetch 함수도 없기 때문에 수정할 부분 없음!


3. with-searchbar 폴더의 page.tsx

import BookItem from "@/components/book-item";
import style from "./page.module.css";
import { BookData } from "@/types";

async function AllBooks() {
  const response = await fetch( 
    `${process.env.NEXT_PUBLIC_API_SERVER_URL}/book`,
    { cache: "force-cache" } // 🚧 항상 새로운 데이터 불러올 필요 없으므로 SSG 렌더링 적용
  );

  if (!response.ok) {
    return <div>오류가 발생했습니다.</div>;
  }
  const allBooks: BookData[] = await response.json();

  return (
    <div>
      {allBooks.map((book) => (
        <BookItem key={book.id} {...book} />
      ))}
    </div>
  );
}

async function RecoBooks() {
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_API_SERVER_URL}/book/random`,
    {
      next: { // 🚧 SGR렌더링 방식 -> Static 렌더링 중 하나이므로 변경 필요없음
        revalidate: 3,
      },
    }
  );

  if (!response.ok) {
    return <div>오류가 발생했습니다.</div>;
  }

  const recoBooks: BookData[] = await response.json();

  return (
    <div>
      {recoBooks.map((book) => (
        <BookItem key={book.id} {...book} />
      ))}
    </div>
  );
}

export default function Home() {
  return (
    <div className={style.container}>
      <section>
        <h3>지금 추천하는 도서</h3>
        **<RecoBooks />** {/* check! */}
      </section>
      <section>
        <h3>등록된 모든 도서</h3>
        **<AllBooks />** {/* check! */}
      </section>
    </div>
  );
}

이렇게 수정한 후 다시 yarn build 해보면 아래 사진처럼 터미널을 확인할 수 있다.

yarn build 하면 생기는 .next 폴더를 들어가보면 html이 생성된 것도 같이 볼 수 있다.

.next > server > index.html 파일이 생성되었고 yarn start를 해보면 굉장히 빠른 속도로 렌더링이 되는 걸 확인할 수 있다.

(* 비슷한 맥락으로 not found 페이지도 Static page 이라는 것 확인!)

✅ 풀 라우트 페이지에 저장되는 Static Page라고 할지라도 데이터 fetchingrevalidate 옵션이 적용되어 있다면 그 시간마다 새로운 데이터가 업데이트 될 수 있다는 것도 함께 알아두도록 하자!


🚧 generateStaticParams() 활용하기

  • generateStaticParams? : getStaticPath 의 app router 버전이다.

Home 컴포넌트와 달리 params 같은 쿼리스트링을 사용한 컴포넌트 페이지는 어떻게 Static page로 만들 수 있을까?

간단명료하게 말하자면 이런 경우에는 Home 컴포넌트처럼 Static page로 만드는 걸 포기해야한다.

하지만 렌더링 속도를 최대한 빠르게 불러올 수 있는 방법이 있는데 그 중 generateStaticParams 를 활용해 최적화 해보는 작업을 해보았다.

export default async function Page({
  params,
}: {
  params: { id: string | string[] }; // Dynamic page
}) {
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_API_SERVER_URL}/book/${params.id}`
  );

  if (!response.ok) {
    if (response.status === 404) {
      notFound(); // 자동으로 redirect
    }
    return <div>오류가 발생했습니다.</div>;
  }

  const book = await response.json();

  const { id, title, subTitle, description, author, publisher, coverImgUrl } =
    book;
    
    
    // return 이하 코드 생략
    
}

현재 params: { id: string | string[] }부분으로 인해 이 컴포넌트는 Dynamic page이다.

사용자의 데이터를 받아와서 결과를 보여주는 부분이라 이 컴포넌트는 앞 예시처럼 Static page로 바꿀 수 없는 상황이다.


generateStaticParams 사용한 코드


export function generateStaticParams() {
  //정적인 파라미터를 생성하는 함수
  
  return [{ id: "1" }, { id: "2" }, { id: "3" }]; 
  // Static으로 build 타임에 완성이 되어서 풀라우트!
}

generateStaticParams()함수를 사용해서 build 타임에 어떤 도서들이 있는지 미리 알려주도록 하는 코드를 작성하면 렌더링 시간을 조금 줄일 수 있다.

이 말은 즉슨 정적인 파라미터를 생성하는 함수를 만들어주는 과정이다!

🚨 generateStaticParams 사용 시 주의할 점!

  1. 문자열로만 데이터를 명시해야한다.
  2. 데이터 페칭 (fetch 같은거)이 되어있는 페이지여도 정적 페이지로 렌더링 된다.

✅ Network 탭으로 결과확인

처음 5번 인덱스로 들어갔을 때는 60ms 정도 걸렸지만 다시 새로고침 후 확인 하면

6ms 로 굉장히 빠르게 데이터를 불러오고 있다는 걸 확인할 수 있다.

.next 폴더를 다시 확인했을 때도 전 후 차이를 확인할 수 있다.

profile
왕쪼랩 탈출 목표자의 코딩 공부기록

1개의 댓글

comment-user-thumbnail
2024년 10월 6일

캐시 어렵,,

답글 달기